autotel 4.1.0 → 4.2.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.
- package/package.json +1 -2
- package/src/attribute-redacting-processor.test.ts +0 -763
- package/src/attribute-redacting-processor.ts +0 -621
- package/src/attributes/attachers.ts +0 -161
- package/src/attributes/builders.ts +0 -529
- package/src/attributes/domains.ts +0 -42
- package/src/attributes/index.ts +0 -81
- package/src/attributes/registry.ts +0 -323
- package/src/attributes/types.ts +0 -211
- package/src/attributes/utils.ts +0 -64
- package/src/attributes/validators.ts +0 -266
- package/src/attributes.test.ts +0 -292
- package/src/auto.ts +0 -67
- package/src/autotel-logger.test.ts +0 -548
- package/src/autotel-logger.ts +0 -364
- package/src/baggage-span-processor.test.ts +0 -202
- package/src/baggage-span-processor.ts +0 -100
- package/src/business-baggage.test.ts +0 -500
- package/src/business-baggage.ts +0 -669
- package/src/circuit-breaker.test.ts +0 -341
- package/src/circuit-breaker.ts +0 -184
- package/src/config.test.ts +0 -94
- package/src/config.ts +0 -172
- package/src/correlated-events.test.ts +0 -151
- package/src/correlated-events.ts +0 -47
- package/src/correlation-id.test.ts +0 -163
- package/src/correlation-id.ts +0 -206
- package/src/db.test.ts +0 -252
- package/src/db.ts +0 -447
- package/src/decorators.test.ts +0 -153
- package/src/decorators.ts +0 -188
- package/src/define-event.test.ts +0 -41
- package/src/define-event.ts +0 -58
- package/src/devtools.ts +0 -60
- package/src/drain-pipeline.test.ts +0 -68
- package/src/drain-pipeline.ts +0 -199
- package/src/drain-toolkit.test.ts +0 -113
- package/src/drain-toolkit.ts +0 -129
- package/src/enricher-toolkit.test.ts +0 -67
- package/src/enricher-toolkit.ts +0 -79
- package/src/enrichers.test.ts +0 -150
- package/src/enrichers.ts +0 -145
- package/src/env-config.test.ts +0 -323
- package/src/env-config.ts +0 -309
- package/src/error-catalog.test.ts +0 -133
- package/src/error-catalog.ts +0 -262
- package/src/event-queue.test.ts +0 -864
- package/src/event-queue.ts +0 -699
- package/src/event-subscriber.ts +0 -262
- package/src/event-testing.ts +0 -197
- package/src/event.test.ts +0 -1104
- package/src/event.ts +0 -988
- package/src/events-config.ts +0 -235
- package/src/exporters.ts +0 -165
- package/src/filtering-span-processor.test.ts +0 -281
- package/src/filtering-span-processor.ts +0 -111
- package/src/flatten-attributes.test.ts +0 -76
- package/src/flatten-attributes.ts +0 -80
- package/src/functional.strict-types.typecheck.ts +0 -53
- package/src/functional.test.ts +0 -1464
- package/src/functional.ts +0 -2539
- package/src/functional.types.test.ts +0 -135
- package/src/hook.mjs +0 -15
- package/src/http.test.ts +0 -485
- package/src/http.ts +0 -424
- package/src/index.ts +0 -433
- package/src/init-auto-redactor.test.ts +0 -53
- package/src/init-redactor.test.ts +0 -8
- package/src/init.customization.test.ts +0 -665
- package/src/init.integrations.test.ts +0 -399
- package/src/init.openllmetry.test.ts +0 -194
- package/src/init.protocol.test.ts +0 -215
- package/src/init.ts +0 -2439
- package/src/instrumentation.test.ts +0 -108
- package/src/instrumentation.ts +0 -319
- package/src/logger.test.ts +0 -125
- package/src/logger.ts +0 -341
- package/src/messaging-adapters.test.ts +0 -595
- package/src/messaging-adapters.ts +0 -583
- package/src/messaging-testing.test.ts +0 -573
- package/src/messaging-testing.ts +0 -935
- package/src/messaging.test.ts +0 -1646
- package/src/messaging.ts +0 -2245
- package/src/metric-helpers.ts +0 -47
- package/src/metric-testing.ts +0 -197
- package/src/metric.ts +0 -446
- package/src/metrics.test.ts +0 -241
- package/src/node-require.ts +0 -123
- package/src/operation-context.ts +0 -93
- package/src/parse-error.test.ts +0 -73
- package/src/parse-error.ts +0 -112
- package/src/posthog-logs.test.ts +0 -115
- package/src/posthog-logs.ts +0 -77
- package/src/pretty-console-exporter.test.ts +0 -545
- package/src/pretty-console-exporter.ts +0 -413
- package/src/pretty-log-formatter.test.ts +0 -123
- package/src/pretty-log-formatter.ts +0 -210
- package/src/processors/canonical-log-line-processor.test.ts +0 -523
- package/src/processors/canonical-log-line-processor.ts +0 -396
- package/src/processors.ts +0 -152
- package/src/rate-limiter.test.ts +0 -199
- package/src/rate-limiter.ts +0 -98
- package/src/redact-values.test.ts +0 -90
- package/src/redact-values.ts +0 -34
- package/src/register.ts +0 -37
- package/src/request-logger.test.ts +0 -545
- package/src/request-logger.ts +0 -342
- package/src/sampling.test.ts +0 -1060
- package/src/sampling.ts +0 -737
- package/src/security-schema.test.ts +0 -45
- package/src/security-schema.ts +0 -107
- package/src/semantic-conventions.ts +0 -15
- package/src/semantic-helpers.test.ts +0 -226
- package/src/semantic-helpers.ts +0 -438
- package/src/shutdown.test.ts +0 -364
- package/src/shutdown.ts +0 -246
- package/src/span-name-normalizer.test.ts +0 -377
- package/src/span-name-normalizer.ts +0 -213
- package/src/stable-hash.ts +0 -27
- package/src/structured-error.test.ts +0 -191
- package/src/structured-error.ts +0 -157
- package/src/stub.integration.test.ts +0 -361
- package/src/tail-sampling-processor.test.ts +0 -230
- package/src/tail-sampling-processor.ts +0 -55
- package/src/test-span-collector.test.ts +0 -234
- package/src/test-span-collector.ts +0 -150
- package/src/testing.ts +0 -705
- package/src/trace-context.test.ts +0 -73
- package/src/trace-context.ts +0 -567
- package/src/trace-helpers.new.test.ts +0 -278
- package/src/trace-helpers.test.ts +0 -290
- package/src/trace-helpers.ts +0 -710
- package/src/trace-hybrid.test.ts +0 -42
- package/src/trace-hybrid.ts +0 -37
- package/src/tracer-provider.test.ts +0 -183
- package/src/tracer-provider.ts +0 -266
- package/src/track.test.ts +0 -154
- package/src/track.ts +0 -216
- package/src/validate.test.ts +0 -287
- package/src/validate.ts +0 -307
- package/src/validation-attributes.ts +0 -43
- package/src/validation.test.ts +0 -330
- package/src/validation.ts +0 -246
- package/src/variable-name-inference.test.ts +0 -178
- package/src/variable-name-inference.ts +0 -242
- package/src/webhook.test.ts +0 -649
- package/src/webhook.ts +0 -637
- package/src/workflow-distributed.test.ts +0 -786
- package/src/workflow-distributed.ts +0 -916
- package/src/workflow.async-safety.integration.test.ts +0 -345
- package/src/workflow.test.ts +0 -647
- package/src/workflow.ts +0 -810
- package/src/yaml-config.test.ts +0 -373
- package/src/yaml-config.ts +0 -351
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { defineDrain, defineHttpDrain } from './drain-toolkit';
|
|
3
|
-
|
|
4
|
-
describe('defineDrain', () => {
|
|
5
|
-
it('calls send with transformed payloads', async () => {
|
|
6
|
-
const send = vi.fn(async () => {});
|
|
7
|
-
const drain = defineDrain<
|
|
8
|
-
{ event: { id: string } },
|
|
9
|
-
{ key: string },
|
|
10
|
-
string
|
|
11
|
-
>({
|
|
12
|
-
name: 'test',
|
|
13
|
-
resolve: async () => ({ key: 'k' }),
|
|
14
|
-
transform: (contexts) => contexts.map((c) => c.event.id),
|
|
15
|
-
send,
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
await drain({ event: { id: 'a' } });
|
|
19
|
-
expect(send).toHaveBeenCalledWith(['a'], { key: 'k' });
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('skips send when resolve returns null', async () => {
|
|
23
|
-
const send = vi.fn(async () => {});
|
|
24
|
-
const drain = defineDrain({
|
|
25
|
-
name: 'test',
|
|
26
|
-
resolve: async () => null,
|
|
27
|
-
send,
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
await drain({ event: { id: 'a' } });
|
|
31
|
-
expect(send).not.toHaveBeenCalled();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('isolates send errors', async () => {
|
|
35
|
-
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
36
|
-
const drain = defineDrain({
|
|
37
|
-
name: 'test',
|
|
38
|
-
resolve: async () => ({ ok: true }),
|
|
39
|
-
send: async () => {
|
|
40
|
-
throw new Error('fail');
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
await expect(drain({ event: { id: 'a' } })).resolves.toBeUndefined();
|
|
45
|
-
expect(spy).toHaveBeenCalled();
|
|
46
|
-
spy.mockRestore();
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('defineHttpDrain', () => {
|
|
51
|
-
const originalFetch = globalThis.fetch;
|
|
52
|
-
|
|
53
|
-
beforeEach(() => {
|
|
54
|
-
vi.restoreAllMocks();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
afterEach(() => {
|
|
58
|
-
globalThis.fetch = originalFetch;
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('encodes payload and posts via fetch', async () => {
|
|
62
|
-
const fetchMock = vi.fn(async () => new Response(null, { status: 200 }));
|
|
63
|
-
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
64
|
-
|
|
65
|
-
const drain = defineHttpDrain<
|
|
66
|
-
{ event: { id: string } },
|
|
67
|
-
{ token: string },
|
|
68
|
-
{ id: string }
|
|
69
|
-
>({
|
|
70
|
-
name: 'http-drain',
|
|
71
|
-
resolve: async () => ({ token: 't' }),
|
|
72
|
-
transform: (contexts) => contexts.map((c) => c.event),
|
|
73
|
-
encode: (payloads, config) => ({
|
|
74
|
-
url: 'https://example.com/ingest',
|
|
75
|
-
headers: {
|
|
76
|
-
'content-type': 'application/json',
|
|
77
|
-
authorization: `Bearer ${config.token}`,
|
|
78
|
-
},
|
|
79
|
-
body: JSON.stringify(payloads),
|
|
80
|
-
}),
|
|
81
|
-
retries: 1,
|
|
82
|
-
timeoutMs: 2000,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
await drain({ event: { id: 'evt_1' } });
|
|
86
|
-
|
|
87
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
88
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe('https://example.com/ingest');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('retries failed requests', async () => {
|
|
92
|
-
const fetchMock = vi
|
|
93
|
-
.fn()
|
|
94
|
-
.mockRejectedValueOnce(new Error('network'))
|
|
95
|
-
.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
96
|
-
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
97
|
-
|
|
98
|
-
const drain = defineHttpDrain({
|
|
99
|
-
name: 'http-drain',
|
|
100
|
-
resolve: async () => ({ ok: true }),
|
|
101
|
-
encode: () => ({
|
|
102
|
-
url: 'https://example.com/ingest',
|
|
103
|
-
headers: { 'content-type': 'application/json' },
|
|
104
|
-
body: '[]',
|
|
105
|
-
}),
|
|
106
|
-
retries: 2,
|
|
107
|
-
timeoutMs: 2000,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
await drain({ event: { id: 'evt_1' } });
|
|
111
|
-
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
112
|
-
});
|
|
113
|
-
});
|
package/src/drain-toolkit.ts
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
export interface DrainOptions<TContext, TConfig, TPayload = TContext> {
|
|
2
|
-
/** Stable identifier used in error logs. */
|
|
3
|
-
name: string;
|
|
4
|
-
/** Return null to skip draining (e.g. missing API key in dev). */
|
|
5
|
-
resolve: () => TConfig | null | Promise<TConfig | null>;
|
|
6
|
-
/** Transform contexts into payloads. Defaults to identity. */
|
|
7
|
-
transform?: (contexts: TContext[]) => TPayload[];
|
|
8
|
-
/** Transport implementation. */
|
|
9
|
-
send: (payloads: TPayload[], config: TConfig) => Promise<void>;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface HttpDrainRequest {
|
|
13
|
-
url: string;
|
|
14
|
-
headers: Record<string, string>;
|
|
15
|
-
body: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface HttpDrainOptions<
|
|
19
|
-
TContext,
|
|
20
|
-
TConfig,
|
|
21
|
-
TPayload = TContext,
|
|
22
|
-
> extends Omit<DrainOptions<TContext, TConfig, TPayload>, 'send'> {
|
|
23
|
-
encode: (payloads: TPayload[], config: TConfig) => HttpDrainRequest | null;
|
|
24
|
-
timeoutMs?: number;
|
|
25
|
-
retries?: number;
|
|
26
|
-
resolveTimeoutMs?: (config: TConfig) => number | undefined;
|
|
27
|
-
resolveRetries?: (config: TConfig) => number | undefined;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const DEFAULT_TIMEOUT_MS = 5000;
|
|
31
|
-
const DEFAULT_RETRIES = 2;
|
|
32
|
-
|
|
33
|
-
function delay(ms: number): Promise<void> {
|
|
34
|
-
return new Promise((resolve) => {
|
|
35
|
-
const t = setTimeout(resolve, ms);
|
|
36
|
-
t.unref?.();
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function postWithRetry(options: {
|
|
41
|
-
name: string;
|
|
42
|
-
request: HttpDrainRequest;
|
|
43
|
-
timeoutMs: number;
|
|
44
|
-
retries: number;
|
|
45
|
-
}): Promise<void> {
|
|
46
|
-
const { name, request, timeoutMs, retries } = options;
|
|
47
|
-
const attempts = Math.max(1, retries);
|
|
48
|
-
let lastError: unknown;
|
|
49
|
-
|
|
50
|
-
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
51
|
-
const controller = new AbortController();
|
|
52
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
53
|
-
timeout.unref?.();
|
|
54
|
-
try {
|
|
55
|
-
const response = await fetch(request.url, {
|
|
56
|
-
method: 'POST',
|
|
57
|
-
headers: request.headers,
|
|
58
|
-
body: request.body,
|
|
59
|
-
signal: controller.signal,
|
|
60
|
-
});
|
|
61
|
-
if (!response.ok) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`[autotel/${name}] HTTP ${response.status} draining ${request.url}`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
return;
|
|
67
|
-
} catch (error) {
|
|
68
|
-
lastError = error;
|
|
69
|
-
if (attempt < attempts) {
|
|
70
|
-
await delay(100 * attempt);
|
|
71
|
-
}
|
|
72
|
-
} finally {
|
|
73
|
-
clearTimeout(timeout);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
throw lastError;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function defineDrain<TContext, TConfig, TPayload = TContext>(
|
|
81
|
-
options: DrainOptions<TContext, TConfig, TPayload>,
|
|
82
|
-
): (ctx: TContext | TContext[]) => Promise<void> {
|
|
83
|
-
return async (ctx: TContext | TContext[]) => {
|
|
84
|
-
const contexts = Array.isArray(ctx) ? ctx : [ctx];
|
|
85
|
-
if (contexts.length === 0) return;
|
|
86
|
-
|
|
87
|
-
const config = await options.resolve();
|
|
88
|
-
if (!config) return;
|
|
89
|
-
|
|
90
|
-
const payloads = options.transform
|
|
91
|
-
? options.transform(contexts)
|
|
92
|
-
: (contexts as unknown as TPayload[]);
|
|
93
|
-
|
|
94
|
-
if (payloads.length === 0) return;
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
await options.send(payloads, config);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
console.error(`[autotel/${options.name}] drain failed:`, error);
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function defineHttpDrain<TContext, TConfig, TPayload = TContext>(
|
|
105
|
-
options: HttpDrainOptions<TContext, TConfig, TPayload>,
|
|
106
|
-
): (ctx: TContext | TContext[]) => Promise<void> {
|
|
107
|
-
return defineDrain<TContext, TConfig, TPayload>({
|
|
108
|
-
name: options.name,
|
|
109
|
-
resolve: options.resolve,
|
|
110
|
-
transform: options.transform,
|
|
111
|
-
send: async (payloads, config) => {
|
|
112
|
-
const request = options.encode(payloads, config);
|
|
113
|
-
if (!request) return;
|
|
114
|
-
const timeoutMs =
|
|
115
|
-
options.resolveTimeoutMs?.(config) ??
|
|
116
|
-
options.timeoutMs ??
|
|
117
|
-
DEFAULT_TIMEOUT_MS;
|
|
118
|
-
const retries =
|
|
119
|
-
options.resolveRetries?.(config) ?? options.retries ?? DEFAULT_RETRIES;
|
|
120
|
-
|
|
121
|
-
await postWithRetry({
|
|
122
|
-
name: options.name,
|
|
123
|
-
request,
|
|
124
|
-
timeoutMs,
|
|
125
|
-
retries,
|
|
126
|
-
});
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { defineEnricher } from './enricher-toolkit';
|
|
3
|
-
|
|
4
|
-
describe('defineEnricher', () => {
|
|
5
|
-
it('merges computed values into existing field by default', () => {
|
|
6
|
-
const enricher = defineEnricher({
|
|
7
|
-
name: 'tenant-enricher',
|
|
8
|
-
field: 'tenant',
|
|
9
|
-
compute: () => ({ plan: 'pro' }),
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const ctx = {
|
|
13
|
-
event: { tenant: { id: 't_1' } },
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
enricher(ctx);
|
|
17
|
-
expect(ctx.event).toEqual({ tenant: { id: 't_1', plan: 'pro' } });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('overwrites field when overwrite=true', () => {
|
|
21
|
-
const enricher = defineEnricher(
|
|
22
|
-
{
|
|
23
|
-
name: 'tenant-enricher',
|
|
24
|
-
field: 'tenant',
|
|
25
|
-
compute: () => ({ plan: 'pro' }),
|
|
26
|
-
},
|
|
27
|
-
{ overwrite: true },
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
const ctx = {
|
|
31
|
-
event: { tenant: { id: 't_1' } },
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
enricher(ctx);
|
|
35
|
-
expect(ctx.event).toEqual({ tenant: { plan: 'pro' } });
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('skips enrichment when compute returns undefined', () => {
|
|
39
|
-
const enricher = defineEnricher({
|
|
40
|
-
name: 'noop-enricher',
|
|
41
|
-
field: 'tenant',
|
|
42
|
-
compute: () => undefined,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const ctx = { event: { a: 1 } as Record<string, unknown> };
|
|
46
|
-
enricher(ctx);
|
|
47
|
-
expect(ctx.event).toEqual({ a: 1 });
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('isolates compute errors and logs them', () => {
|
|
51
|
-
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
52
|
-
const enricher = defineEnricher({
|
|
53
|
-
name: 'broken-enricher',
|
|
54
|
-
field: 'tenant',
|
|
55
|
-
compute: () => {
|
|
56
|
-
throw new Error('boom');
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const ctx = { event: {} as Record<string, unknown> };
|
|
61
|
-
enricher(ctx);
|
|
62
|
-
|
|
63
|
-
expect(ctx.event).toEqual({});
|
|
64
|
-
expect(spy).toHaveBeenCalled();
|
|
65
|
-
spy.mockRestore();
|
|
66
|
-
});
|
|
67
|
-
});
|
package/src/enricher-toolkit.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
export interface EnrichContext<TEvent extends Record<string, unknown>> {
|
|
2
|
-
event: TEvent;
|
|
3
|
-
request?: {
|
|
4
|
-
method?: string;
|
|
5
|
-
path?: string;
|
|
6
|
-
requestId?: string;
|
|
7
|
-
};
|
|
8
|
-
response?: {
|
|
9
|
-
status?: number;
|
|
10
|
-
};
|
|
11
|
-
headers?: Record<string, string>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface EnricherDefinition<
|
|
15
|
-
TEvent extends Record<string, unknown>,
|
|
16
|
-
TValue extends object,
|
|
17
|
-
> {
|
|
18
|
-
/** Stable identifier used in error logs. */
|
|
19
|
-
name: string;
|
|
20
|
-
/** Top-level field to merge computed values into. */
|
|
21
|
-
field: keyof TEvent & string;
|
|
22
|
-
/** Return undefined to skip enrichment. */
|
|
23
|
-
compute: (ctx: EnrichContext<TEvent>) => TValue | undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface EnricherOptions {
|
|
27
|
-
/** Replace existing field value instead of merge. Default false. */
|
|
28
|
-
overwrite?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
32
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function mergeInto(
|
|
36
|
-
target: Record<string, unknown>,
|
|
37
|
-
source: Record<string, unknown>,
|
|
38
|
-
): void {
|
|
39
|
-
for (const key in source) {
|
|
40
|
-
const sourceVal = source[key];
|
|
41
|
-
if (sourceVal === undefined) continue;
|
|
42
|
-
const targetVal = target[key];
|
|
43
|
-
if (isPlainObject(sourceVal) && isPlainObject(targetVal)) {
|
|
44
|
-
mergeInto(targetVal, sourceVal);
|
|
45
|
-
} else {
|
|
46
|
-
target[key] = sourceVal;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function defineEnricher<
|
|
52
|
-
TEvent extends Record<string, unknown>,
|
|
53
|
-
TValue extends object,
|
|
54
|
-
>(
|
|
55
|
-
def: EnricherDefinition<TEvent, TValue>,
|
|
56
|
-
options: EnricherOptions = {},
|
|
57
|
-
): (ctx: EnrichContext<TEvent>) => void {
|
|
58
|
-
return (ctx: EnrichContext<TEvent>) => {
|
|
59
|
-
let computed: TValue | undefined;
|
|
60
|
-
try {
|
|
61
|
-
computed = def.compute(ctx);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
console.error(`[autotel/${def.name}] enrich failed:`, error);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (!computed) return;
|
|
68
|
-
|
|
69
|
-
if (options.overwrite || !isPlainObject(ctx.event[def.field])) {
|
|
70
|
-
(ctx.event as Record<string, unknown>)[def.field] = computed;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
mergeInto(
|
|
75
|
-
ctx.event[def.field] as unknown as Record<string, unknown>,
|
|
76
|
-
computed as unknown as Record<string, unknown>,
|
|
77
|
-
);
|
|
78
|
-
};
|
|
79
|
-
}
|
package/src/enrichers.test.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { userAgent, geo, requestSize } from './enrichers';
|
|
3
|
-
|
|
4
|
-
describe('enrichers', () => {
|
|
5
|
-
describe('userAgent', () => {
|
|
6
|
-
it('parses Chrome on macOS', () => {
|
|
7
|
-
const result = userAgent({
|
|
8
|
-
'user-agent':
|
|
9
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
expect(result).toMatchObject({
|
|
13
|
-
'user_agent.browser': 'Chrome 120.0.0.0',
|
|
14
|
-
'user_agent.os': 'macOS 10.15.7',
|
|
15
|
-
'user_agent.device': 'desktop',
|
|
16
|
-
});
|
|
17
|
-
expect(result?.['user_agent.raw']).toBeDefined();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('parses Firefox on Windows', () => {
|
|
21
|
-
const result = userAgent({
|
|
22
|
-
'user-agent':
|
|
23
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
expect(result).toMatchObject({
|
|
27
|
-
'user_agent.browser': 'Firefox 121.0',
|
|
28
|
-
'user_agent.os': 'Windows 10.0',
|
|
29
|
-
'user_agent.device': 'desktop',
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('detects mobile device', () => {
|
|
34
|
-
const result = userAgent({
|
|
35
|
-
'user-agent':
|
|
36
|
-
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1',
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
expect(result?.['user_agent.device']).toBe('mobile');
|
|
40
|
-
expect(result?.['user_agent.os']).toBe('iOS 17.2');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('detects bot', () => {
|
|
44
|
-
const result = userAgent({
|
|
45
|
-
'user-agent': 'Googlebot/2.1 (+http://www.google.com/bot.html)',
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
expect(result?.['user_agent.device']).toBe('bot');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('returns undefined when no user-agent header', () => {
|
|
52
|
-
expect(userAgent({})).toBeUndefined();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('accepts mixed-case User-Agent header names', () => {
|
|
56
|
-
const result = userAgent({
|
|
57
|
-
'User-Agent':
|
|
58
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0',
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
expect(result).toMatchObject({
|
|
62
|
-
'user_agent.browser': 'Firefox 121.0',
|
|
63
|
-
'user_agent.os': 'Windows 10.0',
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('geo', () => {
|
|
69
|
-
it('extracts Vercel geo headers', () => {
|
|
70
|
-
const result = geo({
|
|
71
|
-
'x-vercel-ip-country': 'US',
|
|
72
|
-
'x-vercel-ip-country-region': 'CA',
|
|
73
|
-
'x-vercel-ip-city': 'San%20Francisco',
|
|
74
|
-
'x-vercel-ip-latitude': '37.7749',
|
|
75
|
-
'x-vercel-ip-longitude': '-122.4194',
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
expect(result).toEqual({
|
|
79
|
-
'geo.country': 'US',
|
|
80
|
-
'geo.region': 'CA',
|
|
81
|
-
'geo.city': 'San Francisco',
|
|
82
|
-
'geo.latitude': '37.7749',
|
|
83
|
-
'geo.longitude': '-122.4194',
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('extracts Cloudflare country header', () => {
|
|
88
|
-
const result = geo({ 'cf-ipcountry': 'GB' });
|
|
89
|
-
|
|
90
|
-
expect(result).toEqual({ 'geo.country': 'GB' });
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('does not throw on malformed encoded city values', () => {
|
|
94
|
-
expect(() =>
|
|
95
|
-
geo({
|
|
96
|
-
'x-vercel-ip-country': 'US',
|
|
97
|
-
'x-vercel-ip-city': '%E0%A4%A',
|
|
98
|
-
}),
|
|
99
|
-
).not.toThrow();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('returns undefined when no geo headers', () => {
|
|
103
|
-
expect(geo({})).toBeUndefined();
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('returns longitude when it is the only geo signal', () => {
|
|
107
|
-
const result = geo({ 'x-vercel-ip-longitude': '-0.1276' });
|
|
108
|
-
|
|
109
|
-
expect(result).toEqual({ 'geo.longitude': '-0.1276' });
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('requestSize', () => {
|
|
114
|
-
it('extracts request and response sizes', () => {
|
|
115
|
-
const result = requestSize(
|
|
116
|
-
{ 'content-length': '1024' },
|
|
117
|
-
{ 'content-length': '2048' },
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
expect(result).toEqual({
|
|
121
|
-
'http.request.body.size': 1024,
|
|
122
|
-
'http.response.body.size': 2048,
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('handles request-only size', () => {
|
|
127
|
-
const result = requestSize({ 'content-length': '512' });
|
|
128
|
-
|
|
129
|
-
expect(result).toEqual({ 'http.request.body.size': 512 });
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('returns undefined when no content-length headers', () => {
|
|
133
|
-
expect(requestSize({}, {})).toBeUndefined();
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('ignores non-numeric content-length', () => {
|
|
137
|
-
expect(requestSize({ 'content-length': 'abc' })).toBeUndefined();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('ignores invalid numeric content-length values', () => {
|
|
141
|
-
expect(requestSize({ 'content-length': '-1' })).toBeUndefined();
|
|
142
|
-
expect(requestSize({ 'content-length': '12.5' })).toBeUndefined();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('ignores non-digit numeric formats for content-length', () => {
|
|
146
|
-
expect(requestSize({ 'content-length': '1e3' })).toBeUndefined();
|
|
147
|
-
expect(requestSize({ 'content-length': '+10' })).toBeUndefined();
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
});
|
package/src/enrichers.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
type Headers = Record<string, string | string[] | undefined>;
|
|
2
|
-
|
|
3
|
-
function get(headers: Headers, key: string): string | undefined {
|
|
4
|
-
const lower = key.toLowerCase();
|
|
5
|
-
for (const [k, v] of Object.entries(headers)) {
|
|
6
|
-
if (k.toLowerCase() === lower) {
|
|
7
|
-
return Array.isArray(v) ? v[0] : v;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
return undefined;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// --- User Agent ---
|
|
14
|
-
|
|
15
|
-
export interface UserAgentAttributes {
|
|
16
|
-
'user_agent.raw': string;
|
|
17
|
-
'user_agent.browser'?: string;
|
|
18
|
-
'user_agent.os'?: string;
|
|
19
|
-
'user_agent.device'?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const BROWSER_RE = /(Firefox|OPR|Edg|Chrome|Safari|MSIE|Trident)[\s/]?([\d.]*)/;
|
|
23
|
-
const OS_RE =
|
|
24
|
-
/(Windows NT|Mac OS X|Linux|Android|iPhone OS|iPad|CrOS)[\s]?([\d._]*)/;
|
|
25
|
-
|
|
26
|
-
function parseBrowser(ua: string): string | undefined {
|
|
27
|
-
const m = BROWSER_RE.exec(ua);
|
|
28
|
-
if (!m) return undefined;
|
|
29
|
-
const name =
|
|
30
|
-
m[1] === 'OPR'
|
|
31
|
-
? 'Opera'
|
|
32
|
-
: m[1] === 'Edg'
|
|
33
|
-
? 'Edge'
|
|
34
|
-
: m[1] === 'Trident'
|
|
35
|
-
? 'IE'
|
|
36
|
-
: m[1];
|
|
37
|
-
return m[2] ? `${name} ${m[2]}` : name;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseOS(ua: string): string | undefined {
|
|
41
|
-
const m = OS_RE.exec(ua);
|
|
42
|
-
if (!m) return undefined;
|
|
43
|
-
const name =
|
|
44
|
-
m[1] === 'iPhone OS'
|
|
45
|
-
? 'iOS'
|
|
46
|
-
: m[1] === 'Windows NT'
|
|
47
|
-
? 'Windows'
|
|
48
|
-
: m[1] === 'Mac OS X'
|
|
49
|
-
? 'macOS'
|
|
50
|
-
: m[1];
|
|
51
|
-
const ver = m[2]?.replaceAll('_', '.') || undefined;
|
|
52
|
-
return ver ? `${name} ${ver}` : name;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function parseDevice(ua: string): string | undefined {
|
|
56
|
-
if (/Mobi|Android.*Mobile|iPhone/.test(ua)) return 'mobile';
|
|
57
|
-
if (/iPad|Android(?!.*Mobile)|Tablet/.test(ua)) return 'tablet';
|
|
58
|
-
if (/Bot|Crawler|Spider|Lighthouse/i.test(ua)) return 'bot';
|
|
59
|
-
return 'desktop';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function userAgent(headers: Headers): UserAgentAttributes | undefined {
|
|
63
|
-
const raw = get(headers, 'user-agent');
|
|
64
|
-
if (!raw) return undefined;
|
|
65
|
-
|
|
66
|
-
const attrs: UserAgentAttributes = { 'user_agent.raw': raw };
|
|
67
|
-
const browser = parseBrowser(raw);
|
|
68
|
-
if (browser) attrs['user_agent.browser'] = browser;
|
|
69
|
-
const os = parseOS(raw);
|
|
70
|
-
if (os) attrs['user_agent.os'] = os;
|
|
71
|
-
const device = parseDevice(raw);
|
|
72
|
-
if (device) attrs['user_agent.device'] = device;
|
|
73
|
-
|
|
74
|
-
return attrs;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// --- Geo ---
|
|
78
|
-
|
|
79
|
-
export interface GeoAttributes {
|
|
80
|
-
'geo.country'?: string;
|
|
81
|
-
'geo.region'?: string;
|
|
82
|
-
'geo.city'?: string;
|
|
83
|
-
'geo.latitude'?: string;
|
|
84
|
-
'geo.longitude'?: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function geo(headers: Headers): GeoAttributes | undefined {
|
|
88
|
-
const country =
|
|
89
|
-
get(headers, 'x-vercel-ip-country') ?? get(headers, 'cf-ipcountry');
|
|
90
|
-
const region = get(headers, 'x-vercel-ip-country-region');
|
|
91
|
-
const city = get(headers, 'x-vercel-ip-city');
|
|
92
|
-
const latitude = get(headers, 'x-vercel-ip-latitude');
|
|
93
|
-
const longitude = get(headers, 'x-vercel-ip-longitude');
|
|
94
|
-
|
|
95
|
-
if (!country && !region && !city && !latitude && !longitude) return undefined;
|
|
96
|
-
|
|
97
|
-
const attrs: GeoAttributes = {};
|
|
98
|
-
if (country) attrs['geo.country'] = country;
|
|
99
|
-
if (region) attrs['geo.region'] = region;
|
|
100
|
-
if (city) {
|
|
101
|
-
try {
|
|
102
|
-
attrs['geo.city'] = decodeURIComponent(city);
|
|
103
|
-
} catch {
|
|
104
|
-
attrs['geo.city'] = city;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (latitude) attrs['geo.latitude'] = latitude;
|
|
108
|
-
if (longitude) attrs['geo.longitude'] = longitude;
|
|
109
|
-
|
|
110
|
-
return attrs;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// --- Request Size ---
|
|
114
|
-
|
|
115
|
-
export interface RequestSizeAttributes {
|
|
116
|
-
'http.request.body.size'?: number;
|
|
117
|
-
'http.response.body.size'?: number;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const DIGITS_RE = /^\d+$/;
|
|
121
|
-
|
|
122
|
-
function parseContentLength(value: string | undefined): number | undefined {
|
|
123
|
-
if (!value || !DIGITS_RE.test(value)) return undefined;
|
|
124
|
-
return Number(value);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function requestSize(
|
|
128
|
-
requestHeaders: Headers,
|
|
129
|
-
responseHeaders?: Headers,
|
|
130
|
-
): RequestSizeAttributes | undefined {
|
|
131
|
-
const reqLen = get(requestHeaders, 'content-length');
|
|
132
|
-
const resLen = responseHeaders
|
|
133
|
-
? get(responseHeaders, 'content-length')
|
|
134
|
-
: undefined;
|
|
135
|
-
|
|
136
|
-
if (!reqLen && !resLen) return undefined;
|
|
137
|
-
|
|
138
|
-
const attrs: RequestSizeAttributes = {};
|
|
139
|
-
const reqBytes = parseContentLength(reqLen);
|
|
140
|
-
if (reqBytes !== undefined) attrs['http.request.body.size'] = reqBytes;
|
|
141
|
-
const resBytes = parseContentLength(resLen);
|
|
142
|
-
if (resBytes !== undefined) attrs['http.response.body.size'] = resBytes;
|
|
143
|
-
|
|
144
|
-
return Object.keys(attrs).length > 0 ? attrs : undefined;
|
|
145
|
-
}
|