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
How It Works
Section titled “How It Works”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() callconst data = tracker.getData(); // also dispatches to webhookData Flow
Section titled “Data Flow”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.
Sink Lifecycle
Section titled “Sink Lifecycle”- Registration — Call
.pipe(sink)to register a sink. You can register multiple sinks, and.pipe()returnsthisfor chaining. - Dispatch — When
getData()is called, all registered sinks receive the fullWriteTrackDataSchemaobject in parallel. - Isolation — Each sink runs independently. If one sink fails, the others still receive data.
- Error handling — Failed sinks emit a
pipe:errorevent.
// Register multiple sinkstracker .pipe(webhook({ url: 'https://api.example.com/typing' })) .pipe(webhook({ url: 'https://backup.example.com/typing' }));
// Handle errorstracker.on('pipe:error', (err, sink) => { console.error('Sink failed:', err);});Context Fields Optional
Section titled “Context Fields ”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});| Field | Type | Description |
|---|---|---|
userId | string | User identifier. Passed through to sinks as-is. |
contentId | string | Content or document identifier. |
metadata | Record<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" } }}The WriteTrackSink Interface
Section titled “The WriteTrackSink Interface”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.
Available Sinks
Section titled “Available Sinks”| Sink | Destination | Min version |
|---|---|---|
| Webhook | Any HTTP endpoint | — |
| Datadog | Datadog RUM | @datadog/browser-rum >=3.0.0 |
| Segment | Segment Analytics | @segment/analytics-next >=1.8.0 |
| OpenTelemetry | OpenTelemetry traces | @opentelemetry/api >=1.0.0 |
| Custom | Your own implementation | — |
Error Handling
Section titled “Error Handling”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.
Next Steps
Section titled “Next Steps”- Webhook Sink — Send data to any HTTP endpoint
- Datadog — Send to Datadog RUM
- Segment — Send to Segment Analytics
- OpenTelemetry — Send as OpenTelemetry spans
- Custom Sink — Build your own sink for any backend