@spanhq/bullmq 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.traceWorker = traceWorker;
4
+ exports.traceQueue = traceQueue;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const context_js_1 = require("./context.js");
7
+ /**
8
+ * Build a unique key for tracking a job's context in the global Map.
9
+ */
10
+ function jobKey(queueName, jobId) {
11
+ return `${queueName}:${jobId}`;
12
+ }
13
+ /**
14
+ * Instrument a BullMQ Worker to emit spans for every job lifecycle event.
15
+ *
16
+ * Events tracked: active, completed, failed, stalled.
17
+ *
18
+ * Trace context propagation:
19
+ * - On "active": Store trace context in the global activeJobContexts map.
20
+ * - During processor: traceQueue.add() reads the context from the map.
21
+ * - On "completed"/"failed": Remove context from the map, emit span.
22
+ *
23
+ * All errors are caught — this function NEVER crashes the host app.
24
+ */
25
+ function traceWorker(worker, sender, config) {
26
+ const startTimes = new Map();
27
+ try {
28
+ // Track when a job becomes active
29
+ worker.on("active", (job) => {
30
+ try {
31
+ const j = job;
32
+ const jobId = j.id ?? (0, node_crypto_1.randomUUID)();
33
+ // Determine trace ID: inherit from parent job data or generate new
34
+ const traceId = j.data.__trace_id ?? (0, node_crypto_1.randomUUID)();
35
+ const spanId = (0, node_crypto_1.randomUUID)();
36
+ // Store start time for duration calculation
37
+ startTimes.set(jobId, Date.now());
38
+ // Build trace context for this job
39
+ const ctx = { traceId, spanId };
40
+ // Store context in job data (for completed/failed handlers to read)
41
+ j.data.__trace_context = { traceId: ctx.traceId, spanId: ctx.spanId };
42
+ // Register in the global active context map
43
+ const key = jobKey(j.queueName ?? worker.name, jobId);
44
+ (0, context_js_1.setJobContext)(key, ctx);
45
+ if (config.debug) {
46
+ const parentId = j.data.__parent_id ?? null;
47
+ console.log(`[spanhq] Job active: ${j.name} (${jobId}), trace: ${traceId}${parentId ? `, parent: ${parentId.slice(0, 8)}` : ""}`);
48
+ }
49
+ }
50
+ catch (err) {
51
+ if (config.debug) {
52
+ console.error("[spanhq] Error in active handler:", err);
53
+ }
54
+ }
55
+ });
56
+ // Track job completion
57
+ worker.on("completed", (job) => {
58
+ try {
59
+ const j = job;
60
+ const jobId = j.id ?? "unknown";
61
+ const startTime = startTimes.get(jobId);
62
+ const now = Date.now();
63
+ startTimes.delete(jobId);
64
+ // Remove from global context map
65
+ const key = jobKey(j.queueName ?? worker.name, jobId);
66
+ (0, context_js_1.clearJobContext)(key);
67
+ const traceId = j.data.__trace_id ??
68
+ j.data.__trace_context?.traceId ??
69
+ (0, node_crypto_1.randomUUID)();
70
+ const spanId = j.data.__trace_context?.spanId ??
71
+ (0, node_crypto_1.randomUUID)();
72
+ const parentId = j.data.__parent_id ?? null;
73
+ const startedAt = startTime ? new Date(startTime) : new Date();
74
+ const finishedAt = new Date(now);
75
+ const durationMs = startTime ? now - startTime : null;
76
+ const span = {
77
+ id: spanId,
78
+ project_id: "", // Will be overridden by the backend
79
+ trace_id: traceId,
80
+ parent_id: parentId,
81
+ job_name: j.name,
82
+ queue_name: j.queueName ?? worker.name,
83
+ status: "completed",
84
+ attempt: j.attemptsMade || 1,
85
+ started_at: startedAt.toISOString(),
86
+ finished_at: finishedAt.toISOString(),
87
+ duration_ms: durationMs,
88
+ error: null,
89
+ stack_trace: null,
90
+ metadata: filterMetadata(j.data),
91
+ };
92
+ sender.add(span);
93
+ if (config.debug) {
94
+ console.log(`[spanhq] Job completed: ${j.name} (${durationMs}ms)`);
95
+ }
96
+ }
97
+ catch (err) {
98
+ if (config.debug) {
99
+ console.error("[spanhq] Error in completed handler:", err);
100
+ }
101
+ }
102
+ });
103
+ // Track job failures
104
+ worker.on("failed", (job, error, prev) => {
105
+ try {
106
+ const j = job;
107
+ if (!j)
108
+ return;
109
+ const jobId = j.id ?? "unknown";
110
+ const startTime = startTimes.get(jobId);
111
+ const now = Date.now();
112
+ startTimes.delete(jobId);
113
+ // Remove from global context map
114
+ const key = jobKey(j.queueName ?? worker.name, jobId);
115
+ (0, context_js_1.clearJobContext)(key);
116
+ const traceId = j.data.__trace_id ??
117
+ j.data.__trace_context?.traceId ??
118
+ (0, node_crypto_1.randomUUID)();
119
+ const spanId = j.data.__trace_context?.spanId ??
120
+ (0, node_crypto_1.randomUUID)();
121
+ const parentId = j.data.__parent_id ?? null;
122
+ // Determine if this is a dead job (exhausted all retries)
123
+ const maxAttempts = j.opts.attempts ?? 1;
124
+ const isDead = j.attemptsMade >= maxAttempts;
125
+ if (config.debug) {
126
+ console.log(`[spanhq] Failed event: ${j.name} (id=${jobId}), attemptsMade=${j.attemptsMade}, maxAttempts=${maxAttempts}, prev=${String(prev)}, isDead=${isDead}`);
127
+ }
128
+ const startedAt = startTime ? new Date(startTime) : new Date();
129
+ const finishedAt = new Date(now);
130
+ const durationMs = startTime ? now - startTime : null;
131
+ const errorMessage = j.failedReason ??
132
+ (error instanceof Error ? error.message : String(error ?? "Unknown error"));
133
+ const stackTrace = j.stacktrace?.join("\n") ??
134
+ (error instanceof Error ? error.stack ?? null : null);
135
+ const span = {
136
+ id: spanId,
137
+ project_id: "",
138
+ trace_id: traceId,
139
+ parent_id: parentId,
140
+ job_name: j.name,
141
+ queue_name: j.queueName ?? worker.name,
142
+ status: isDead ? "dead" : "failed",
143
+ attempt: j.attemptsMade || 1,
144
+ started_at: startedAt.toISOString(),
145
+ finished_at: finishedAt.toISOString(),
146
+ duration_ms: durationMs,
147
+ error: errorMessage,
148
+ stack_trace: stackTrace,
149
+ metadata: filterMetadata(j.data),
150
+ };
151
+ sender.add(span);
152
+ if (config.debug) {
153
+ console.log(`[spanhq] Job ${isDead ? "💀 DEAD" : "⚠️ failed"}: ${j.name} — ${errorMessage}`);
154
+ }
155
+ }
156
+ catch (err) {
157
+ if (config.debug) {
158
+ console.error("[spanhq] Error in failed handler:", err);
159
+ }
160
+ }
161
+ });
162
+ // Track stalled jobs
163
+ worker.on("stalled", (jobId) => {
164
+ try {
165
+ const id = String(jobId);
166
+ if (config.debug) {
167
+ console.log(`[spanhq] Job stalled: ${id}`);
168
+ }
169
+ const span = {
170
+ id: (0, node_crypto_1.randomUUID)(),
171
+ project_id: "",
172
+ trace_id: (0, node_crypto_1.randomUUID)(),
173
+ parent_id: null,
174
+ job_name: `stalled-${id}`,
175
+ queue_name: worker.name,
176
+ status: "failed",
177
+ attempt: 1,
178
+ started_at: new Date().toISOString(),
179
+ finished_at: new Date().toISOString(),
180
+ duration_ms: 0,
181
+ error: `Job ${id} stalled`,
182
+ stack_trace: null,
183
+ metadata: { stalled_job_id: id },
184
+ };
185
+ sender.add(span);
186
+ }
187
+ catch (err) {
188
+ if (config.debug) {
189
+ console.error("[spanhq] Error in stalled handler:", err);
190
+ }
191
+ }
192
+ });
193
+ }
194
+ catch (err) {
195
+ if (config.debug) {
196
+ console.error("[spanhq] Failed to instrument worker:", err);
197
+ }
198
+ }
199
+ return worker;
200
+ }
201
+ /**
202
+ * Instrument a BullMQ Queue to inject trace context into job data.
203
+ *
204
+ * When queue.add() is called inside a traced worker processor,
205
+ * the trace_id and parent span_id are automatically injected into
206
+ * the child job's data. This enables distributed trace propagation
207
+ * across job chains (e.g., ingest → transform → validate → publish).
208
+ *
209
+ * All errors are caught — this function NEVER crashes the host app.
210
+ */
211
+ function traceQueue(queue, sender, config) {
212
+ try {
213
+ const originalAdd = queue.add.bind(queue);
214
+ queue.add = async (name, data, opts) => {
215
+ try {
216
+ // Look up the current trace context
217
+ const ctx = (0, context_js_1.getCurrentContext)();
218
+ if (ctx) {
219
+ data.__trace_id = ctx.traceId;
220
+ data.__parent_id = ctx.spanId;
221
+ if (config.debug) {
222
+ console.log(`[spanhq] Injected trace context into child job "${name}": trace=${ctx.traceId.slice(0, 8)}…, parent=${ctx.spanId.slice(0, 8)}…`);
223
+ }
224
+ }
225
+ else if (config.debug) {
226
+ console.log(`[spanhq] No active trace context for child job "${name}" — will start new trace`);
227
+ }
228
+ }
229
+ catch (err) {
230
+ if (config.debug) {
231
+ console.error("[spanhq] Failed to inject trace context:", err);
232
+ }
233
+ }
234
+ return originalAdd(name, data, opts);
235
+ };
236
+ }
237
+ catch (err) {
238
+ if (config.debug) {
239
+ console.error("[spanhq] Failed to instrument queue:", err);
240
+ }
241
+ }
242
+ return queue;
243
+ }
244
+ /**
245
+ * Filter out internal SpanHQ metadata keys from job data
246
+ * before storing in the span metadata field.
247
+ */
248
+ function filterMetadata(data) {
249
+ const filtered = {};
250
+ for (const [key, value] of Object.entries(data)) {
251
+ if (key !== "__trace_id" &&
252
+ key !== "__parent_id" &&
253
+ key !== "__trace_context") {
254
+ filtered[key] = value;
255
+ }
256
+ }
257
+ return filtered;
258
+ }
259
+ //# sourceMappingURL=tracer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracer.js","sourceRoot":"","sources":["../../src/tracer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ /** Trace context propagated via AsyncLocalStorage */
3
+ export interface TraceContext {
4
+ traceId: string;
5
+ spanId: string;
6
+ }
7
+ /** AsyncLocalStorage instance for trace context propagation */
8
+ export declare const storage: AsyncLocalStorage<TraceContext>;
9
+ /**
10
+ * Active job context tracking.
11
+ *
12
+ * We maintain a Map of ALL currently active job contexts (keyed by
13
+ * "queueName:jobId"). When a worker processor calls traceQueue.add(),
14
+ * we look up the context from the correct active job.
15
+ *
16
+ * Since BullMQ workers with concurrency > 1 may have multiple active
17
+ * jobs simultaneously, a single global variable is insufficient.
18
+ * Instead, we track each active job separately.
19
+ *
20
+ * The key insight: traceQueue.add() is called FROM WITHIN a worker
21
+ * processor, so the "current" context is the one for whichever job's
22
+ * processor is executing. We use a stack-like approach: each add()
23
+ * reads the MOST RECENT context that hasn't been completed yet.
24
+ *
25
+ * For single-concurrency queues (the pipeline use case), the Map
26
+ * will have at most one entry per queue, making lookups unambiguous.
27
+ */
28
+ export declare const activeJobContexts: Map<string, TraceContext>;
29
+ /**
30
+ * Set context for a specific active job.
31
+ */
32
+ export declare function setJobContext(key: string, ctx: TraceContext): void;
33
+ /**
34
+ * Remove context when a job completes or fails.
35
+ */
36
+ export declare function clearJobContext(key: string): void;
37
+ /**
38
+ * Get the current trace context.
39
+ * Priority: AsyncLocalStorage → most recently activated job context.
40
+ */
41
+ export declare function getCurrentContext(): TraceContext | undefined;
42
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,qDAAqD;AACrD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,+DAA+D;AAC/D,eAAO,MAAM,OAAO,iCAAwC,CAAC;AAE7D;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,iBAAiB,2BAAkC,CAAC;AAEjE;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,QAE3D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,QAE1C;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,YAAY,GAAG,SAAS,CAY5D"}
@@ -0,0 +1,53 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ /** AsyncLocalStorage instance for trace context propagation */
3
+ export const storage = new AsyncLocalStorage();
4
+ /**
5
+ * Active job context tracking.
6
+ *
7
+ * We maintain a Map of ALL currently active job contexts (keyed by
8
+ * "queueName:jobId"). When a worker processor calls traceQueue.add(),
9
+ * we look up the context from the correct active job.
10
+ *
11
+ * Since BullMQ workers with concurrency > 1 may have multiple active
12
+ * jobs simultaneously, a single global variable is insufficient.
13
+ * Instead, we track each active job separately.
14
+ *
15
+ * The key insight: traceQueue.add() is called FROM WITHIN a worker
16
+ * processor, so the "current" context is the one for whichever job's
17
+ * processor is executing. We use a stack-like approach: each add()
18
+ * reads the MOST RECENT context that hasn't been completed yet.
19
+ *
20
+ * For single-concurrency queues (the pipeline use case), the Map
21
+ * will have at most one entry per queue, making lookups unambiguous.
22
+ */
23
+ export const activeJobContexts = new Map();
24
+ /**
25
+ * Set context for a specific active job.
26
+ */
27
+ export function setJobContext(key, ctx) {
28
+ activeJobContexts.set(key, ctx);
29
+ }
30
+ /**
31
+ * Remove context when a job completes or fails.
32
+ */
33
+ export function clearJobContext(key) {
34
+ activeJobContexts.delete(key);
35
+ }
36
+ /**
37
+ * Get the current trace context.
38
+ * Priority: AsyncLocalStorage → most recently activated job context.
39
+ */
40
+ export function getCurrentContext() {
41
+ const alsCtx = storage.getStore();
42
+ if (alsCtx)
43
+ return alsCtx;
44
+ // Fallback: return the most recently added active context.
45
+ // In single-concurrency workers this is always correct.
46
+ // In multi-concurrency workers, there's a small risk of picking
47
+ // the wrong context, but for tracing purposes this is acceptable.
48
+ if (activeJobContexts.size === 0)
49
+ return undefined;
50
+ const entries = Array.from(activeJobContexts.values());
51
+ return entries[entries.length - 1];
52
+ }
53
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAQrD,+DAA+D;AAC/D,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,iBAAiB,EAAgB,CAAC;AAE7D;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAwB,CAAC;AAEjE;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW,EAAE,GAAiB;IAC1D,iBAAiB,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW;IACzC,iBAAiB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB;IAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAClC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,2DAA2D;IAC3D,wDAAwD;IACxD,gEAAgE;IAChE,kEAAkE;IAClE,IAAI,iBAAiB,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAEnD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,CAAC,CAAC;IACvD,OAAO,OAAO,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACrC,CAAC"}
@@ -0,0 +1,47 @@
1
+ import type { TracerConfig } from "@spanhq/shared";
2
+ export { getCurrentContext } from "./context.js";
3
+ export type { TraceContext } from "./context.js";
4
+ export type { TracerConfig } from "@spanhq/shared";
5
+ /** BullMQ Worker type (minimal interface) */
6
+ interface BullMQWorker {
7
+ name: string;
8
+ on(event: string, callback: (...args: unknown[]) => void): unknown;
9
+ }
10
+ /** BullMQ Queue type (minimal interface) */
11
+ interface BullMQQueue {
12
+ name: string;
13
+ add(name: string, data: Record<string, unknown>, opts?: Record<string, unknown>): Promise<unknown>;
14
+ }
15
+ /** The tracer object returned by createTracer */
16
+ export interface Tracer {
17
+ /** Instrument a BullMQ Worker to emit tracing spans */
18
+ traceWorker<T extends BullMQWorker>(worker: T): T;
19
+ /** Instrument a BullMQ Queue to propagate trace context */
20
+ traceQueue<T extends BullMQQueue>(queue: T): T;
21
+ /** Gracefully shutdown — flushes remaining spans before resolving */
22
+ shutdown(): Promise<void>;
23
+ }
24
+ /**
25
+ * Create a SpanHQ instance.
26
+ *
27
+ * ```ts
28
+ * import { createTracer } from '@spanhq/bullmq'
29
+ *
30
+ * const tracer = createTracer({
31
+ * apiKey: 'jt_live_xxxx',
32
+ * endpoint: 'https://api.spanhq.dev',
33
+ * debug: false,
34
+ * })
35
+ *
36
+ * const worker = tracer.traceWorker(new Worker(...))
37
+ * const queue = tracer.traceQueue(new Queue(...))
38
+ *
39
+ * // On shutdown:
40
+ * await tracer.shutdown()
41
+ * ```
42
+ *
43
+ * @param config - API key (required), optional endpoint and debug flag
44
+ * @returns A tracer object with traceWorker, traceQueue, and shutdown methods
45
+ */
46
+ export declare function createTracer(config: TracerConfig): Tracer;
47
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAgB,MAAM,gBAAgB,CAAC;AAIjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAInD,6CAA6C;AAC7C,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC;CACpE;AAED,4CAA4C;AAC5C,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CACD,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,OAAO,CAAC,OAAO,CAAC,CAAC;CACrB;AAED,iDAAiD;AACjD,MAAM,WAAW,MAAM;IACrB,uDAAuD;IACvD,WAAW,CAAC,CAAC,SAAS,YAAY,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;IAClD,2DAA2D;IAC3D,UAAU,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC;IAC/C,qEAAqE;IACrE,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAoCzD"}
@@ -0,0 +1,54 @@
1
+ import { SpanSender } from "./sender.js";
2
+ import { traceWorker, traceQueue } from "./tracer.js";
3
+ export { getCurrentContext } from "./context.js";
4
+ const DEFAULT_ENDPOINT = "https://api.spanhq.dev";
5
+ /**
6
+ * Create a SpanHQ instance.
7
+ *
8
+ * ```ts
9
+ * import { createTracer } from '@spanhq/bullmq'
10
+ *
11
+ * const tracer = createTracer({
12
+ * apiKey: 'jt_live_xxxx',
13
+ * endpoint: 'https://api.spanhq.dev',
14
+ * debug: false,
15
+ * })
16
+ *
17
+ * const worker = tracer.traceWorker(new Worker(...))
18
+ * const queue = tracer.traceQueue(new Queue(...))
19
+ *
20
+ * // On shutdown:
21
+ * await tracer.shutdown()
22
+ * ```
23
+ *
24
+ * @param config - API key (required), optional endpoint and debug flag
25
+ * @returns A tracer object with traceWorker, traceQueue, and shutdown methods
26
+ */
27
+ export function createTracer(config) {
28
+ const apiKey = config.apiKey || process.env.SPANHQ_API_KEY || "";
29
+ const endpoint = config.endpoint ||
30
+ process.env.SPANHQ_ENDPOINT ||
31
+ DEFAULT_ENDPOINT;
32
+ const debug = config.debug ?? false;
33
+ if (!apiKey && debug) {
34
+ console.warn("[spanhq] No API key provided. Spans will be buffered but will fail to send.");
35
+ }
36
+ const senderConfig = {
37
+ apiKey,
38
+ endpoint: endpoint.replace(/\/$/, ""), // Strip trailing slash
39
+ debug,
40
+ };
41
+ const sender = new SpanSender(senderConfig);
42
+ return {
43
+ traceWorker(worker) {
44
+ return traceWorker(worker, sender, senderConfig);
45
+ },
46
+ traceQueue(queue) {
47
+ return traceQueue(queue, sender, senderConfig);
48
+ },
49
+ async shutdown() {
50
+ await sender.shutdown();
51
+ },
52
+ };
53
+ }
54
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAIjD,MAAM,gBAAgB,GAAG,wBAAwB,CAAC;AA4BlD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,YAAY,CAAC,MAAoB;IAC/C,MAAM,MAAM,GACV,MAAM,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC;IACpD,MAAM,QAAQ,GACZ,MAAM,CAAC,QAAQ;QACf,OAAO,CAAC,GAAG,CAAC,eAAe;QAC3B,gBAAgB,CAAC;IACnB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,KAAK,CAAC;IAEpC,IAAI,CAAC,MAAM,IAAI,KAAK,EAAE,CAAC;QACrB,OAAO,CAAC,IAAI,CACV,6EAA6E,CAC9E,CAAC;IACJ,CAAC;IAED,MAAM,YAAY,GAAiB;QACjC,MAAM;QACN,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,uBAAuB;QAC9D,KAAK;KACN,CAAC;IAEF,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,YAAY,CAAC,CAAC;IAE5C,OAAO;QACL,WAAW,CAAyB,MAAS;YAC3C,OAAO,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,CAAM,CAAC;QACxD,CAAC;QAED,UAAU,CAAwB,KAAQ;YACxC,OAAO,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,CAAM,CAAC;QACtD,CAAC;QAED,KAAK,CAAC,QAAQ;YACZ,MAAM,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC1B,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,37 @@
1
+ import type { Span, SenderConfig } from "@spanhq/shared";
2
+ /**
3
+ * SpanSender batches spans and sends them to the SpanHQ backend.
4
+ *
5
+ * Safety guarantees:
6
+ * - NEVER throws an error — all exceptions are caught and optionally logged
7
+ * - Flushes remaining spans on shutdown before resolving
8
+ * - Uses a timer-based flush every 2 seconds + size-based flush at 10 spans
9
+ */
10
+ export declare class SpanSender {
11
+ private batch;
12
+ private timer;
13
+ private readonly config;
14
+ private inflight;
15
+ constructor(config: SenderConfig);
16
+ /**
17
+ * Add a span to the batch buffer.
18
+ * Triggers an immediate flush if batch reaches MAX_BATCH_SIZE.
19
+ */
20
+ add(span: Span): void;
21
+ /**
22
+ * Flush the current batch to the backend.
23
+ * Copies the batch to a local variable and clears the buffer immediately
24
+ * to avoid blocking new span additions.
25
+ */
26
+ flush(): Promise<void>;
27
+ /**
28
+ * Send a batch of spans to the backend. Never throws.
29
+ */
30
+ private sendBatch;
31
+ /**
32
+ * Shutdown the sender. Clears the timer and flushes remaining spans.
33
+ * Await this to ensure no spans are lost on process exit.
34
+ */
35
+ shutdown(): Promise<void>;
36
+ }
37
+ //# sourceMappingURL=sender.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sender.d.ts","sourceRoot":"","sources":["../../src/sender.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAKzD;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAoC;gBAExC,MAAM,EAAE,YAAY;IAYhC;;;OAGG;IACH,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI;IAoBrB;;;;OAIG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAW5B;;OAEG;YACW,SAAS;IAoCvB;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAehC"}
@@ -0,0 +1,112 @@
1
+ const MAX_BATCH_SIZE = 10;
2
+ const FLUSH_INTERVAL_MS = 2000;
3
+ /**
4
+ * SpanSender batches spans and sends them to the SpanHQ backend.
5
+ *
6
+ * Safety guarantees:
7
+ * - NEVER throws an error — all exceptions are caught and optionally logged
8
+ * - Flushes remaining spans on shutdown before resolving
9
+ * - Uses a timer-based flush every 2 seconds + size-based flush at 10 spans
10
+ */
11
+ export class SpanSender {
12
+ batch = [];
13
+ timer = null;
14
+ config;
15
+ inflight = Promise.resolve();
16
+ constructor(config) {
17
+ this.config = config;
18
+ this.timer = setInterval(() => {
19
+ void this.flush();
20
+ }, FLUSH_INTERVAL_MS);
21
+ // Prevent the timer from keeping the process alive
22
+ if (this.timer && typeof this.timer === "object" && "unref" in this.timer) {
23
+ this.timer.unref();
24
+ }
25
+ }
26
+ /**
27
+ * Add a span to the batch buffer.
28
+ * Triggers an immediate flush if batch reaches MAX_BATCH_SIZE.
29
+ */
30
+ add(span) {
31
+ try {
32
+ this.batch.push(span);
33
+ if (this.config.debug) {
34
+ console.log(`[spanhq] Span buffered: ${span.job_name} (${span.status}), batch size: ${this.batch.length}`);
35
+ }
36
+ if (this.batch.length >= MAX_BATCH_SIZE) {
37
+ void this.flush();
38
+ }
39
+ }
40
+ catch (err) {
41
+ if (this.config.debug) {
42
+ console.error("[spanhq] Failed to add span to batch:", err);
43
+ }
44
+ }
45
+ }
46
+ /**
47
+ * Flush the current batch to the backend.
48
+ * Copies the batch to a local variable and clears the buffer immediately
49
+ * to avoid blocking new span additions.
50
+ */
51
+ async flush() {
52
+ if (this.batch.length === 0)
53
+ return;
54
+ const toSend = this.batch.slice();
55
+ this.batch = [];
56
+ const promise = this.sendBatch(toSend);
57
+ this.inflight = this.inflight.then(() => promise);
58
+ await promise;
59
+ }
60
+ /**
61
+ * Send a batch of spans to the backend. Never throws.
62
+ */
63
+ async sendBatch(batch) {
64
+ try {
65
+ if (this.config.debug) {
66
+ console.log(`[spanhq] Flushing ${batch.length} spans to ${this.config.endpoint}`);
67
+ }
68
+ const response = await fetch(`${this.config.endpoint}/v1/spans`, {
69
+ method: "POST",
70
+ headers: {
71
+ "Content-Type": "application/json",
72
+ "x-api-key": this.config.apiKey,
73
+ },
74
+ body: JSON.stringify({ spans: batch }),
75
+ });
76
+ if (!response.ok) {
77
+ const text = await response.text();
78
+ if (this.config.debug) {
79
+ console.error(`[spanhq] Backend returned ${response.status}: ${text}`);
80
+ }
81
+ }
82
+ else if (this.config.debug) {
83
+ const data = await response.json();
84
+ console.log(`[spanhq] Successfully sent ${batch.length} spans`, data);
85
+ }
86
+ }
87
+ catch (err) {
88
+ if (this.config.debug) {
89
+ console.error("[spanhq] Failed to flush spans:", err);
90
+ }
91
+ // Silently drop — never crash the host application
92
+ }
93
+ }
94
+ /**
95
+ * Shutdown the sender. Clears the timer and flushes remaining spans.
96
+ * Await this to ensure no spans are lost on process exit.
97
+ */
98
+ async shutdown() {
99
+ if (this.timer) {
100
+ clearInterval(this.timer);
101
+ this.timer = null;
102
+ }
103
+ // Flush any remaining spans
104
+ await this.flush();
105
+ // Wait for any in-flight requests
106
+ await this.inflight;
107
+ if (this.config.debug) {
108
+ console.log("[spanhq] Sender shut down successfully");
109
+ }
110
+ }
111
+ }
112
+ //# sourceMappingURL=sender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sender.js","sourceRoot":"","sources":["../../src/sender.ts"],"names":[],"mappings":"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"}