Skip to content

Output Sinks

Output sinks route session data from getData() to external destinations like webhooks, analytics platforms, or custom backends.

Available sinks: Webhook | Datadog | Segment | OpenTelemetry | Custom

import { WriteTrack, webhook } from 'writetrack';
const tracker = new WriteTrack({
target: textarea,
userId: 'u_abc123',
contentId: 'post_draft_42',
});
tracker.pipe(webhook({ url: 'https://api.example.com/typing' }));
tracker.start();
// Data flows to all registered sinks on each getData() call
const data = tracker.getData(); // also dispatches to webhook

Sinks receive data when getData() is called — typically on form submit or when you’re done capturing. Every getData() call dispatches to all registered sinks. There is no once-only guard — if you call getData() three times, sinks receive data three times.

  1. Registration — Call .pipe(sink) to register a sink. You can register multiple sinks, and .pipe() returns this for chaining.
  2. Dispatch — When getData() is called, all registered sinks receive the full WriteTrackDataSchema object in parallel.
  3. Isolation — Each sink runs independently. If one sink fails, the others still receive data.
  4. Error handling — Failed sinks emit a pipe:error event.
// Register multiple sinks
tracker
.pipe(webhook({ url: 'https://api.example.com/typing' }))
.pipe(webhook({ url: 'https://backup.example.com/typing' }));
// Handle errors
tracker.on('pipe:error', (err, sink) => {
console.error('Sink failed:', err);
});

Pass context fields in the constructor to tag sessions with user and content identifiers. These flow through to getData() output and to all sinks.

const tracker = new WriteTrack({
target: textarea,
license: 'wt_live_...',
userId: 'u_abc123', // who is typing
contentId: 'post_draft_42', // what they're typing into
metadata: { formName: 'signup' }, // arbitrary tags
});
FieldTypeDescription
userIdstringUser identifier. Passed through to sinks as-is.
contentIdstringContent or document identifier.
metadataRecord<string, unknown>Arbitrary key-value pairs. Appears as custom in output.

These fields appear in the metadata section of getData() output:

{
"version": "2.1.0",
"metadata": {
"tool": { "name": "writetrack", "version": "0.5.0" },
"targetElement": "textarea",
"timestamp": "2026-02-11T12:00:00.000Z",
"duration": 45000,
"userId": "u_abc123",
"contentId": "post_draft_42",
"custom": { "formName": "signup" }
}
}

A sink is any object with a send method:

interface WriteTrackSink {
send(data: WriteTrackDataSchema): Promise<void>;
}

WriteTrack ships named sink factories for common destinations. You can also pass any object that implements this interface.

SinkDestinationMin version
WebhookAny HTTP endpoint
DatadogDatadog RUM@datadog/browser-rum >=3.0.0
SegmentSegment Analytics@segment/analytics-next >=1.8.0
OpenTelemetryOpenTelemetry traces@opentelemetry/api >=1.0.0
CustomYour own implementation

Listen for errors with the pipe:error event:

tracker.on('pipe:error', (err, sink) => {
// err: the Error thrown or rejected by the sink
// sink: the WriteTrackSink that failed
console.error('Sink delivery failed:', err);
});

Sink errors are caught via promise rejection and emitted as pipe:error events. Since the send method returns a Promise<void>, throwing inside an async send() produces a rejected promise that WriteTrack handles automatically.