umami-server-tracker
v2.0.0
Published
Server-side Umami analytics client for bots, APIs, and workers. Maps users without a browser to Umami visitors, with first-class Telegram helpers.
Maintainers
Readme
umami-server-tracker
Server-side Umami client for environments that have users but no browser: Telegram bots, Discord bots, REST APIs, workers, payment webhooks.
Official @umami/node talks to /api/send. This package adds the missing visitor identity layer Umami needs when the request does not come from a page view:
- a valid
User-Agent(Umami rejects requests without one, and may drop bot-like agents) - a stable
X-Forwarded-Forderived from the full visitor id, so unique visitors stay unique CF-IPCountryinferred fromlanguage/ Telegramlanguage_code- helpers that accept a Telegraf / grammY
ctx, a Telegramfromuser, or a raw id
Zero runtime dependencies. Node.js 18+ (native fetch). ESM and CommonJS.
Install
npm install umami-server-trackerWhy not @umami/node?
@umami/node is enough for a backend that already has a real client IP and User-Agent. Bots do not. Without a synthetic visitor:
- events are stored without a country
- many users collapse into one visitor
- Umami bot detection may ignore the request even when HTTP 200 is returned
This client sends the same /api/send payload Umami expects, plus visitor headers.
Quick start
import { createUmamiTracker } from 'umami-server-tracker';
const umami = createUmamiTracker({
host: process.env.UMAMI_HOST, // https://analytics.example.com
websiteId: process.env.UMAMI_WEBSITE_ID,
hostname: 'my-api',
});
await umami.track('payment_success', {
userId: order.userId,
language: 'en-US',
data: { revenue: 4.99, currency: 'USD' },
});If host or websiteId is missing, calls become no-ops. Network errors are swallowed unless throwOnError: true.
Telegram bots
import { createTelegramTracker } from 'umami-server-tracker';
const umami = createTelegramTracker({
host: process.env.UMAMI_HOST,
websiteId: process.env.UMAMI_WEBSITE_ID,
hostname: 'gdz-tutor',
});
// Telegraf / grammY context
umami.track(ctx, 'START', { referral: true });
// Telegram `from` object
umami.track(ctx.from, 'HELP');
// Raw chat id
umami.track(ctx.from.id, 'SOLVE_OK', { cached: true });createTelegramTracker() is createUmamiTracker({ identity: 'telegram' }). It uses a Telegram-like User-Agent and a documentation-range IPv6 derived from the full signed Telegram id (users, groups, and channels never share an address).
Drop-in logEvent
Matches the helpers copied across several bots (eventType / event_type, userId / user_id, optional ctx):
umami.logEvent({
eventType: 'START',
userId: ctx.from.id,
eventProperties: { referral: true },
language: ctx.from.language_code,
});
umami.logEvent({
event_type: 'HELP',
user_id: ctx.from.id,
ctx,
});API
createUmamiTracker(options) / createTelegramTracker(options)
| Option | Default | Description |
| --- | --- | --- |
| host | | Umami origin, trailing slash optional |
| websiteId | | Website UUID |
| hostname | localhost | Reported hostname |
| timeout | 2500 | Request timeout, ms |
| identity | generic (telegram in createTelegramTracker) | telegram | generic | none |
| userAgent | built from identity | String or (userId) => string |
| ipAddress | stable documentation IPv6 from userId | String or (userId) => string |
| defaultCountry | | Used when language cannot be mapped |
| languageCountry | built-in map (uk→UA, ru→RU, …) | Overrides merged on top of defaults |
| throwOnError | false | Reject on HTTP/network errors |
| onError | | (error) => void |
| fetch | globalThis.fetch | Custom fetch |
| enabled | auto | Force on/off |
| urlFromName | (name) => '/' + name | Payload url |
| maxNameLength | 50 | Umami event name limit |
| maxDataStringLength | 500 | Truncate JSON-stringified property values |
Methods
tracker.track(name, input?)
tracker.track(input)
tracker.track(ctxOrUserId, name, data?)
tracker.logEvent(input)
tracker.pageview({ url, userId, language, data })
tracker.identify({ userId, data })
tracker.send(...) // same overloads as track, returns { ok, status, error }
tracker.sendBatch(events)
tracker.enabledtrack / logEvent / pageview never throw by default. Use send (or throwOnError) when you need the result.
Event data values must be string / number / boolean for Umami. Nested values are JSON.stringify'd and clipped.
Identity and geo
Umami attributes a visitor from IP + User-Agent, and country from GeoIP or CF-IPCountry.
Telegram Bot API ids are already unique across peer types, but they are large signed 64-bit values — not 1 and 2:
| Peer | Typical value | Bot API range |
| --- | --- | --- |
| User | 24342342342 | 1 … 1099511627775 |
| Basic group | -123456789 | -999999999999 … -1 |
| Supergroup / channel | -1002123456789 | -1997852516352 … -1000000000001 |
Numeric ids are written into the RFC 3849 documentation prefix 2001:db8::/32. The third hextet is the kind (0 users, 2 negative chats, 1 hashed string ids), then the 64-bit magnitude:
2001:db8:{kind}:0:{id 64-bit as 4 hextets}User 1 becomes 2001:db8:0:0:0:0:0:1, user 24342342342 becomes 2001:db8:0:0:0:5:aaea:aac6, group -1 becomes 2001:db8:2:0:0:0:0:1. That space is large enough that Telegram ids never wrap or alias each other. Country is not taken from that IP; it comes from language:
- region subtag (
uk-UA→UA,en-GB→GB) - language map (
uk→UA) defaultCountryif set
Override the map when a product is region-specific:
createTelegramTracker({
host,
websiteId,
hostname: 'war-alert',
defaultCountry: 'UA',
languageCountry: { ru: 'UA' },
});Self-hosted Umami
Bot traffic can still be dropped by Umami bot detection even after a 200 response. If events never appear, set DISABLE_BOT_CHECK=1 on the Umami instance, or keep the Telegram/generic User-Agent this package sends.
Cloud and self-hosted both use POST {host}/api/send. For Umami Cloud, host is https://cloud.umami.is.
TypeScript
Types are included. ctx is structurally typed — you do not need Telegraf or grammY as a dependency.
License
MIT
