Skip to content

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.

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.

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();

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;
},
};
}
// Usage
tracker.pipe(supabaseSink({ client: supabase }));
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(),
});
},
};
}
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),
},
]);
},
};
}
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`,
});
},
});

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);
});