@tenb/skill-telemetry
v0.1.4
Published
Lightweight telemetry SDK for SkillForge skills — instant or batched reporting
Readme
@tenb/skill-telemetry
Lightweight telemetry SDK for SkillForge skills. Zero dependencies, two modes.
Install
npm install @tenb/skill-telemetrySlug format
The slug must be author/skill-name — analytics endpoints require both segments. Single-segment values like dating-pilot will cause analytics to return zero results.
// ✅ Correct
createReporter('jaydy/dating-pilot');
// ❌ Wrong — analytics will show 0 calls
createReporter('dating-pilot');Instant Mode vs Batched Mode
| | Instant | Batched | |---|---|---| | When to use | Short-lived skills, one-off operations | Long-running skills, background processes | | How it works | One HTTP request per event | Buffers events, flushes every N min or when buffer is full | | Cost | Higher per-event overhead | Amortized — better for high-frequency events |
Use instant for: swipe actions, search queries, one-time API calls.
Use batched for: chat systems, polling loops, any skill that runs for minutes or longer.
Short-lived CLI tools — await flush before exit
Instant mode is fire-and-forget. If your CLI process exits immediately after calling report(), the HTTP request may not complete. Use flushAsync() to drain all in-flight requests before exiting:
const reporter = createReporter('author/my-cli');
// ... run your command ...
reporter.report({ toolName: 'run', success: true, duration: elapsed });
// Ensure delivery before exit
await reporter.flushAsync();
process.exit(0);Instant Mode
import { createReporter } from '@tenb/skill-telemetry';
const report = createReporter('author/skill-name', {
userId: 'machine-uuid-or-hashed-id', // optional but recommended
});
// Simple report
report.report({ toolName: 'search', success: true, duration: 150 });
// With business metadata
report.report({
toolName: 'search',
success: true,
duration: 150,
metadata: { resultCount: 42, query: 'typescript' },
});
// Wrap a function (automatic timing + error classification)
const result = await report.wrap('analyze', async () => {
return await doAnalysis(input);
});
// Timer handle — explicit success/fail, supports extra fields
const t = report.start('generate');
try {
const result = await generate(input);
t.success({ metadata: { tokensUsed: result.tokens } });
} catch (e) {
t.fail(e, {
errorCode: 'QUOTA_EXCEEDED',
errorMessage: 'monthly token quota reached', // sanitized, no PII
});
}
// Calling success() or fail() more than once is a no-op (one-shot semantics)Batched Mode
import { createReporter } from '@tenb/skill-telemetry';
const report = createReporter('author/skill-name', {
userId: 'machine-uuid-or-hashed-id',
});
// Start batched: buffer events, flush every 5 min or when buffer hits 50
report.startBatched();
// Enqueue events (non-blocking, auto-deduped within 1-second windows)
report.enqueue({ toolName: 'chat-reply', success: true, duration: 200 });
report.enqueue({ toolName: 'message-received', success: true, duration: 0 });
// Auto-flushes on:
// - Buffer reaches maxBufferSize (default 50)
// - Timer interval (default 5 min)
// - Process exit (SIGINT/SIGTERM/beforeExit)
// Manual flush + stop
report.stop();User Identity
Set userId at construction or later with identify():
// At construction (preferred — applies to all events)
const report = createReporter('my-skill', { userId: 'device-uuid' });
// After construction (e.g. resolved after auth)
report.identify('resolved-user-id');Use an opaque identifier (machine UUID, hashed email, session token). Never pass raw PII.
You can also set SKILLFORGE_USER_ID env var as a fallback.
Session Grouping
Every reporter instance auto-generates a sessionId (crypto.randomUUID). All events from the same instance share that session ID, so the backend can reconstruct per-session funnels (swipe → match → message).
Override it if you need cross-process session continuity:
const report = createReporter('my-skill', { sessionId: existingSessionId });Example: dating-pilot integration
import { createReporter } from '@tenb/skill-telemetry';
const telemetry = createReporter('jaydy/dating-pilot', {
userId: getMachineId(), // stable device identifier
});
// Swipe (instant mode — one-off operation)
export async function swipeWithFilter(params) {
return telemetry.wrap('swipe', async () => {
const result = await doSwipe(params);
return result;
}, { metadata: { liked: result.likedCount, skipped: result.skippedCount } });
}
// Chat system (batched mode — long-running)
export async function initChatSystem() {
telemetry.startBatched(300000); // flush every 5 min
onMessageReceived((msg) => {
telemetry.enqueue({ toolName: 'message-received', success: true, duration: 0 });
});
onAIReplySent((reply) => {
telemetry.enqueue({
toolName: 'ai-reply',
success: true,
duration: reply.latency,
metadata: { model: reply.model },
});
});
}
export async function stopChatSystem() {
telemetry.stop(); // flush remaining buffer
}Options
createReporter('slug', {
apiUrl: 'http://localhost:3002', // default: https://skillforge.ibea.ai
disabled: false, // or set SKILLFORGE_TELEMETRY=off
flushInterval: 300000, // batched mode: 5 min
maxBufferSize: 50, // batched mode: auto-flush threshold
userId: 'opaque-user-id', // per-user analytics (never PII)
sessionId: 'session-uuid', // override auto-generated session ID
});Privacy
- Only sends: skillSlug, toolName, success, duration, errorType, errorCode, errorMessage (if provided), sessionId, userId (if provided), metadata (if provided)
- Never sends: user content, API keys, personal data
errorMessageandmetadataare caller-provided — you are responsible for stripping PII before passing them- Set
SKILLFORGE_TELEMETRY=offto disable - All requests are fire-and-forget (never blocks your skill)
