@echovisionlab/geul-telemetry 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.
Files changed (58) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +38 -0
  3. package/fixtures/audit-records.json +316 -0
  4. package/fixtures/collaboration-checkpoint-entity-types.json +15 -0
  5. package/fixtures/domain-audit-actions.json +132 -0
  6. package/fixtures/domain-audit-wire-parity.json +1751 -0
  7. package/fixtures/http-request-results.json +21 -0
  8. package/fixtures/job-catalog.json +23 -0
  9. package/fixtures/locale-content-audit.json +126 -0
  10. package/fixtures/log-redaction.json +72 -0
  11. package/fixtures/request-record.json +14 -0
  12. package/fixtures/security-access-records.json +61 -0
  13. package/fixtures/service-identities.json +11 -0
  14. package/fixtures/system-catalog.json +28 -0
  15. package/fixtures/translation-job-terminal-catalog.json +45 -0
  16. package/fixtures/translation-settings-fields.json +1 -0
  17. package/package.json +61 -0
  18. package/policy/log-redaction.json +113 -0
  19. package/src/actor.ts +81 -0
  20. package/src/audit/account.ts +206 -0
  21. package/src/audit/actor-policy.ts +63 -0
  22. package/src/audit/artist-label.ts +312 -0
  23. package/src/audit/attributes.ts +107 -0
  24. package/src/audit/builder.ts +25 -0
  25. package/src/audit/catalog.ts +116 -0
  26. package/src/audit/content.ts +723 -0
  27. package/src/audit/email-authoring.ts +220 -0
  28. package/src/audit/file.ts +168 -0
  29. package/src/audit/form.ts +229 -0
  30. package/src/audit/integrations.ts +207 -0
  31. package/src/audit/locale-code.ts +6 -0
  32. package/src/audit/locale-content.ts +358 -0
  33. package/src/audit/member-account.ts +313 -0
  34. package/src/audit/member.ts +171 -0
  35. package/src/audit/program.ts +339 -0
  36. package/src/audit/reference-data.ts +230 -0
  37. package/src/audit/reference-entities.ts +332 -0
  38. package/src/audit/relation-download-policy.ts +292 -0
  39. package/src/audit/release-campaign.ts +422 -0
  40. package/src/audit/series-type.ts +298 -0
  41. package/src/audit/settings.ts +306 -0
  42. package/src/audit/source-locale.ts +337 -0
  43. package/src/audit/types.ts +9 -0
  44. package/src/audit.ts +21 -0
  45. package/src/authentication.ts +183 -0
  46. package/src/error-metadata.ts +11 -0
  47. package/src/index.ts +11 -0
  48. package/src/job.ts +56 -0
  49. package/src/log-attribute-policy.ts +21 -0
  50. package/src/otel.ts +64 -0
  51. package/src/records.ts +1244 -0
  52. package/src/redaction.ts +35 -0
  53. package/src/request-context.ts +73 -0
  54. package/src/request-id.ts +13 -0
  55. package/src/request-result.ts +73 -0
  56. package/src/request.ts +60 -0
  57. package/src/system.ts +355 -0
  58. package/src/trace.ts +13 -0
@@ -0,0 +1,35 @@
1
+ import { stableErrorType } from "./error-metadata.ts";
2
+ import { isForbiddenKey, normalizeKey } from "./log-attribute-policy.ts";
3
+
4
+ // Stable public entrypoint retained for package consumers. Policy ownership and
5
+ // error classification live in focused modules; only attribute normalization
6
+ // is implemented here.
7
+ export { stableErrorType } from "./error-metadata.ts";
8
+ export { isForbiddenKey, normalizeKey } from "./log-attribute-policy.ts";
9
+
10
+ export type NormalizedLogAttributes = Record<string, string | number | boolean>;
11
+
12
+ // Conservative log boundary shared by TypeScript services. Sensitive keys,
13
+ // raw errors, and untyped objects never reach stdout or OTLP exporters.
14
+ export function normalizeLogAttributes(
15
+ attributes?: Readonly<Record<string, unknown>>,
16
+ ): NormalizedLogAttributes {
17
+ const normalized: NormalizedLogAttributes = {};
18
+ if (attributes === undefined) return normalized;
19
+ for (const [key, value] of Object.entries(attributes)) {
20
+ const normalizedKey = normalizeKey(key);
21
+ if (normalizedKey === "" || isForbiddenKey(normalizedKey)) continue;
22
+ if (normalizedKey === "error" || normalizedKey === "err") {
23
+ normalized.error_type = stableErrorType(value);
24
+ continue;
25
+ }
26
+ if (
27
+ typeof value === "string" ||
28
+ typeof value === "number" ||
29
+ typeof value === "boolean"
30
+ ) {
31
+ normalized[normalizedKey] = value;
32
+ }
33
+ }
34
+ return normalized;
35
+ }
@@ -0,0 +1,73 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { isIP } from "node:net";
3
+
4
+ import type { Actor } from "./actor.ts";
5
+ import {
6
+ createRequestId,
7
+ isRequestId,
8
+ MESSAGE_REQUEST_ID_HEADER,
9
+ REQUEST_ID_HEADER,
10
+ } from "./request-id.ts";
11
+
12
+ export { isRequestId, MESSAGE_REQUEST_ID_HEADER, REQUEST_ID_HEADER };
13
+
14
+ export interface RequestContext {
15
+ readonly requestId: string;
16
+ readonly actor: Actor;
17
+ readonly requestedAt: Date;
18
+ readonly sourceIp?: string;
19
+ }
20
+
21
+ const requestScope = new AsyncLocalStorage<RequestContext>();
22
+ export function isCanonicalSourceIp(value: string): boolean {
23
+ const version = isIP(value);
24
+ if (version === 4) {
25
+ return true;
26
+ }
27
+ if (version !== 6 || value !== value.toLowerCase()) {
28
+ return false;
29
+ }
30
+ const hostname = new URL(`http://[${value}]/`).hostname;
31
+ return hostname.slice(1, -1) === value;
32
+ }
33
+
34
+ export function createPublicRequestContext(sourceIp?: string): RequestContext {
35
+ if (sourceIp !== undefined && !isCanonicalSourceIp(sourceIp)) {
36
+ throw new TypeError("sourceIp must be a canonical IPv4 or IPv6 address");
37
+ }
38
+ const requestContext: RequestContext = {
39
+ requestId: createRequestId(),
40
+ actor: { kind: "anonymous" },
41
+ requestedAt: new Date(),
42
+ ...(sourceIp === undefined ? {} : { sourceIp }),
43
+ };
44
+ return Object.freeze(requestContext);
45
+ }
46
+
47
+ export function createPropagatedRequestContext(
48
+ requestId: string,
49
+ actor: Actor,
50
+ ): RequestContext {
51
+ if (!isRequestId(requestId)) {
52
+ throw new TypeError("requestId must be a canonical UUIDv4");
53
+ }
54
+ return Object.freeze({ requestId, actor, requestedAt: new Date() });
55
+ }
56
+
57
+ export function withActor(
58
+ requestContext: RequestContext,
59
+ actor: Actor,
60
+ ): RequestContext {
61
+ return Object.freeze({ ...requestContext, actor });
62
+ }
63
+
64
+ export function runWithRequestContext<T>(
65
+ requestContext: RequestContext,
66
+ callback: () => T,
67
+ ): T {
68
+ return requestScope.run(requestContext, callback);
69
+ }
70
+
71
+ export function activeRequestContext(): RequestContext | undefined {
72
+ return requestScope.getStore();
73
+ }
@@ -0,0 +1,13 @@
1
+ export const REQUEST_ID_HEADER = "X-Request-ID";
2
+ export const MESSAGE_REQUEST_ID_HEADER = "x-request-id";
3
+
4
+ const canonicalUUIDv4 =
5
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
6
+
7
+ export function isRequestId(value: string): boolean {
8
+ return canonicalUUIDv4.test(value);
9
+ }
10
+
11
+ export function createRequestId(): string {
12
+ return globalThis.crypto.randomUUID();
13
+ }
@@ -0,0 +1,73 @@
1
+ import type { RequestOutcome } from "./records.ts";
2
+
3
+ export const REQUEST_REASONS = [
4
+ "authentication_required",
5
+ "permission_denied",
6
+ "rate_limited",
7
+ "client_error",
8
+ "server_error",
9
+ ] as const;
10
+
11
+ export type RequestReason = (typeof REQUEST_REASONS)[number];
12
+
13
+ export interface RequestResult {
14
+ readonly status_code: number;
15
+ readonly duration_ms: number;
16
+ readonly outcome: RequestOutcome;
17
+ readonly error_code?: string;
18
+ readonly reason?: RequestReason;
19
+ }
20
+
21
+ export function classifyHTTPResult(
22
+ statusCode: number,
23
+ durationMs: number,
24
+ ): RequestResult {
25
+ if (
26
+ !Number.isInteger(statusCode) ||
27
+ statusCode < 100 ||
28
+ statusCode > 599 ||
29
+ !Number.isInteger(durationMs) ||
30
+ durationMs < 0
31
+ ) {
32
+ throw new TypeError(
33
+ "HTTP result requires a valid status_code and non-negative duration_ms",
34
+ );
35
+ }
36
+ if (statusCode < 400) {
37
+ return {
38
+ status_code: statusCode,
39
+ duration_ms: durationMs,
40
+ outcome: "succeeded",
41
+ };
42
+ }
43
+ if (statusCode === 401) {
44
+ return {
45
+ status_code: statusCode,
46
+ duration_ms: durationMs,
47
+ outcome: "blocked",
48
+ reason: "authentication_required",
49
+ };
50
+ }
51
+ if (statusCode === 403) {
52
+ return {
53
+ status_code: statusCode,
54
+ duration_ms: durationMs,
55
+ outcome: "blocked",
56
+ reason: "permission_denied",
57
+ };
58
+ }
59
+ if (statusCode === 429) {
60
+ return {
61
+ status_code: statusCode,
62
+ duration_ms: durationMs,
63
+ outcome: "blocked",
64
+ reason: "rate_limited",
65
+ };
66
+ }
67
+ return {
68
+ status_code: statusCode,
69
+ duration_ms: durationMs,
70
+ outcome: "failed",
71
+ reason: statusCode < 500 ? "client_error" : "server_error",
72
+ };
73
+ }
package/src/request.ts ADDED
@@ -0,0 +1,60 @@
1
+ import type { RecordActor } from "./actor.ts";
2
+ import {
3
+ validateRequestRecord,
4
+ type Correlation,
5
+ type RequestRecord,
6
+ } from "./records.ts";
7
+ import type { RequestResult } from "./request-result.ts";
8
+
9
+ export {
10
+ classifyHTTPResult,
11
+ REQUEST_REASONS,
12
+ type RequestReason,
13
+ type RequestResult,
14
+ } from "./request-result.ts";
15
+
16
+ export type RequestMetadata = Correlation &
17
+ RecordActor & {
18
+ readonly occurred_at: string;
19
+ };
20
+
21
+ export function buildHTTPRequestRecord(
22
+ metadata: RequestMetadata,
23
+ httpMethod: string,
24
+ httpRoute: string,
25
+ result: RequestResult,
26
+ ): RequestRecord {
27
+ return buildRequestRecord(metadata, result, {
28
+ http_method: httpMethod,
29
+ http_route: httpRoute,
30
+ });
31
+ }
32
+
33
+ export function buildRPCRequestRecord(
34
+ metadata: RequestMetadata,
35
+ httpMethod: string,
36
+ rpcService: string,
37
+ rpcMethod: string,
38
+ result: RequestResult,
39
+ ): RequestRecord {
40
+ return buildRequestRecord(metadata, result, {
41
+ http_method: httpMethod,
42
+ rpc_service: rpcService,
43
+ rpc_method: rpcMethod,
44
+ });
45
+ }
46
+
47
+ function buildRequestRecord(
48
+ metadata: RequestMetadata,
49
+ result: RequestResult,
50
+ boundary: Partial<RequestRecord>,
51
+ ): RequestRecord {
52
+ const record = {
53
+ ...metadata,
54
+ ...boundary,
55
+ ...result,
56
+ event: "request.completed",
57
+ } as RequestRecord;
58
+ validateRequestRecord(record);
59
+ return record;
60
+ }
package/src/system.ts ADDED
@@ -0,0 +1,355 @@
1
+ import {
2
+ SYSTEM_EVENT_OUTCOMES,
3
+ validateSystemRecord,
4
+ type AuditAction,
5
+ type AuditAppendFailureReason,
6
+ type Correlation,
7
+ type SecurityAction,
8
+ type SystemEvent,
9
+ type SystemRecord,
10
+ type TranslationEntityType,
11
+ type TranslationFailureReason,
12
+ type TranslationJobTerminalOutcome,
13
+ } from "./records.ts";
14
+ import type { JobFailureReason, JobKind } from "./job.ts";
15
+
16
+ export type SystemMetadata = Correlation & {
17
+ readonly occurred_at: string;
18
+ };
19
+
20
+ export interface SystemFailure {
21
+ readonly error_code?: string;
22
+ readonly reason?: string;
23
+ }
24
+
25
+ export type QueueFailureReason =
26
+ | "enqueue_failed"
27
+ | "handler_failed"
28
+ | "completion_failed"
29
+ | "shutdown"
30
+ | "visibility_update_failed"
31
+ | "archive_failed";
32
+
33
+ export interface QueuePublishContext {
34
+ readonly queue: string;
35
+ readonly message_id: string;
36
+ readonly command_id: string;
37
+ readonly duration_ms: number;
38
+ }
39
+
40
+ export interface QueueDeliveryContext {
41
+ readonly queue: string;
42
+ readonly message_id: string;
43
+ readonly command_id: string;
44
+ readonly retry_count: number;
45
+ readonly duration_ms: number;
46
+ }
47
+
48
+ export interface QueueHandoffContext {
49
+ readonly queue: string;
50
+ readonly message_id: string;
51
+ readonly command_id: string;
52
+ readonly retry_count: number;
53
+ }
54
+
55
+ export interface JobContext {
56
+ readonly job_kind: JobKind;
57
+ readonly job_id: string;
58
+ }
59
+
60
+ export interface JobFailure {
61
+ readonly reason: JobFailureReason;
62
+ }
63
+
64
+ /** Bounded public context for an authoritative Translation terminal transition. */
65
+ export interface TranslationJobTerminalContext {
66
+ readonly job_id: string;
67
+ readonly entity_type: TranslationEntityType;
68
+ readonly target_locale: string;
69
+ readonly duration_ms: number;
70
+ }
71
+
72
+ export type CollaborationCheckpointFailureReason =
73
+ | "shared_document_unavailable"
74
+ | "source_document_unavailable"
75
+ | "locale_ownership_changed"
76
+ | "shared_structure_changed"
77
+ | "shared_materialization_changed"
78
+ | "document_revision_changed"
79
+ | "target_revision_changed"
80
+ | "persist_failed";
81
+
82
+ export type CollaborationCheckpointEntityType =
83
+ | "post"
84
+ | "page"
85
+ | "work"
86
+ | "release"
87
+ | "label"
88
+ | "artist"
89
+ | "campaign"
90
+ | "program_event"
91
+ | "map_theme"
92
+ | "terms_history"
93
+ | "privacy_history"
94
+ | "form"
95
+ | "email_layout";
96
+
97
+ export interface CollaborationCheckpointContext {
98
+ readonly entity_type: CollaborationCheckpointEntityType;
99
+ readonly entity_id: string;
100
+ readonly retry_count: number;
101
+ }
102
+
103
+ export type ClientRenderComponent = "general" | "admin" | "global";
104
+
105
+ export type SystemLogLevel = "info" | "warn" | "error";
106
+
107
+ export function systemLogLevel(record: SystemRecord): SystemLogLevel {
108
+ validateSystemRecord(record);
109
+ if (record.outcome === "failed") return "error";
110
+ if (record.outcome === "degraded" || record.outcome === "requeued") {
111
+ return "warn";
112
+ }
113
+ return "info";
114
+ }
115
+
116
+ export const buildServiceReadyRecord = (
117
+ metadata: SystemMetadata,
118
+ component: string,
119
+ ) => buildSystemRecord(metadata, "service.ready", { component });
120
+ export const buildServiceDegradedRecord = (
121
+ metadata: SystemMetadata,
122
+ component: string,
123
+ failure: SystemFailure,
124
+ ) => buildSystemRecord(metadata, "service.degraded", { component, ...failure });
125
+ export const buildServiceStoppingRecord = (
126
+ metadata: SystemMetadata,
127
+ component: string,
128
+ ) => buildSystemRecord(metadata, "service.stopping", { component });
129
+ export const buildServiceFailedRecord = (
130
+ metadata: SystemMetadata,
131
+ component: string,
132
+ failure: SystemFailure,
133
+ ) => buildSystemRecord(metadata, "service.failed", { component, ...failure });
134
+ export const buildDependencyDegradedRecord = (
135
+ metadata: SystemMetadata,
136
+ dependency: string,
137
+ operation: string,
138
+ failure: SystemFailure,
139
+ ) =>
140
+ buildSystemRecord(metadata, "dependency.degraded", {
141
+ dependency,
142
+ operation,
143
+ ...failure,
144
+ });
145
+ export const buildDependencyRecoveredRecord = (
146
+ metadata: SystemMetadata,
147
+ dependency: string,
148
+ operation: string,
149
+ ) =>
150
+ buildSystemRecord(metadata, "dependency.recovered", {
151
+ dependency,
152
+ operation,
153
+ });
154
+ export const buildQueuePublishSucceededRecord = (
155
+ metadata: SystemMetadata,
156
+ queue: QueuePublishContext,
157
+ ) =>
158
+ buildSystemRecord(metadata, "queue.publish.succeeded", {
159
+ domain: "queue",
160
+ ...queue,
161
+ });
162
+ export const buildQueuePublishFailedRecord = (
163
+ metadata: SystemMetadata,
164
+ queue: QueuePublishContext,
165
+ reason: QueueFailureReason,
166
+ ) =>
167
+ buildSystemRecord(metadata, "queue.publish.failed", {
168
+ domain: "queue",
169
+ ...queue,
170
+ reason,
171
+ });
172
+ export const buildQueueDeliverySucceededRecord = (
173
+ metadata: SystemMetadata,
174
+ queue: QueueDeliveryContext,
175
+ ) =>
176
+ buildSystemRecord(metadata, "queue.delivery.succeeded", {
177
+ domain: "queue",
178
+ ...queue,
179
+ });
180
+ export const buildQueueDeliveryFailedRecord = (
181
+ metadata: SystemMetadata,
182
+ queue: QueueDeliveryContext,
183
+ reason: QueueFailureReason,
184
+ ) =>
185
+ buildSystemRecord(metadata, "queue.delivery.failed", {
186
+ domain: "queue",
187
+ ...queue,
188
+ reason,
189
+ });
190
+ export const buildQueueDeliveryRequeuedRecord = (
191
+ metadata: SystemMetadata,
192
+ queue: QueueDeliveryContext,
193
+ reason: QueueFailureReason,
194
+ ) =>
195
+ buildSystemRecord(metadata, "queue.delivery.requeued", {
196
+ domain: "queue",
197
+ ...queue,
198
+ reason,
199
+ });
200
+ export const buildQueueRetryAcceptedRecord = (
201
+ metadata: SystemMetadata,
202
+ queue: QueueHandoffContext,
203
+ ) =>
204
+ buildSystemRecord(metadata, "queue.retry.accepted", {
205
+ domain: "queue",
206
+ ...queue,
207
+ });
208
+ export const buildQueueRetryFailedRecord = (
209
+ metadata: SystemMetadata,
210
+ queue: QueueHandoffContext,
211
+ reason: QueueFailureReason,
212
+ ) =>
213
+ buildSystemRecord(metadata, "queue.retry.failed", {
214
+ domain: "queue",
215
+ ...queue,
216
+ reason,
217
+ });
218
+ export const buildQueueDLQAcceptedRecord = (
219
+ metadata: SystemMetadata,
220
+ queue: QueueHandoffContext,
221
+ ) =>
222
+ buildSystemRecord(metadata, "queue.dlq.accepted", {
223
+ domain: "queue",
224
+ ...queue,
225
+ });
226
+ export const buildQueueDLQFailedRecord = (
227
+ metadata: SystemMetadata,
228
+ queue: QueueHandoffContext,
229
+ reason: QueueFailureReason,
230
+ ) =>
231
+ buildSystemRecord(metadata, "queue.dlq.failed", {
232
+ domain: "queue",
233
+ ...queue,
234
+ reason,
235
+ });
236
+ export const buildJobStartedRecord = (
237
+ metadata: SystemMetadata,
238
+ job: JobContext,
239
+ ) => buildSystemRecord(metadata, "job.started", job);
240
+ export const buildJobSucceededRecord = (
241
+ metadata: SystemMetadata,
242
+ job: JobContext,
243
+ durationMs: number,
244
+ ) =>
245
+ buildSystemRecord(metadata, "job.succeeded", {
246
+ ...job,
247
+ duration_ms: durationMs,
248
+ });
249
+ export const buildJobFailedRecord = (
250
+ metadata: SystemMetadata,
251
+ job: JobContext,
252
+ durationMs: number,
253
+ failure: JobFailure,
254
+ ) =>
255
+ buildSystemRecord(metadata, "job.failed", {
256
+ ...job,
257
+ duration_ms: durationMs,
258
+ ...failure,
259
+ });
260
+ export const buildTranslationJobTerminalRecord = (
261
+ metadata: SystemMetadata,
262
+ job: TranslationJobTerminalContext,
263
+ outcome: TranslationJobTerminalOutcome,
264
+ errorClassification?: TranslationFailureReason,
265
+ ) => {
266
+ if (outcome === "failed") {
267
+ if (errorClassification === undefined) {
268
+ throw new TypeError(
269
+ "failed translation.job.terminal requires an error classification",
270
+ );
271
+ }
272
+ } else if (errorClassification !== undefined) {
273
+ throw new TypeError(
274
+ `${outcome} translation terminal events cannot contain an error classification`,
275
+ );
276
+ }
277
+ return buildSystemRecord(metadata, "translation.job.terminal", {
278
+ domain: "translation",
279
+ ...job,
280
+ outcome,
281
+ ...(errorClassification === undefined
282
+ ? {}
283
+ : { error_classification: errorClassification }),
284
+ });
285
+ };
286
+ export const buildCollaborationCheckpointFailedRecord = (
287
+ metadata: SystemMetadata,
288
+ checkpoint: CollaborationCheckpointContext,
289
+ reason: CollaborationCheckpointFailureReason,
290
+ ) =>
291
+ buildSystemRecord(metadata, "collaboration.checkpoint.failed", {
292
+ domain: "collaboration",
293
+ ...checkpoint,
294
+ reason,
295
+ });
296
+ export const buildClientRenderFailedRecord = (
297
+ metadata: SystemMetadata,
298
+ component: ClientRenderComponent,
299
+ ) =>
300
+ buildSystemRecord(metadata, "client.render.failed", {
301
+ domain: "client",
302
+ component,
303
+ reason: "react_error_boundary",
304
+ });
305
+ export const buildDomainAuditAppendFailedRecord = (
306
+ metadata: SystemMetadata,
307
+ action: AuditAction,
308
+ reason: AuditAppendFailureReason,
309
+ ) =>
310
+ buildSystemRecord(metadata, "audit.append.failed", {
311
+ domain: "audit",
312
+ record_class: "domain_audit",
313
+ action,
314
+ reason,
315
+ });
316
+ export const buildSecurityAccessAppendFailedRecord = (
317
+ metadata: SystemMetadata,
318
+ action: SecurityAction,
319
+ reason: AuditAppendFailureReason,
320
+ ) =>
321
+ buildSystemRecord(metadata, "audit.append.failed", {
322
+ domain: "audit",
323
+ record_class: "security_access",
324
+ action,
325
+ reason,
326
+ });
327
+ export const buildTelemetryPipelineDegradedRecord = (
328
+ metadata: SystemMetadata,
329
+ component: string,
330
+ failure: SystemFailure,
331
+ ) =>
332
+ buildSystemRecord(metadata, "telemetry.pipeline.degraded", {
333
+ component,
334
+ ...failure,
335
+ });
336
+ export const buildTelemetryPipelineRecoveredRecord = (
337
+ metadata: SystemMetadata,
338
+ component: string,
339
+ ) => buildSystemRecord(metadata, "telemetry.pipeline.recovered", { component });
340
+
341
+ function buildSystemRecord(
342
+ metadata: SystemMetadata,
343
+ event: SystemEvent,
344
+ attributes: Partial<SystemRecord>,
345
+ ): SystemRecord {
346
+ const outcome = attributes.outcome ?? SYSTEM_EVENT_OUTCOMES[event]!;
347
+ const record = {
348
+ ...metadata,
349
+ ...attributes,
350
+ event,
351
+ outcome,
352
+ } as SystemRecord;
353
+ validateSystemRecord(record);
354
+ return record;
355
+ }
package/src/trace.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { context, isSpanContextValid, trace } from "@opentelemetry/api";
2
+
3
+ import type { Correlation } from "./records.ts";
4
+
5
+ export type TraceCorrelation = Pick<Correlation, "trace_id" | "span_id">;
6
+
7
+ // Browser-safe correlation primitive. Request IDs remain server RequestContext
8
+ // state; browser-imported code can read only the active OpenTelemetry span.
9
+ export function traceCorrelationFromActiveContext(): TraceCorrelation {
10
+ const spanContext = trace.getSpanContext(context.active());
11
+ if (spanContext === undefined || !isSpanContextValid(spanContext)) return {};
12
+ return { trace_id: spanContext.traceId, span_id: spanContext.spanId };
13
+ }