Dotyc
API Reference

createEmitter

Create the client-safe emitter: typed track(), two delivery lanes, identify()/reset(), batching transport, optional identity provider.

Creates the emitter — the deliberately dumb side of Dotyc. It knows only the catalog and a transport: no adapters, no secrets, no wiring. track() is fully typed by inference from the catalog.

import { createEmitter, http } from 'dotyc'

Signature

function createEmitter<C extends Catalog>(options: {
  catalog: C
  transport: Transport
  identity?: IdentityProvider
}): Emitter<C>

Options

OptionDescription
catalogThe catalog from defineCatalog. Anchors the types of track().
transportWhere events go. Use http(url, settings) for the standard first-party endpoint, or direct(dispatcher) (from dotyc/ingest) for in-process server-side dispatch.
identityOptional identity provider (emitter facet). Plugs into your existing auth so you never call identify() manually. Without it, use manual identify() / reset().

The http transport

http('/api/ingest', {
  batchSize: 20,        // flush after N buffered events
  flushInterval: 5_000, // flush every N ms
})

Defaults: batching by size and interval, plus sendBeacon on page unload so trailing events survive navigation. Everything is a setting — nothing is hard-coded. One POST carries one Envelope containing N events.

Two lanes: durable and best-effort

The transport keeps two queues, one per delivery class, and an envelope is never mixed — the important never travels with the noise:

  • Best-effort lanedelivery: 'best-effort' events are batched lazily (size/interval settings above) and fire-and-forget: no ack tracking, sendBeacon on unload, a lost batch is acceptable by contract.
  • Durable lanedelivery: 'durable' events are flushed immediately in their own envelopes and kept in a retry queue until the ingest response acks them (accepted or legacy). Not acked — network failure, 5xx — means retry. This is the emitter half of at-least-once; the dispatcher's outbox is the server half.

This split kills head-of-line blocking by construction: a burst of best-effort volume can never delay a business fact.

Draft — subject to change: the durable lane's retry queue is in-memory today; whether it also persists locally (e.g. localStorage) across page loads is not decided. The flush policy of the two lanes may also be tuned.

Return value

An emitter with:

  • track(name, properties) — typed by inference: unknown event names and non-conforming properties are compile errors. On an http-transport emitter, source: 'trusted' events are excluded from the type entirely — trying to track one is a compile error, because trusted events may only be emitted in-process via direct(dispatcher). Fire-and-forget from the call site's perspective (no ack promise) — the durable lane's retrying happens under the hood.
  • identify({ userId, accountId, traits }) — declares the identity claim. Deduplicated: a hash of the last identity is persisted alongside anonymousId, so repeated calls with an unchanged identity (auth SDKs notify on every render/refresh) are no-ops.
  • reset() — logout: rotates sessionId and purges the persisted identity.

Every tracked event is stamped at emission with an eventId (a ULID — sortable, unique) and the schemaHash of its definition. The eventId is what makes retries safe end to end: ingest dedupes replays over a TTL window, and adapters can deduplicate at-least-once deliveries.

Examples

export const dotyc = createEmitter({
  catalog,
  transport: http('/api/ingest', { batchSize: 20, flushInterval: 5_000 }),
})

dotyc.track('feature_used', { feature: 'gantt_view' }) // OK
dotyc.track('feature_used', {})                        // compile error
dotyc.track('typo_event', { feature: 'x' })            // compile error
dotyc.track('subscription_upgraded', { plan: 'pro' })  // compile error: source 'trusted'

Server-side, skip HTTP entirely and dispatch in-process through the same wiring — this is the only emitter that can carry trusted events:

import { direct } from 'dotyc/ingest'

const dotycServer = createEmitter({ catalog, transport: direct(dispatcher) })
dotycServer.track('subscription_upgraded', { plan: 'pro' }) // OK here

Notes

  • Rejections are loud in development. The ingest response acks every event (accepted | rejected | legacy); in dev, the emitter surfaces rejected events noisily in the console instead of letting them vanish — no more debugging blind.
  • anonymousId lives in a cookie (SSR-friendly, shared across subdomains, readable by a same-origin ingest), generated and persisted automatically before any login; localStorage is the fallback when cookies are unavailable.
  • Client identity is a claim. Whatever the emitter sends is unverified (verified: false); the dispatcher resolves the real identity from the session or JWT and it wins.
  • identify() is not a catalog event. It flows through the dedicated identify channel to adapters, never through the wiring. Want business logic on login? Declare an explicit signed_in event in your catalog.

See also

On this page