Custom Sink
Any object with a send method can be a WriteTrack sink. This lets you route session data to any destination without waiting for an official integration.
The Interface
Section titled “The Interface”interface WriteTrackSink { send(data: WriteTrackDataSchema): Promise<void>;}The send method receives the full WriteTrackDataSchema object and should return a Promise<void>. Throw or reject the promise to signal failure — WriteTrack will emit a pipe:error event.
Inline Sink
Section titled “Inline Sink”The simplest approach — pass an object literal to .pipe():
import { WriteTrack } from 'writetrack';
const tracker = new WriteTrack({ target: textarea });
tracker.pipe({ send: async (data) => { await fetch('/api/typing-sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); },});
tracker.start();Sink Factory Pattern
Section titled “Sink Factory Pattern”For reusable sinks, write a factory function that returns a WriteTrackSink:
import type { WriteTrackSink } from 'writetrack';
function supabaseSink(options: { client: SupabaseClient; table?: string;}): WriteTrackSink { const { client, table = 'typing_sessions' } = options;
return { async send(data) { const { error } = await client.from(table).insert({ user_id: data.metadata.userId, content_id: data.metadata.contentId, quality_level: data.quality.qualityLevel, duration_ms: data.metadata.duration, keystroke_count: data.session.events.length, raw_data: data, created_at: data.metadata.timestamp, });
if (error) throw error; }, };}
// Usagetracker.pipe(supabaseSink({ client: supabase }));More Examples
Section titled “More Examples”Firebase Firestore
Section titled “Firebase Firestore”function firestoreSink(options: { db: Firestore; collectionName?: string;}): WriteTrackSink { const { db, collectionName = 'writetrack_sessions' } = options;
return { async send(data) { await addDoc(collection(db, collectionName), { ...data.metadata, qualityLevel: data.quality.qualityLevel, keystrokeCount: data.session.events.length, rawData: data, createdAt: serverTimestamp(), }); }, };}BigQuery
Section titled “BigQuery”function bigquerySink(options: { client: BigQuery; dataset: string; table?: string;}): WriteTrackSink { const { client, dataset, table = 'typing_sessions' } = options;
return { async send(data) { await client .dataset(dataset) .table(table) .insert([ { user_id: data.metadata.userId, quality_level: data.quality.qualityLevel, duration_ms: data.metadata.duration, event_count: data.session.events.length, timestamp: data.metadata.timestamp, payload: JSON.stringify(data), }, ]); }, };}Console Logger (for Development)
Section titled “Console Logger (for Development)”tracker.pipe({ send: async (data) => { console.log('[WriteTrack]', { quality: data.quality.qualityLevel, keystrokes: data.session.events.length, duration: `${(data.metadata.duration / 1000).toFixed(1)}s`, }); },});Error Handling
Section titled “Error Handling”If your send method returns a rejected promise (including throws inside async send()), WriteTrack catches it and emits a pipe:error event. Other sinks still receive data normally.
tracker.on('pipe:error', (err, sink) => { console.error('Custom sink failed:', err);});Next Steps
Section titled “Next Steps”- Output Sinks Overview — How the pipe system works
- Webhook Sink — Pre-built HTTP sink