@open-mercato/shared 0.6.8-develop.6998.1.d2bc46c56c → 0.6.8-develop.7000.1.0ae682bad1

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.
@@ -26,10 +26,23 @@ function getTelemetryRuntime() {
26
26
  function resetTelemetryRuntime() {
27
27
  store().active = void 0;
28
28
  }
29
+ const NOOP_SPAN = { setAttributes() {
30
+ } };
31
+ function withTelemetrySpan(name, fn, options) {
32
+ const runtime = getTelemetryRuntime();
33
+ if (!runtime?.withSpan) return fn(NOOP_SPAN);
34
+ return runtime.withSpan(name, fn, options);
35
+ }
36
+ function captureTelemetryTrace() {
37
+ const carrier = getTelemetryRuntime()?.captureTraceContext();
38
+ return carrier && Object.keys(carrier).length > 0 ? carrier : void 0;
39
+ }
29
40
  export {
41
+ captureTelemetryTrace,
30
42
  getTelemetryRuntime,
31
43
  isTelemetryBackendEnabled,
32
44
  registerTelemetryRuntime,
33
- resetTelemetryRuntime
45
+ resetTelemetryRuntime,
46
+ withTelemetrySpan
34
47
  };
35
48
  //# sourceMappingURL=runtime.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/lib/telemetry/runtime.ts"],
4
- "sourcesContent": ["export type TelemetryTraceCarrier = Record<string, string>\n\nexport type TelemetryRuntime = {\n /**\n * True only when the active SDK may safely use the process-global W3C\n * propagator for cross-boundary extraction.\n */\n canUseGlobalTracePropagation(): boolean\n captureTraceContext(): TelemetryTraceCarrier\n continueTrace<T>(\n carrier: TelemetryTraceCarrier | undefined,\n name: string,\n fn: () => T,\n options?: { kind?: 'internal' | 'server' | 'client' | 'producer' | 'consumer' },\n ): T\n recordHttpDuration(method: string, route: string, status: number, startedAt: number): void\n reportError(\n error: unknown,\n context?: {\n module?: string\n attributes?: Record<string, string | number | boolean | undefined>\n },\n ): void\n shutdown(): Promise<void>\n}\n\nconst GLOBAL_KEY = Symbol.for('@open-mercato/shared.telemetryRuntime')\nconst ENABLED_BACKENDS = new Set(['console', 'signoz', 'newrelic', 'otlp'])\n\ntype TelemetryRuntimeStore = {\n active?: TelemetryRuntime\n}\n\nfunction store(): TelemetryRuntimeStore {\n const globalStore = globalThis as unknown as Record<symbol, TelemetryRuntimeStore | undefined>\n let current = globalStore[GLOBAL_KEY]\n if (!current) {\n current = {}\n globalStore[GLOBAL_KEY] = current\n }\n return current\n}\n\n/**\n * This check is intentionally owned by shared code so hosts can decide whether\n * to dynamically import the telemetry package without evaluating that package.\n */\nexport function isTelemetryBackendEnabled(raw?: string): boolean {\n const value = raw ?? (\n typeof process === 'undefined'\n ? undefined\n : process.env.TELEMETRY_BACKEND\n )\n return ENABLED_BACKENDS.has((value ?? '').trim().toLowerCase())\n}\n\nexport function registerTelemetryRuntime(runtime: TelemetryRuntime): () => void {\n store().active = runtime\n return () => {\n const current = store()\n if (current.active === runtime) current.active = undefined\n }\n}\n\nexport function getTelemetryRuntime(): TelemetryRuntime | undefined {\n return store().active\n}\n\n/** Test-only: clear the process-wide telemetry bridge. */\nexport function resetTelemetryRuntime(): void {\n store().active = undefined\n}\n"],
5
- "mappings": "AA0BA,MAAM,aAAa,uBAAO,IAAI,uCAAuC;AACrE,MAAM,mBAAmB,oBAAI,IAAI,CAAC,WAAW,UAAU,YAAY,MAAM,CAAC;AAM1E,SAAS,QAA+B;AACtC,QAAM,cAAc;AACpB,MAAI,UAAU,YAAY,UAAU;AACpC,MAAI,CAAC,SAAS;AACZ,cAAU,CAAC;AACX,gBAAY,UAAU,IAAI;AAAA,EAC5B;AACA,SAAO;AACT;AAMO,SAAS,0BAA0B,KAAuB;AAC/D,QAAM,QAAQ,QACZ,OAAO,YAAY,cACf,SACA,QAAQ,IAAI;AAElB,SAAO,iBAAiB,KAAK,SAAS,IAAI,KAAK,EAAE,YAAY,CAAC;AAChE;AAEO,SAAS,yBAAyB,SAAuC;AAC9E,QAAM,EAAE,SAAS;AACjB,SAAO,MAAM;AACX,UAAM,UAAU,MAAM;AACtB,QAAI,QAAQ,WAAW,QAAS,SAAQ,SAAS;AAAA,EACnD;AACF;AAEO,SAAS,sBAAoD;AAClE,SAAO,MAAM,EAAE;AACjB;AAGO,SAAS,wBAA8B;AAC5C,QAAM,EAAE,SAAS;AACnB;",
4
+ "sourcesContent": ["export type TelemetryTraceCarrier = Record<string, string>\n\nexport type TelemetrySpanAttributes = Record<string, string | number | boolean | undefined>\n\nexport type TelemetrySpanKind = 'internal' | 'server' | 'client' | 'producer' | 'consumer'\n\n/** The subset of the telemetry package's `Span` that bridge consumers need. */\nexport type TelemetrySpan = {\n setAttributes(attributes: TelemetrySpanAttributes): void\n /**\n * Rename an in-flight span whose identity is only known once it has run.\n * Optional so a bootstrap predating it still satisfies the contract \u2014 call it\n * as `span.updateName?.(\u2026)`.\n */\n updateName?(name: string): void\n}\n\nexport type TelemetrySpanOptions = {\n kind?: TelemetrySpanKind\n attributes?: TelemetrySpanAttributes\n /** Start a new trace so the sampler decides for this span alone. */\n root?: boolean\n /** Causal links to other traces, as W3C carriers. */\n links?: TelemetryTraceCarrier[]\n}\n\nexport type TelemetryRuntime = {\n /**\n * True only when the active SDK may safely use the process-global W3C\n * propagator for cross-boundary extraction.\n */\n canUseGlobalTracePropagation(): boolean\n captureTraceContext(): TelemetryTraceCarrier\n continueTrace<T>(\n carrier: TelemetryTraceCarrier | undefined,\n name: string,\n fn: () => T,\n options?: { kind?: 'internal' | 'server' | 'client' | 'producer' | 'consumer' },\n ): T\n /**\n * Optional so an older bootstrap that predates span support still satisfies\n * the contract; consumers go through `withTelemetrySpan` and degrade to\n * running `fn` untraced.\n */\n withSpan?<T>(name: string, fn: (span: TelemetrySpan) => T, options?: TelemetrySpanOptions): T\n recordHttpDuration(method: string, route: string, status: number, startedAt: number): void\n reportError(\n error: unknown,\n context?: {\n module?: string\n attributes?: Record<string, string | number | boolean | undefined>\n },\n ): void\n shutdown(): Promise<void>\n}\n\nconst GLOBAL_KEY = Symbol.for('@open-mercato/shared.telemetryRuntime')\nconst ENABLED_BACKENDS = new Set(['console', 'signoz', 'newrelic', 'otlp'])\n\ntype TelemetryRuntimeStore = {\n active?: TelemetryRuntime\n}\n\nfunction store(): TelemetryRuntimeStore {\n const globalStore = globalThis as unknown as Record<symbol, TelemetryRuntimeStore | undefined>\n let current = globalStore[GLOBAL_KEY]\n if (!current) {\n current = {}\n globalStore[GLOBAL_KEY] = current\n }\n return current\n}\n\n/**\n * This check is intentionally owned by shared code so hosts can decide whether\n * to dynamically import the telemetry package without evaluating that package.\n */\nexport function isTelemetryBackendEnabled(raw?: string): boolean {\n const value = raw ?? (\n typeof process === 'undefined'\n ? undefined\n : process.env.TELEMETRY_BACKEND\n )\n return ENABLED_BACKENDS.has((value ?? '').trim().toLowerCase())\n}\n\nexport function registerTelemetryRuntime(runtime: TelemetryRuntime): () => void {\n store().active = runtime\n return () => {\n const current = store()\n if (current.active === runtime) current.active = undefined\n }\n}\n\nexport function getTelemetryRuntime(): TelemetryRuntime | undefined {\n return store().active\n}\n\n/** Test-only: clear the process-wide telemetry bridge. */\nexport function resetTelemetryRuntime(): void {\n store().active = undefined\n}\n\nconst NOOP_SPAN: TelemetrySpan = { setAttributes() {} }\n\n/**\n * Run `fn` inside a span, from a package that must not depend on\n * `@open-mercato/telemetry`. With telemetry off this is `fn` plus one global\n * lookup \u2014 no span object is allocated and the OTEL SDK is never reached.\n *\n * Pass `root: true` for the unit of work a long-lived job should be sampled and\n * rendered by (a batch, a page) so the job is not one trace under one sampling\n * decision, and `links` to keep the causal chain back to what triggered it.\n */\nexport function withTelemetrySpan<T>(\n name: string,\n fn: (span: TelemetrySpan) => T,\n options?: TelemetrySpanOptions,\n): T {\n const runtime = getTelemetryRuntime()\n if (!runtime?.withSpan) return fn(NOOP_SPAN)\n return runtime.withSpan(name, fn, options)\n}\n\n/**\n * The active trace as a carrier, for use as a `links` entry. `undefined` when\n * telemetry is off or nothing is active, which `withTelemetrySpan` treats as\n * \"no link\" rather than an invalid one.\n */\nexport function captureTelemetryTrace(): TelemetryTraceCarrier | undefined {\n const carrier = getTelemetryRuntime()?.captureTraceContext()\n return carrier && Object.keys(carrier).length > 0 ? carrier : undefined\n}\n"],
5
+ "mappings": "AAwDA,MAAM,aAAa,uBAAO,IAAI,uCAAuC;AACrE,MAAM,mBAAmB,oBAAI,IAAI,CAAC,WAAW,UAAU,YAAY,MAAM,CAAC;AAM1E,SAAS,QAA+B;AACtC,QAAM,cAAc;AACpB,MAAI,UAAU,YAAY,UAAU;AACpC,MAAI,CAAC,SAAS;AACZ,cAAU,CAAC;AACX,gBAAY,UAAU,IAAI;AAAA,EAC5B;AACA,SAAO;AACT;AAMO,SAAS,0BAA0B,KAAuB;AAC/D,QAAM,QAAQ,QACZ,OAAO,YAAY,cACf,SACA,QAAQ,IAAI;AAElB,SAAO,iBAAiB,KAAK,SAAS,IAAI,KAAK,EAAE,YAAY,CAAC;AAChE;AAEO,SAAS,yBAAyB,SAAuC;AAC9E,QAAM,EAAE,SAAS;AACjB,SAAO,MAAM;AACX,UAAM,UAAU,MAAM;AACtB,QAAI,QAAQ,WAAW,QAAS,SAAQ,SAAS;AAAA,EACnD;AACF;AAEO,SAAS,sBAAoD;AAClE,SAAO,MAAM,EAAE;AACjB;AAGO,SAAS,wBAA8B;AAC5C,QAAM,EAAE,SAAS;AACnB;AAEA,MAAM,YAA2B,EAAE,gBAAgB;AAAC,EAAE;AAW/C,SAAS,kBACd,MACA,IACA,SACG;AACH,QAAM,UAAU,oBAAoB;AACpC,MAAI,CAAC,SAAS,SAAU,QAAO,GAAG,SAAS;AAC3C,SAAO,QAAQ,SAAS,MAAM,IAAI,OAAO;AAC3C;AAOO,SAAS,wBAA2D;AACzE,QAAM,UAAU,oBAAoB,GAAG,oBAAoB;AAC3D,SAAO,WAAW,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,UAAU;AAChE;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,4 @@
1
- const APP_VERSION = "0.6.8-develop.6998.1.d2bc46c56c";
1
+ const APP_VERSION = "0.6.8-develop.7000.1.0ae682bad1";
2
2
  const appVersion = APP_VERSION;
3
3
  export {
4
4
  APP_VERSION,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/version.ts"],
4
- "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.6998.1.d2bc46c56c';\nexport const appVersion = APP_VERSION;\n"],
4
+ "sourcesContent": ["// Build-time generated version\nexport const APP_VERSION = '0.6.8-develop.7000.1.0ae682bad1';\nexport const appVersion = APP_VERSION;\n"],
5
5
  "mappings": "AACO,MAAM,cAAc;AACpB,MAAM,aAAa;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/shared",
3
- "version": "0.6.8-develop.6998.1.d2bc46c56c",
3
+ "version": "0.6.8-develop.7000.1.0ae682bad1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -105,7 +105,7 @@
105
105
  "@mikro-orm/core": "^7.1.8",
106
106
  "@mikro-orm/decorators": "^7.1.8",
107
107
  "@mikro-orm/postgresql": "^7.1.8",
108
- "@open-mercato/cache": "0.6.8-develop.6998.1.d2bc46c56c",
108
+ "@open-mercato/cache": "0.6.8-develop.7000.1.0ae682bad1",
109
109
  "@types/sanitize-html": "^2.16.1",
110
110
  "dotenv": "^17.4.2",
111
111
  "pino": "^10.3.1",
@@ -0,0 +1,211 @@
1
+ import {
2
+ captureTelemetryTrace,
3
+ getTelemetryRuntime,
4
+ isTelemetryBackendEnabled,
5
+ registerTelemetryRuntime,
6
+ resetTelemetryRuntime,
7
+ withTelemetrySpan,
8
+ type TelemetryRuntime,
9
+ type TelemetrySpan,
10
+ type TelemetrySpanOptions,
11
+ type TelemetryTraceCarrier,
12
+ } from '../runtime'
13
+
14
+ type SpanCall = { name: string; options?: TelemetrySpanOptions }
15
+
16
+ function createRuntime(overrides: Partial<TelemetryRuntime> = {}): TelemetryRuntime {
17
+ return {
18
+ canUseGlobalTracePropagation: () => true,
19
+ captureTraceContext: () => ({}),
20
+ continueTrace: (_carrier, _name, fn) => fn(),
21
+ recordHttpDuration: () => {},
22
+ reportError: () => {},
23
+ shutdown: async () => {},
24
+ ...overrides,
25
+ }
26
+ }
27
+
28
+ function createTracingRuntime(): { runtime: TelemetryRuntime; calls: SpanCall[]; span: TelemetrySpan } {
29
+ const calls: SpanCall[] = []
30
+ const span: TelemetrySpan = { setAttributes: jest.fn(), updateName: jest.fn() }
31
+ const runtime = createRuntime({
32
+ withSpan: (name, fn, options) => {
33
+ calls.push({ name, options })
34
+ return fn(span)
35
+ },
36
+ })
37
+ return { runtime, calls, span }
38
+ }
39
+
40
+ describe('isTelemetryBackendEnabled', () => {
41
+ const originalBackend = process.env.TELEMETRY_BACKEND
42
+
43
+ afterEach(() => {
44
+ if (originalBackend === undefined) delete process.env.TELEMETRY_BACKEND
45
+ else process.env.TELEMETRY_BACKEND = originalBackend
46
+ })
47
+
48
+ it('recognizes every supported backend', () => {
49
+ for (const backend of ['console', 'signoz', 'newrelic', 'otlp']) {
50
+ expect(isTelemetryBackendEnabled(backend)).toBe(true)
51
+ }
52
+ })
53
+
54
+ it('normalizes surrounding whitespace and casing', () => {
55
+ expect(isTelemetryBackendEnabled(' CoNsOle ')).toBe(true)
56
+ expect(isTelemetryBackendEnabled('\tOTLP\n')).toBe(true)
57
+ })
58
+
59
+ it('rejects unknown, blank, and missing values', () => {
60
+ expect(isTelemetryBackendEnabled('off')).toBe(false)
61
+ expect(isTelemetryBackendEnabled('consoles')).toBe(false)
62
+ expect(isTelemetryBackendEnabled('')).toBe(false)
63
+ expect(isTelemetryBackendEnabled(' ')).toBe(false)
64
+
65
+ delete process.env.TELEMETRY_BACKEND
66
+ expect(isTelemetryBackendEnabled()).toBe(false)
67
+ })
68
+
69
+ it('falls back to TELEMETRY_BACKEND only when no value is passed', () => {
70
+ process.env.TELEMETRY_BACKEND = 'signoz'
71
+ expect(isTelemetryBackendEnabled()).toBe(true)
72
+ expect(isTelemetryBackendEnabled('off')).toBe(false)
73
+
74
+ process.env.TELEMETRY_BACKEND = 'off'
75
+ expect(isTelemetryBackendEnabled()).toBe(false)
76
+ expect(isTelemetryBackendEnabled('console')).toBe(true)
77
+ })
78
+ })
79
+
80
+ describe('telemetry runtime registry', () => {
81
+ afterEach(() => {
82
+ resetTelemetryRuntime()
83
+ })
84
+
85
+ it('exposes no runtime until one is registered', () => {
86
+ expect(getTelemetryRuntime()).toBeUndefined()
87
+ })
88
+
89
+ it('exposes the registered runtime across separate lookups', () => {
90
+ const runtime = createRuntime()
91
+ registerTelemetryRuntime(runtime)
92
+
93
+ expect(getTelemetryRuntime()).toBe(runtime)
94
+ expect(getTelemetryRuntime()).toBe(runtime)
95
+ })
96
+
97
+ it('clears the runtime when its own disposer runs', () => {
98
+ const dispose = registerTelemetryRuntime(createRuntime())
99
+ dispose()
100
+
101
+ expect(getTelemetryRuntime()).toBeUndefined()
102
+ })
103
+
104
+ it('leaves a newer runtime in place when a superseded disposer runs', () => {
105
+ const first = createRuntime()
106
+ const second = createRuntime()
107
+ const disposeFirst = registerTelemetryRuntime(first)
108
+ registerTelemetryRuntime(second)
109
+
110
+ disposeFirst()
111
+
112
+ expect(getTelemetryRuntime()).toBe(second)
113
+ })
114
+
115
+ it('clears the runtime on reset', () => {
116
+ registerTelemetryRuntime(createRuntime())
117
+ resetTelemetryRuntime()
118
+
119
+ expect(getTelemetryRuntime()).toBeUndefined()
120
+ })
121
+ })
122
+
123
+ describe('withTelemetrySpan', () => {
124
+ afterEach(() => {
125
+ resetTelemetryRuntime()
126
+ })
127
+
128
+ it('runs the callback untraced with a usable noop span when telemetry is off', () => {
129
+ const seen: TelemetrySpan[] = []
130
+ const result = withTelemetrySpan('job.batch', (span) => {
131
+ seen.push(span)
132
+ span.setAttributes({ 'om.tenant_id': 'tenant-1', count: 2, ok: true })
133
+ span.updateName?.('job.batch.renamed')
134
+ return 'done'
135
+ })
136
+
137
+ expect(result).toBe('done')
138
+ expect(seen).toHaveLength(1)
139
+ })
140
+
141
+ it('runs the callback untraced when the registered runtime predates span support', () => {
142
+ registerTelemetryRuntime(createRuntime())
143
+
144
+ const result = withTelemetrySpan('job.batch', (span) => {
145
+ span.setAttributes({ ok: true })
146
+ return 41 + 1
147
+ })
148
+
149
+ expect(result).toBe(42)
150
+ })
151
+
152
+ it('delegates to the runtime and forwards the name, options, and span', () => {
153
+ const { runtime, calls, span } = createTracingRuntime()
154
+ registerTelemetryRuntime(runtime)
155
+
156
+ const options: TelemetrySpanOptions = {
157
+ kind: 'consumer',
158
+ root: true,
159
+ links: [{ traceparent: '00-aaaa-bbbb-01' }],
160
+ attributes: { 'data_sync.batch_index': 3 },
161
+ }
162
+
163
+ const result = withTelemetrySpan(
164
+ 'data_sync.import.batch',
165
+ (received) => {
166
+ expect(received).toBe(span)
167
+ received.setAttributes({ 'data_sync.records': 10 })
168
+ return 'traced'
169
+ },
170
+ options,
171
+ )
172
+
173
+ expect(result).toBe('traced')
174
+ expect(calls).toEqual([{ name: 'data_sync.import.batch', options }])
175
+ expect(span.setAttributes).toHaveBeenCalledWith({ 'data_sync.records': 10 })
176
+ })
177
+
178
+ it('propagates a callback failure from the traced path', () => {
179
+ const { runtime } = createTracingRuntime()
180
+ registerTelemetryRuntime(runtime)
181
+
182
+ expect(() =>
183
+ withTelemetrySpan('data_sync.import.batch', () => {
184
+ throw new Error('[internal] batch failed')
185
+ }),
186
+ ).toThrow('[internal] batch failed')
187
+ })
188
+ })
189
+
190
+ describe('captureTelemetryTrace', () => {
191
+ afterEach(() => {
192
+ resetTelemetryRuntime()
193
+ })
194
+
195
+ it('returns undefined when telemetry is off', () => {
196
+ expect(captureTelemetryTrace()).toBeUndefined()
197
+ })
198
+
199
+ it('returns undefined when the active runtime captures an empty carrier', () => {
200
+ registerTelemetryRuntime(createRuntime({ captureTraceContext: () => ({}) }))
201
+
202
+ expect(captureTelemetryTrace()).toBeUndefined()
203
+ })
204
+
205
+ it('returns the captured carrier when a trace is active', () => {
206
+ const carrier: TelemetryTraceCarrier = { traceparent: '00-aaaa-bbbb-01' }
207
+ registerTelemetryRuntime(createRuntime({ captureTraceContext: () => carrier }))
208
+
209
+ expect(captureTelemetryTrace()).toEqual(carrier)
210
+ })
211
+ })
@@ -1,5 +1,29 @@
1
1
  export type TelemetryTraceCarrier = Record<string, string>
2
2
 
3
+ export type TelemetrySpanAttributes = Record<string, string | number | boolean | undefined>
4
+
5
+ export type TelemetrySpanKind = 'internal' | 'server' | 'client' | 'producer' | 'consumer'
6
+
7
+ /** The subset of the telemetry package's `Span` that bridge consumers need. */
8
+ export type TelemetrySpan = {
9
+ setAttributes(attributes: TelemetrySpanAttributes): void
10
+ /**
11
+ * Rename an in-flight span whose identity is only known once it has run.
12
+ * Optional so a bootstrap predating it still satisfies the contract — call it
13
+ * as `span.updateName?.(…)`.
14
+ */
15
+ updateName?(name: string): void
16
+ }
17
+
18
+ export type TelemetrySpanOptions = {
19
+ kind?: TelemetrySpanKind
20
+ attributes?: TelemetrySpanAttributes
21
+ /** Start a new trace so the sampler decides for this span alone. */
22
+ root?: boolean
23
+ /** Causal links to other traces, as W3C carriers. */
24
+ links?: TelemetryTraceCarrier[]
25
+ }
26
+
3
27
  export type TelemetryRuntime = {
4
28
  /**
5
29
  * True only when the active SDK may safely use the process-global W3C
@@ -13,6 +37,12 @@ export type TelemetryRuntime = {
13
37
  fn: () => T,
14
38
  options?: { kind?: 'internal' | 'server' | 'client' | 'producer' | 'consumer' },
15
39
  ): T
40
+ /**
41
+ * Optional so an older bootstrap that predates span support still satisfies
42
+ * the contract; consumers go through `withTelemetrySpan` and degrade to
43
+ * running `fn` untraced.
44
+ */
45
+ withSpan?<T>(name: string, fn: (span: TelemetrySpan) => T, options?: TelemetrySpanOptions): T
16
46
  recordHttpDuration(method: string, route: string, status: number, startedAt: number): void
17
47
  reportError(
18
48
  error: unknown,
@@ -70,3 +100,34 @@ export function getTelemetryRuntime(): TelemetryRuntime | undefined {
70
100
  export function resetTelemetryRuntime(): void {
71
101
  store().active = undefined
72
102
  }
103
+
104
+ const NOOP_SPAN: TelemetrySpan = { setAttributes() {} }
105
+
106
+ /**
107
+ * Run `fn` inside a span, from a package that must not depend on
108
+ * `@open-mercato/telemetry`. With telemetry off this is `fn` plus one global
109
+ * lookup — no span object is allocated and the OTEL SDK is never reached.
110
+ *
111
+ * Pass `root: true` for the unit of work a long-lived job should be sampled and
112
+ * rendered by (a batch, a page) so the job is not one trace under one sampling
113
+ * decision, and `links` to keep the causal chain back to what triggered it.
114
+ */
115
+ export function withTelemetrySpan<T>(
116
+ name: string,
117
+ fn: (span: TelemetrySpan) => T,
118
+ options?: TelemetrySpanOptions,
119
+ ): T {
120
+ const runtime = getTelemetryRuntime()
121
+ if (!runtime?.withSpan) return fn(NOOP_SPAN)
122
+ return runtime.withSpan(name, fn, options)
123
+ }
124
+
125
+ /**
126
+ * The active trace as a carrier, for use as a `links` entry. `undefined` when
127
+ * telemetry is off or nothing is active, which `withTelemetrySpan` treats as
128
+ * "no link" rather than an invalid one.
129
+ */
130
+ export function captureTelemetryTrace(): TelemetryTraceCarrier | undefined {
131
+ const carrier = getTelemetryRuntime()?.captureTraceContext()
132
+ return carrier && Object.keys(carrier).length > 0 ? carrier : undefined
133
+ }
@@ -62,6 +62,10 @@ export interface EventPayload {
62
62
  export interface EmitOptions {
63
63
  /** If true, the event will be persisted to a queue for async processing */
64
64
  persistent?: boolean
65
+ /** Trusted tenant scope forwarded to subscribers separately from the payload */
66
+ tenantId?: string | null
67
+ /** Trusted organization scope forwarded to subscribers separately from the payload */
68
+ organizationId?: string | null
65
69
  }
66
70
 
67
71
  // =============================================================================