autotel-edge 3.0.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/chunk-F32WSLNX.js +309 -0
  4. package/dist/chunk-F32WSLNX.js.map +1 -0
  5. package/dist/events.d.ts +86 -0
  6. package/dist/events.js +157 -0
  7. package/dist/events.js.map +1 -0
  8. package/dist/index.d.ts +326 -0
  9. package/dist/index.js +921 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/logger.d.ts +89 -0
  12. package/dist/logger.js +81 -0
  13. package/dist/logger.js.map +1 -0
  14. package/dist/sampling.d.ts +166 -0
  15. package/dist/sampling.js +108 -0
  16. package/dist/sampling.js.map +1 -0
  17. package/dist/testing.d.ts +2 -0
  18. package/dist/testing.js +3 -0
  19. package/dist/testing.js.map +1 -0
  20. package/dist/types-Dj85cPUj.d.ts +182 -0
  21. package/package.json +88 -0
  22. package/src/api/logger.test.ts +367 -0
  23. package/src/api/logger.ts +197 -0
  24. package/src/compose.ts +243 -0
  25. package/src/core/buffer.ts +16 -0
  26. package/src/core/config.test.ts +388 -0
  27. package/src/core/config.ts +167 -0
  28. package/src/core/context.ts +224 -0
  29. package/src/core/exporter.ts +99 -0
  30. package/src/core/provider.ts +45 -0
  31. package/src/core/span.ts +222 -0
  32. package/src/core/spanprocessor.test.ts +521 -0
  33. package/src/core/spanprocessor.ts +232 -0
  34. package/src/core/trace-context.ts +66 -0
  35. package/src/core/tracer.test.ts +123 -0
  36. package/src/core/tracer.ts +216 -0
  37. package/src/events/index.test.ts +242 -0
  38. package/src/events/index.ts +338 -0
  39. package/src/events.ts +6 -0
  40. package/src/functional.test.ts +702 -0
  41. package/src/functional.ts +846 -0
  42. package/src/index.ts +81 -0
  43. package/src/logger.ts +13 -0
  44. package/src/sampling/index.test.ts +297 -0
  45. package/src/sampling/index.ts +276 -0
  46. package/src/sampling.ts +6 -0
  47. package/src/testing/index.ts +9 -0
  48. package/src/testing.ts +6 -0
  49. package/src/types.ts +267 -0
@@ -0,0 +1,242 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { context as otelContext, trace } from '@opentelemetry/api';
3
+ import { createEdgeSubscribers, getEdgeSubscribers } from './index';
4
+ import { getActiveConfig, parseConfig, setConfig } from '../core/config';
5
+
6
+ describe('createEdgeSubscribers', () => {
7
+ it('dispatches events with service name and attributes', () => {
8
+ const transport = vi.fn();
9
+ const Subscribers = createEdgeSubscribers({
10
+ service: 'edge-service',
11
+ transport,
12
+ includeTraceContext: false,
13
+ });
14
+
15
+ Subscribers.trackEvent('user.signup', { plan: 'pro' });
16
+
17
+ expect(transport).toHaveBeenCalledTimes(1);
18
+ const event = transport.mock.calls[0][0];
19
+ expect(event.type).toBe('event');
20
+ expect(event.event).toBe('user.signup');
21
+ expect(event.service).toBe('edge-service');
22
+ expect(event.attributes).toEqual({ plan: 'pro' });
23
+ expect(event.timestamp).toBeGreaterThan(0);
24
+ });
25
+
26
+ it('passes promise to waitUntil when delivery is fire-and-forget', async () => {
27
+ const waitUntil = vi.fn();
28
+ const Subscribers = createEdgeSubscribers({
29
+ service: 'edge',
30
+ transport: () => Promise.resolve(),
31
+ waitUntil,
32
+ });
33
+
34
+ Subscribers.trackEvent('user.signup');
35
+
36
+ expect(waitUntil).toHaveBeenCalledTimes(1);
37
+ const promise = waitUntil.mock.calls[0][0];
38
+ await promise;
39
+ });
40
+
41
+ it('returns promise when delivery is await', async () => {
42
+ const Subscribers = createEdgeSubscribers({
43
+ service: 'edge',
44
+ transport: () => Promise.resolve(),
45
+ delivery: 'await',
46
+ });
47
+
48
+ const result = Subscribers.trackEvent('user.signup');
49
+
50
+ expect(result).toBeInstanceOf(Promise);
51
+ await result;
52
+ });
53
+
54
+ it('logs trace context when span is active', () => {
55
+ const transport = vi.fn();
56
+ const Subscribers = createEdgeSubscribers({
57
+ service: 'edge',
58
+ transport,
59
+ });
60
+
61
+ const tracer = trace.getTracer('edge-test');
62
+ tracer.startActiveSpan('test-span', (span) => {
63
+ Subscribers.trackEvent('user.signup');
64
+ span.end();
65
+ });
66
+
67
+ const event = transport.mock.calls[0][0];
68
+ expect(event.traceId).toMatch(/^[0-9a-f]{32}$/);
69
+ expect(event.spanId).toMatch(/^[0-9a-f]{16}$/);
70
+ expect(event.correlationId).toBe(event.traceId?.slice(0, 16));
71
+ });
72
+
73
+ it('uses active config service name when available', () => {
74
+ const transport = vi.fn();
75
+ const Subscribers = createEdgeSubscribers({
76
+ transport,
77
+ includeTraceContext: false,
78
+ });
79
+
80
+ const resolved = parseConfig({
81
+ service: { name: 'from-config' },
82
+ spanProcessors: [],
83
+ });
84
+ const configContext = setConfig(resolved);
85
+
86
+ otelContext.with(configContext, () => {
87
+ Subscribers.trackEvent('user.signup');
88
+ });
89
+
90
+ const event = transport.mock.calls[0][0];
91
+ expect(event.service).toBe('from-config');
92
+ expect(getActiveConfig()).toBeNull();
93
+ });
94
+
95
+ it('invokes onError handler for rejected transports in fire-and-forget mode', async () => {
96
+ const waitUntil = vi.fn((promise: Promise<void>) => {
97
+ void promise;
98
+ });
99
+ const onError = vi.fn();
100
+ const Subscribers = createEdgeSubscribers({
101
+ service: 'edge',
102
+ transport: () => Promise.reject(new Error('boom')),
103
+ waitUntil,
104
+ onError,
105
+ });
106
+
107
+ Subscribers.trackEvent('user.signup');
108
+
109
+ await vi.waitFor(() => {
110
+ expect(onError).toHaveBeenCalledTimes(1);
111
+ });
112
+ });
113
+
114
+ it('supports binding waitUntil for reuse', async () => {
115
+ const waitUntil = vi.fn((promise: Promise<void>) => {
116
+ void promise;
117
+ });
118
+ const transport = vi.fn(() => Promise.resolve());
119
+ const Subscribers = createEdgeSubscribers({
120
+ service: 'edge',
121
+ transport,
122
+ });
123
+
124
+ const bound = Subscribers.bind({ waitUntil });
125
+ bound.trackEvent('user.signup');
126
+
127
+ expect(waitUntil).toHaveBeenCalledTimes(1);
128
+ const promise = waitUntil.mock.calls[0][0];
129
+ await promise;
130
+ });
131
+
132
+ it('merges multiple bindings', async () => {
133
+ const waitUntil = vi.fn((promise: Promise<void>) => {
134
+ void promise;
135
+ });
136
+ const transport = vi.fn(() => Promise.resolve());
137
+ const Subscribers = createEdgeSubscribers({
138
+ service: 'edge',
139
+ transport,
140
+ });
141
+
142
+ const bound = Subscribers.bind({ waitUntil }).bind({ delivery: 'await' });
143
+ const result = bound.trackEvent('user.signup');
144
+
145
+ expect(result).toBeInstanceOf(Promise);
146
+ await result;
147
+ });
148
+ });
149
+
150
+ describe('getEdgeSubscribers', () => {
151
+ it('returns null when subscribers are not configured', () => {
152
+ const resolved = parseConfig({
153
+ service: { name: 'test' },
154
+ spanProcessors: [],
155
+ });
156
+ const configContext = setConfig(resolved);
157
+
158
+ otelContext.with(configContext, () => {
159
+ const Subscribers = getEdgeSubscribers();
160
+ expect(Subscribers).toBeNull();
161
+ });
162
+ });
163
+
164
+ it('creates Subscribers instance from config subscribers', () => {
165
+ const transport1 = vi.fn();
166
+ const transport2 = vi.fn();
167
+ const resolved = parseConfig({
168
+ service: { name: 'test' },
169
+ spanProcessors: [],
170
+ subscribers: [transport1, transport2],
171
+ });
172
+ const configContext = setConfig(resolved);
173
+
174
+ otelContext.with(configContext, () => {
175
+ const Subscribers = getEdgeSubscribers();
176
+ expect(Subscribers).not.toBeNull();
177
+ Subscribers!.trackEvent('user.signup', { plan: 'pro' });
178
+
179
+ expect(transport1).toHaveBeenCalledTimes(1);
180
+ expect(transport2).toHaveBeenCalledTimes(1);
181
+ const event = transport1.mock.calls[0][0];
182
+ expect(event.type).toBe('event');
183
+ expect(event.event).toBe('user.signup');
184
+ expect(event.service).toBe('test');
185
+ });
186
+ });
187
+
188
+ it('binds waitUntil from ExecutionContext', async () => {
189
+ const waitUntil = vi.fn((promise: Promise<void>) => {
190
+ void promise;
191
+ });
192
+ const transport = vi.fn(() => Promise.resolve());
193
+ const resolved = parseConfig({
194
+ service: { name: 'test' },
195
+ spanProcessors: [],
196
+ subscribers: [transport],
197
+ });
198
+ const configContext = setConfig(resolved);
199
+
200
+ const mockCtx = { waitUntil } as ExecutionContext;
201
+
202
+ await otelContext.with(configContext, async () => {
203
+ const Subscribers = getEdgeSubscribers(mockCtx);
204
+ expect(Subscribers).not.toBeNull();
205
+ Subscribers!.trackEvent('user.signup');
206
+
207
+ expect(waitUntil).toHaveBeenCalledTimes(1);
208
+ const promise = waitUntil.mock.calls[0][0];
209
+ await promise;
210
+ });
211
+ });
212
+
213
+ it('logs subscriber errors without interrupting other subscribers', async () => {
214
+ const successAdapter = vi.fn(() => Promise.resolve());
215
+ const failingAdapter = vi.fn(() => Promise.reject(new Error('boom')));
216
+ const resolved = parseConfig({
217
+ service: { name: 'test' },
218
+ spanProcessors: [],
219
+ subscribers: [successAdapter, failingAdapter],
220
+ });
221
+ const configContext = setConfig(resolved);
222
+
223
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
224
+
225
+ await otelContext.with(configContext, async () => {
226
+ const Subscribers = getEdgeSubscribers();
227
+ expect(Subscribers).not.toBeNull();
228
+ await Subscribers!.trackEvent('user.signup', undefined, { delivery: 'await' });
229
+ });
230
+
231
+ expect(successAdapter).toHaveBeenCalledTimes(1);
232
+ expect(failingAdapter).toHaveBeenCalledTimes(1);
233
+ expect(consoleSpy).toHaveBeenCalledWith(
234
+ '[autotel-edge] Subscribers subscriber failed',
235
+ expect.any(Error),
236
+ expect.objectContaining({ subscriberIndex: expect.any(Number), eventType: 'event' }),
237
+ );
238
+
239
+ consoleSpy.mockRestore();
240
+ });
241
+ });
242
+
@@ -0,0 +1,338 @@
1
+ import { trace } from '@opentelemetry/api';
2
+ import { getActiveConfig } from '../core/config';
3
+ import type {
4
+ OrPromise,
5
+ EdgeEvent,
6
+ FunnelStepStatus,
7
+ OutcomeStatus,
8
+ EdgeTrackEvent,
9
+ EdgeFunnelStepEvent,
10
+ EdgeOutcomeEvent,
11
+ EdgeValueEvent,
12
+ EdgeEventBase,
13
+ } from '../types';
14
+
15
+ export type SubscriberDeliveryMode = 'fire-and-forget' | 'await';
16
+
17
+ // Re-export event types for convenience
18
+
19
+
20
+ export type EdgeTransport = (event: EdgeEvent) => OrPromise<void>;
21
+
22
+ export interface EdgeDispatchOptions {
23
+ delivery?: SubscriberDeliveryMode;
24
+ waitUntil?: (promise: Promise<void>) => void;
25
+ }
26
+
27
+ export interface CreateEdgeSubscribersOptions {
28
+ /**
29
+ * User-supplied transport invoked for every Subscribers event.
30
+ * Implementations can call PostHog, Stripe, Zapier, Durable Objects, etc.
31
+ */
32
+ transport: EdgeTransport;
33
+ /**
34
+ * Optional service name used when no request config is active.
35
+ * Defaults to the active instrumentation config's service name or "edge-service".
36
+ */
37
+ service?: string;
38
+ /**
39
+ * Default delivery behaviour for transport promises.
40
+ * "fire-and-forget" (default) will not block the calling code.
41
+ */
42
+ delivery?: SubscriberDeliveryMode;
43
+ /**
44
+ * Default waitUntil handler (Cloudflare Workers, Vercel waitUntil).
45
+ * Used when delivery === "fire-and-forget".
46
+ */
47
+ waitUntil?: (promise: Promise<void>) => void;
48
+ /**
49
+ * Optional error handler invoked when the transport rejects.
50
+ */
51
+ onError?: (error: unknown, event: EdgeEvent) => void;
52
+ /**
53
+ * Include OpenTelemetry trace/span identifiers in the payload. Default true.
54
+ */
55
+ includeTraceContext?: boolean;
56
+ }
57
+
58
+ export interface EdgeSubscribers {
59
+ trackEvent(
60
+ event: string,
61
+ attributes?: Record<string, unknown>,
62
+ options?: EdgeDispatchOptions,
63
+ ): OrPromise<void>;
64
+ trackFunnelStep(
65
+ funnel: string,
66
+ status: FunnelStepStatus,
67
+ attributes?: Record<string, unknown>,
68
+ options?: EdgeDispatchOptions,
69
+ ): OrPromise<void>;
70
+ trackOutcome(
71
+ operation: string,
72
+ outcome: OutcomeStatus,
73
+ attributes?: Record<string, unknown>,
74
+ options?: EdgeDispatchOptions,
75
+ ): OrPromise<void>;
76
+ trackValue(
77
+ metric: string,
78
+ value: number,
79
+ attributes?: Record<string, unknown>,
80
+ options?: EdgeDispatchOptions,
81
+ ): OrPromise<void>;
82
+ dispatch(event: EdgeEvent, options?: EdgeDispatchOptions): OrPromise<void>;
83
+ bind(options: EdgeDispatchOptions): EdgeSubscribers;
84
+ }
85
+
86
+ const DEFAULT_SERVICE_NAME = 'edge-service';
87
+
88
+ /**
89
+ * Extract a normalized event name from any EdgeEvent.
90
+ * Useful for subscribers that need to send events to events platforms.
91
+ *
92
+ * @deprecated Use `event.name` directly instead - it's now a property on all events.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const subscriber: EdgeSubscribersAdapter = async (event) => {
97
+ * await sendToEvents(event.name, event.attributes) // Use event.name directly
98
+ * }
99
+ * ```
100
+ */
101
+ export function getEventName(event: EdgeEvent): string {
102
+ return event.name;
103
+ }
104
+
105
+ function isPromiseLike(value: unknown): value is Promise<unknown> {
106
+ return typeof value === 'object' && value !== null && 'then' in value;
107
+ }
108
+
109
+ function createBaseEvent(
110
+ attributes: Record<string, unknown> | undefined,
111
+ options: CreateEdgeSubscribersOptions,
112
+ ): Omit<EdgeEventBase, 'name'> {
113
+ const config = getActiveConfig();
114
+ // Prioritize explicit service override, then config service name, then default
115
+ const serviceName = options.service ?? config?.service.name ?? DEFAULT_SERVICE_NAME;
116
+ const baseAttributes = attributes ? { ...attributes } : {};
117
+
118
+ const baseEvent: Omit<EdgeEventBase, 'name'> = {
119
+ service: serviceName,
120
+ timestamp: Date.now(),
121
+ attributes: baseAttributes,
122
+ };
123
+
124
+ if (options.includeTraceContext ?? true) {
125
+ const span = trace.getActiveSpan();
126
+ const spanContext = span?.spanContext();
127
+ if (spanContext) {
128
+ baseEvent.traceId = spanContext.traceId;
129
+ baseEvent.spanId = spanContext.spanId;
130
+ baseEvent.correlationId = spanContext.traceId.slice(0, 16);
131
+ }
132
+ }
133
+
134
+ return baseEvent;
135
+ }
136
+
137
+ function handleError(
138
+ error: unknown,
139
+ event: EdgeEvent,
140
+ options: CreateEdgeSubscribersOptions,
141
+ ): void {
142
+ if (options.onError) {
143
+ try {
144
+ options.onError(error, event);
145
+ return;
146
+ } catch (handlerError) {
147
+ console.error('[autotel-edge] Subscribers onError handler failed', handlerError);
148
+ }
149
+ }
150
+
151
+ console.error('[autotel-edge] Subscribers transport failed', error, { event });
152
+ }
153
+
154
+ function deliverResult(
155
+ result: OrPromise<void>,
156
+ event: EdgeEvent,
157
+ delivery: SubscriberDeliveryMode,
158
+ waitUntil: EdgeDispatchOptions['waitUntil'],
159
+ createOptions: CreateEdgeSubscribersOptions,
160
+ ): OrPromise<void> {
161
+ if (!isPromiseLike(result)) {
162
+ return delivery === 'await' ? Promise.resolve() : undefined;
163
+ }
164
+
165
+ if (delivery === 'await') {
166
+ return Promise.resolve(result).catch((error) => {
167
+ handleError(error, event, createOptions);
168
+ throw error;
169
+ });
170
+ }
171
+
172
+ const background = Promise.resolve(result).catch((error) => {
173
+ handleError(error, event, createOptions);
174
+ });
175
+
176
+ if (waitUntil) {
177
+ waitUntil(background);
178
+ } else {
179
+ void background;
180
+ }
181
+
182
+ return undefined;
183
+ }
184
+
185
+ function createSubscribersInstance(
186
+ options: CreateEdgeSubscribersOptions,
187
+ bindings: EdgeDispatchOptions = {},
188
+ ): EdgeSubscribers {
189
+ const dispatch = (
190
+ event: EdgeEvent,
191
+ callOptions?: EdgeDispatchOptions,
192
+ ): OrPromise<void> => {
193
+ const delivery =
194
+ callOptions?.delivery ??
195
+ bindings.delivery ??
196
+ options.delivery ??
197
+ ('fire-and-forget' as SubscriberDeliveryMode);
198
+ const waitUntil = callOptions?.waitUntil ?? bindings.waitUntil ?? options.waitUntil;
199
+ const result = options.transport(event);
200
+ return deliverResult(result, event, delivery, waitUntil, options);
201
+ };
202
+
203
+ const trackEvent: EdgeSubscribers['trackEvent'] = (eventName, attributes, callOptions) => {
204
+ const baseEvent = createBaseEvent(attributes, options);
205
+ const event: EdgeTrackEvent = {
206
+ ...baseEvent,
207
+ type: 'event',
208
+ event: eventName,
209
+ name: eventName,
210
+ } as EdgeTrackEvent;
211
+ return dispatch(event, callOptions);
212
+ };
213
+
214
+ const trackFunnelStep: EdgeSubscribers['trackFunnelStep'] = (
215
+ funnel,
216
+ status,
217
+ attributes,
218
+ callOptions,
219
+ ) => {
220
+ const baseEvent = createBaseEvent(attributes, options);
221
+ const event: EdgeFunnelStepEvent = {
222
+ ...baseEvent,
223
+ type: 'funnel-step',
224
+ funnel,
225
+ status,
226
+ name: `funnel-step.${status}`,
227
+ } as EdgeFunnelStepEvent;
228
+ return dispatch(event, callOptions);
229
+ };
230
+
231
+ const trackOutcome: EdgeSubscribers['trackOutcome'] = (
232
+ operation,
233
+ outcome,
234
+ attributes,
235
+ callOptions,
236
+ ) => {
237
+ const baseEvent = createBaseEvent(attributes, options);
238
+ const event: EdgeOutcomeEvent = {
239
+ ...baseEvent,
240
+ type: 'outcome',
241
+ operation,
242
+ outcome,
243
+ name: `outcome.${outcome}`,
244
+ } as EdgeOutcomeEvent;
245
+ return dispatch(event, callOptions);
246
+ };
247
+
248
+ const trackValue: EdgeSubscribers['trackValue'] = (
249
+ metric,
250
+ value,
251
+ attributes,
252
+ callOptions,
253
+ ) => {
254
+ const baseEvent = createBaseEvent(attributes, options);
255
+ const event: EdgeValueEvent = {
256
+ ...baseEvent,
257
+ type: 'value',
258
+ metric,
259
+ value,
260
+ name: `value.${metric}`,
261
+ } as EdgeValueEvent;
262
+ return dispatch(event, callOptions);
263
+ };
264
+
265
+ return {
266
+ trackEvent,
267
+ trackFunnelStep,
268
+ trackOutcome,
269
+ trackValue,
270
+ dispatch,
271
+ bind: (nextBindings: EdgeDispatchOptions) =>
272
+ createSubscribersInstance(options, { ...bindings, ...nextBindings }),
273
+ };
274
+ }
275
+
276
+ export function createEdgeSubscribers(options: CreateEdgeSubscribersOptions): EdgeSubscribers {
277
+ if (typeof options.transport !== 'function') {
278
+ throw new TypeError('createEdgeSubscribers: options.transport is required');
279
+ }
280
+
281
+ return createSubscribersInstance(options);
282
+ }
283
+
284
+ /**
285
+ * Get Subscribers instance from active config, bound to the current ExecutionContext.
286
+ * Returns null if Subscribers is not configured.
287
+ *
288
+ * @example
289
+ * ```typescript
290
+ * export default {
291
+ * async fetch(request, env, ctx) {
292
+ * const Subscribers = getEdgeSubscribers(ctx);
293
+ * if (Subscribers) {
294
+ * Subscribers.trackEvent('user.signup', { plan: 'pro' });
295
+ * }
296
+ * return new Response('ok');
297
+ * }
298
+ * }
299
+ * ```
300
+ */
301
+ export function getEdgeSubscribers(
302
+ ctx?: { waitUntil(promise: Promise<any>): void },
303
+ ): EdgeSubscribers | null {
304
+ const config = getActiveConfig();
305
+ if (!config) {
306
+ return null;
307
+ }
308
+
309
+ const subscribers = config.subscribers ?? [];
310
+ if (subscribers.length === 0) {
311
+ return null;
312
+ }
313
+
314
+ // Combine all subscribers into a single transport function
315
+ const combinedTransport: EdgeTransport = async (event) => {
316
+ await Promise.all(
317
+ subscribers.map(async (subscriber, index) => {
318
+ try {
319
+ await subscriber(event);
320
+ } catch (error) {
321
+ console.error('[autotel-edge] Subscribers subscriber failed', error, {
322
+ subscriberIndex: index,
323
+ eventType: event.type,
324
+ });
325
+ }
326
+ }),
327
+ );
328
+ };
329
+
330
+ return createEdgeSubscribers({
331
+ transport: combinedTransport,
332
+ service: config.service.name,
333
+ waitUntil: ctx ? (promise) => ctx.waitUntil(promise) : undefined,
334
+ });
335
+ }
336
+
337
+
338
+ export {type FunnelStepStatus, type OutcomeStatus, type EdgeEvent, type EdgeTrackEvent, type EdgeFunnelStepEvent, type EdgeOutcomeEvent, type EdgeValueEvent} from '../types';
package/src/events.ts ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Events system for edge runtimes
3
+ * Entry point: autotel-edge/events
4
+ */
5
+
6
+ export * from './events/index';