@dxos/observability 0.8.4-main.ef1bc66f44 → 0.8.4-main.f466a3d56e

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 (133) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +14 -11
  3. package/dist/lib/browser/{chunk-K4VFBKST.mjs → chunk-PSFTIG54.mjs} +9 -2
  4. package/dist/lib/browser/chunk-PSFTIG54.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +181 -283
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/{log-processor-FDLTDQEM.mjs → log-processor-HPHWNBOK.mjs} +11 -18
  8. package/dist/lib/browser/log-processor-HPHWNBOK.mjs.map +7 -0
  9. package/dist/lib/browser/{logs-ATTRIUTL.mjs → logs-UTNIFYHF.mjs} +7 -4
  10. package/dist/lib/browser/logs-UTNIFYHF.mjs.map +7 -0
  11. package/dist/lib/browser/meta.json +1 -1
  12. package/dist/lib/browser/{metrics-PKTV6IGF.mjs → metrics-PRGSYAZJ.mjs} +7 -21
  13. package/dist/lib/browser/metrics-PRGSYAZJ.mjs.map +7 -0
  14. package/dist/lib/browser/traces-browser-XRINKQUA.mjs +154 -0
  15. package/dist/lib/browser/traces-browser-XRINKQUA.mjs.map +7 -0
  16. package/dist/lib/node-esm/{chunk-FEVP3MK4.mjs → chunk-EDDZWPYV.mjs} +9 -2
  17. package/dist/lib/node-esm/chunk-EDDZWPYV.mjs.map +7 -0
  18. package/dist/lib/node-esm/index.mjs +166 -250
  19. package/dist/lib/node-esm/index.mjs.map +4 -4
  20. package/dist/lib/node-esm/{log-processor-TKJVJJSJ.mjs → log-processor-ZODHERST.mjs} +11 -18
  21. package/dist/lib/node-esm/log-processor-ZODHERST.mjs.map +7 -0
  22. package/dist/lib/node-esm/{logs-7J45KLM7.mjs → logs-ARJUVN3T.mjs} +7 -4
  23. package/dist/lib/node-esm/logs-ARJUVN3T.mjs.map +7 -0
  24. package/dist/lib/node-esm/meta.json +1 -1
  25. package/dist/lib/node-esm/{metrics-H7DDLYSG.mjs → metrics-DREJOOAC.mjs} +7 -21
  26. package/dist/lib/node-esm/metrics-DREJOOAC.mjs.map +7 -0
  27. package/dist/lib/node-esm/traces-74F7JUKF.mjs +125 -0
  28. package/dist/lib/node-esm/traces-74F7JUKF.mjs.map +7 -0
  29. package/dist/types/src/cli-observability-secrets.json +2 -2
  30. package/dist/types/src/extensions/otel/extension.d.ts.map +1 -1
  31. package/dist/types/src/extensions/otel/extension.js +112 -16
  32. package/dist/types/src/extensions/otel/extension.js.map +1 -1
  33. package/dist/types/src/extensions/otel/logs.d.ts.map +1 -1
  34. package/dist/types/src/extensions/otel/logs.js +6 -3
  35. package/dist/types/src/extensions/otel/logs.js.map +1 -1
  36. package/dist/types/src/extensions/otel/metrics.d.ts.map +1 -1
  37. package/dist/types/src/extensions/otel/metrics.js +2 -2
  38. package/dist/types/src/extensions/otel/metrics.js.map +1 -1
  39. package/dist/types/src/extensions/otel/otel.d.ts +7 -0
  40. package/dist/types/src/extensions/otel/otel.d.ts.map +1 -1
  41. package/dist/types/src/extensions/otel/otel.js +12 -0
  42. package/dist/types/src/extensions/otel/otel.js.map +1 -1
  43. package/dist/types/src/extensions/otel/span-processors.d.ts +25 -0
  44. package/dist/types/src/extensions/otel/span-processors.d.ts.map +1 -0
  45. package/dist/types/src/extensions/otel/span-processors.js +41 -0
  46. package/dist/types/src/extensions/otel/span-processors.js.map +1 -0
  47. package/dist/types/src/extensions/otel/traces-browser.d.ts +17 -0
  48. package/dist/types/src/extensions/otel/traces-browser.d.ts.map +1 -1
  49. package/dist/types/src/extensions/otel/traces-browser.js +73 -12
  50. package/dist/types/src/extensions/otel/traces-browser.js.map +1 -1
  51. package/dist/types/src/extensions/otel/traces.d.ts +17 -0
  52. package/dist/types/src/extensions/otel/traces.d.ts.map +1 -1
  53. package/dist/types/src/extensions/otel/traces.js +59 -9
  54. package/dist/types/src/extensions/otel/traces.js.map +1 -1
  55. package/dist/types/src/extensions/posthog/extension.d.ts +13 -0
  56. package/dist/types/src/extensions/posthog/extension.d.ts.map +1 -1
  57. package/dist/types/src/extensions/posthog/extension.js +41 -28
  58. package/dist/types/src/extensions/posthog/extension.js.map +1 -1
  59. package/dist/types/src/extensions/posthog/log-processor.d.ts.map +1 -1
  60. package/dist/types/src/extensions/posthog/log-processor.js +10 -19
  61. package/dist/types/src/extensions/posthog/log-processor.js.map +1 -1
  62. package/dist/types/src/extensions/posthog/log-processor.test.js +2 -2
  63. package/dist/types/src/extensions/posthog/log-processor.test.js.map +1 -1
  64. package/dist/types/src/index.d.ts +0 -1
  65. package/dist/types/src/index.d.ts.map +1 -1
  66. package/dist/types/src/index.js +0 -1
  67. package/dist/types/src/index.js.map +1 -1
  68. package/dist/types/src/observability-extension.d.ts +1 -1
  69. package/dist/types/src/observability-extension.d.ts.map +1 -1
  70. package/dist/types/src/observability.d.ts.map +1 -1
  71. package/dist/types/src/observability.js +4 -2
  72. package/dist/types/src/observability.js.map +1 -1
  73. package/dist/types/src/observability.test.js +1 -1
  74. package/dist/types/src/observability.test.js.map +1 -1
  75. package/dist/types/src/providers/client-observability.d.ts.map +1 -1
  76. package/dist/types/src/providers/client-observability.js +2 -0
  77. package/dist/types/src/providers/client-observability.js.map +1 -1
  78. package/dist/types/src/providers/ip-data.d.ts.map +1 -1
  79. package/dist/types/src/providers/ip-data.js +4 -2
  80. package/dist/types/src/providers/ip-data.js.map +1 -1
  81. package/dist/types/src/storage/browser.d.ts.map +1 -1
  82. package/dist/types/src/storage/browser.js +15 -4
  83. package/dist/types/src/storage/browser.js.map +1 -1
  84. package/dist/types/src/storage/node.d.ts.map +1 -1
  85. package/dist/types/src/storage/node.js +1 -1
  86. package/dist/types/src/storage/node.js.map +1 -1
  87. package/dist/types/src/storage/node.test.js +1 -1
  88. package/dist/types/src/storage/node.test.js.map +1 -1
  89. package/dist/types/tsconfig.tsbuildinfo +1 -1
  90. package/package.json +36 -32
  91. package/src/cli-observability-secrets.json +2 -2
  92. package/src/extensions/otel/extension.ts +118 -17
  93. package/src/extensions/otel/logs.ts +6 -3
  94. package/src/extensions/otel/metrics.ts +2 -2
  95. package/src/extensions/otel/otel.ts +13 -0
  96. package/src/extensions/otel/span-processors.ts +45 -0
  97. package/src/extensions/otel/traces-browser.ts +95 -13
  98. package/src/extensions/otel/traces.ts +76 -12
  99. package/src/extensions/posthog/extension.ts +54 -27
  100. package/src/extensions/posthog/log-processor.test.ts +7 -6
  101. package/src/extensions/posthog/log-processor.ts +10 -21
  102. package/src/index.ts +0 -1
  103. package/src/observability-extension.ts +1 -1
  104. package/src/observability.test.ts +1 -1
  105. package/src/observability.ts +4 -2
  106. package/src/providers/client-observability.ts +2 -0
  107. package/src/providers/ip-data.ts +5 -2
  108. package/src/storage/browser.ts +14 -4
  109. package/src/storage/node.test.ts +1 -2
  110. package/src/storage/node.ts +1 -2
  111. package/src/vite-import-meta.d.ts +14 -0
  112. package/dist/lib/browser/chunk-K4VFBKST.mjs.map +0 -7
  113. package/dist/lib/browser/log-processor-FDLTDQEM.mjs.map +0 -7
  114. package/dist/lib/browser/logs-ATTRIUTL.mjs.map +0 -7
  115. package/dist/lib/browser/metrics-PKTV6IGF.mjs.map +0 -7
  116. package/dist/lib/browser/traces-browser-XYXBF5ZX.mjs +0 -62
  117. package/dist/lib/browser/traces-browser-XYXBF5ZX.mjs.map +0 -7
  118. package/dist/lib/node-esm/chunk-FEVP3MK4.mjs.map +0 -7
  119. package/dist/lib/node-esm/log-processor-TKJVJJSJ.mjs.map +0 -7
  120. package/dist/lib/node-esm/logs-7J45KLM7.mjs.map +0 -7
  121. package/dist/lib/node-esm/metrics-H7DDLYSG.mjs.map +0 -7
  122. package/dist/lib/node-esm/traces-KMTHMYFX.mjs +0 -44
  123. package/dist/lib/node-esm/traces-KMTHMYFX.mjs.map +0 -7
  124. package/dist/types/src/log-buffer.d.ts +0 -34
  125. package/dist/types/src/log-buffer.d.ts.map +0 -1
  126. package/dist/types/src/log-buffer.js +0 -70
  127. package/dist/types/src/log-buffer.js.map +0 -1
  128. package/dist/types/src/log-buffer.test.d.ts +0 -2
  129. package/dist/types/src/log-buffer.test.d.ts.map +0 -1
  130. package/dist/types/src/log-buffer.test.js +0 -107
  131. package/dist/types/src/log-buffer.test.js.map +0 -1
  132. package/src/log-buffer.test.ts +0 -134
  133. package/src/log-buffer.ts +0 -101
@@ -2,48 +2,112 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type Tracer, trace } from '@opentelemetry/api';
5
+ import {
6
+ ROOT_CONTEXT,
7
+ SpanStatusCode,
8
+ type Tracer,
9
+ context as otelContext,
10
+ propagation,
11
+ trace,
12
+ } from '@opentelemetry/api';
13
+ import { W3CTraceContextPropagator } from '@opentelemetry/core';
6
14
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
7
15
  import { BasicTracerProvider, BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
8
16
  import { ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';
9
- import { log } from 'debug';
10
17
 
11
- import { type StartSpanOptions, TRACE_PROCESSOR } from '@dxos/tracing';
18
+ import { log } from '@dxos/log';
19
+ import { type RemoteSpan, type StartSpanOptions, TRACE_PROCESSOR } from '@dxos/tracing';
12
20
 
13
- import { type OtelOptions } from './otel';
21
+ import { type OtelOptions, resolveOtlpUrl } from './otel';
22
+ import { TagInjectorSpanProcessor } from './span-processors';
14
23
 
15
24
  export class OtelTraces {
16
25
  private _tracer: Tracer;
26
+ private readonly _tracerProvider: BasicTracerProvider;
17
27
 
18
28
  constructor(private readonly options: OtelOptions) {
19
- const tracerProvider = new BasicTracerProvider({
29
+ propagation.setGlobalPropagator(new W3CTraceContextPropagator());
30
+
31
+ this._tracerProvider = new BasicTracerProvider({
20
32
  resource: this.options.resource,
21
33
  spanProcessors: [
34
+ new TagInjectorSpanProcessor(this.options.getTags),
22
35
  new BatchSpanProcessor(
23
36
  new OTLPTraceExporter({
24
- url: this.options.endpoint + '/v1/traces',
37
+ url: resolveOtlpUrl(this.options.endpoint + '/v1/traces'),
25
38
  headers: this.options.headers,
26
- concurrencyLimit: 10, // an optional limit on pending requests
39
+ concurrencyLimit: 10,
27
40
  }),
28
41
  ),
29
42
  ],
30
43
  });
31
44
 
32
- trace.setGlobalTracerProvider(tracerProvider);
45
+ trace.setGlobalTracerProvider(this._tracerProvider);
33
46
  this._tracer = trace.getTracer(
34
47
  'dxos-observability',
35
48
  this.options.resource.attributes[ATTR_SERVICE_VERSION]?.toString(),
36
49
  );
37
50
  }
38
51
 
52
+ /**
53
+ * Forcibly flush the BatchSpanProcessor. Call before process exit to avoid
54
+ * losing queued spans (which manifests as "Missing Span" in SigNoz — their
55
+ * already-exported children reference a parent that never made it to OTLP).
56
+ */
57
+ public async flush(): Promise<void> {
58
+ await this._tracerProvider.forceFlush();
59
+ }
60
+
61
+ /**
62
+ * Flush + shut down the tracer provider via `BasicTracerProvider.shutdown()`,
63
+ * which forces a final export then terminates all span processors.
64
+ *
65
+ * Terminal and effectively one-shot: safe to call after `flush()`, but
66
+ * `flush()` MUST NOT be called after `close()` — shutdown stops further
67
+ * exporting, so subsequent `close()`/`flush()` calls resolve without
68
+ * emitting new spans.
69
+ */
70
+ public async close(): Promise<void> {
71
+ await this._tracerProvider.shutdown();
72
+ }
73
+
39
74
  public start(): void {
40
75
  log('trace processor registered');
41
76
 
42
- TRACE_PROCESSOR.remoteTracing.registerProcessor({
43
- startSpan: (options: StartSpanOptions) => {
77
+ const tracer = this._tracer;
78
+
79
+ TRACE_PROCESSOR.tracingBackend = {
80
+ startSpan: (options: StartSpanOptions): RemoteSpan => {
44
81
  log('begin otel trace', { options });
45
- return this._tracer.startSpan(options.name, options);
82
+ const parentCtx = options.parentContext
83
+ ? propagation.extract(ROOT_CONTEXT, {
84
+ traceparent: options.parentContext.traceparent,
85
+ tracestate: options.parentContext.tracestate ?? '',
86
+ })
87
+ : otelContext.active();
88
+
89
+ const span = tracer.startSpan(options.name, options, parentCtx);
90
+
91
+ const sc = span.spanContext();
92
+ const spanContext =
93
+ sc && sc.traceId && sc.spanId
94
+ ? {
95
+ traceparent: `00-${sc.traceId}-${sc.spanId}-${(sc.traceFlags ?? 0).toString(16).padStart(2, '0')}`,
96
+ tracestate: sc.traceState?.serialize(),
97
+ }
98
+ : undefined;
99
+
100
+ return {
101
+ end: (endTime?: number) => span.end(endTime),
102
+ setError: (err: unknown) => {
103
+ if (err instanceof Error) {
104
+ span.recordException(err);
105
+ }
106
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : String(err) });
107
+ },
108
+ spanContext,
109
+ };
46
110
  },
47
- });
111
+ };
48
112
  }
49
113
  }
@@ -7,8 +7,8 @@ import { type PostHogConfig } from 'posthog-js';
7
7
 
8
8
  import { type Config } from '@dxos/config';
9
9
  import { log } from '@dxos/log';
10
+ import { type IdbLogStore } from '@dxos/log-store-idb';
10
11
 
11
- import { LogBuffer } from '../../log-buffer';
12
12
  import { type Extension } from '../../observability-extension';
13
13
  import { stubExtension } from '../stub';
14
14
 
@@ -19,6 +19,18 @@ export type ExtensionsOptions = {
19
19
  /** Deployment environment, e.g. `production` or `staging`. */
20
20
  environment?: string;
21
21
  posthog?: Partial<PostHogConfig>;
22
+ /**
23
+ * Shared persistent log store for debug log dumps.
24
+ * The owning app is expected to register `logStore.processor` with `log` itself —
25
+ * this extension only consumes the buffered logs (via `export()`).
26
+ */
27
+ logStore?: IdbLogStore;
28
+ /**
29
+ * Maximum byte size passed to `logStore.export()` when uploading feedback logs.
30
+ * Should match the upload limit enforced by the server receiving the logs.
31
+ * When omitted the full store is exported without trimming.
32
+ */
33
+ feedbackLogMaxSize?: number;
22
34
  };
23
35
 
24
36
  /** Upload serialized logs to the feedback-logs endpoint. Returns the R2 key on success. */
@@ -47,6 +59,8 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
47
59
  release,
48
60
  environment,
49
61
  posthog: posthogConfig,
62
+ logStore,
63
+ feedbackLogMaxSize,
50
64
  }) {
51
65
  if (typeof window === 'undefined') {
52
66
  log('PostHog is being stubbed because it is running in a worker.');
@@ -63,8 +77,8 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
63
77
 
64
78
  const { default: posthog } = yield* Effect.promise(() => import('posthog-js'));
65
79
  const { logProcessor } = yield* Effect.promise(() => import('./log-processor'));
66
- const logBuffer = new LogBuffer();
67
80
  let feedbackSurveyAvailable: boolean | null = null;
81
+ let unregisterPosthogProcessors: (() => void) | undefined;
68
82
 
69
83
  const checkFeedbackSurveyAvailable = (): Effect.Effect<boolean> =>
70
84
  feedbackSurveyId
@@ -98,17 +112,16 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
98
112
  ...(environment ? { environment } : {}),
99
113
  });
100
114
  }
101
- log.runtimeConfig.processors.push(logProcessor);
102
- log.runtimeConfig.processors.push(logBuffer.logProcessor);
115
+ unregisterPosthogProcessors?.();
116
+ const removePosthogLog = log.addProcessor(logProcessor);
117
+ unregisterPosthogProcessors = () => {
118
+ removePosthogLog();
119
+ };
103
120
  }),
104
121
  close: () =>
105
122
  Effect.sync(() => {
106
- for (const processor of [logProcessor, logBuffer.logProcessor]) {
107
- const index = log.runtimeConfig.processors.indexOf(processor);
108
- if (index !== -1) {
109
- log.runtimeConfig.processors.splice(index, 1);
110
- }
111
- }
123
+ unregisterPosthogProcessors?.();
124
+ unregisterPosthogProcessors = undefined;
112
125
  }),
113
126
  enable: () => Effect.sync(() => posthog.opt_in_capturing()),
114
127
  disable: () => Effect.sync(() => posthog.opt_out_capturing()),
@@ -143,25 +156,39 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Extension
143
156
  kind: 'feedback',
144
157
  // TODO(wittjosiah): Support custom surveys.
145
158
  captureUserFeedback: (form) => {
146
- posthog.getSurveys(async (surveys) => {
147
- const survey = surveys.find((survey) => survey.id === feedbackSurveyId);
148
- if (!survey || survey.questions.length === 0) {
149
- log.error('Missing feedback survey or survey has no questions', { feedbackSurveyId });
150
- return;
151
- }
159
+ return new Promise<string | undefined>((resolve, reject) => {
160
+ posthog.getSurveys((surveys) => {
161
+ void (async () => {
162
+ try {
163
+ const survey = surveys.find((survey) => survey.id === feedbackSurveyId);
164
+ if (!survey || survey.questions.length === 0) {
165
+ log.error('Missing feedback survey or survey has no questions', { feedbackSurveyId });
166
+ resolve(undefined);
167
+ return;
168
+ }
152
169
 
153
- let debugLogDumpKey: string | undefined;
154
- if (form.includeLogs !== false && logBuffer.size > 0) {
155
- debugLogDumpKey = await uploadLogs(logBuffer.serialize());
156
- }
170
+ let debugLogDumpKey: string | null = null;
171
+ if (form.includeLogs !== false && logStore !== undefined) {
172
+ const ndjson = await logStore.export({ maxSize: feedbackLogMaxSize });
173
+ if (ndjson.length > 0) {
174
+ debugLogDumpKey = (await uploadLogs(ndjson)) ?? 'failed';
175
+ }
176
+ }
157
177
 
158
- // https://posthog.com/docs/surveys/implementing-custom-surveys
159
- const question = survey.questions[0];
160
- posthog.capture('survey sent', {
161
- $survey_id: survey.id,
162
- $survey_questions: [{ id: question.id, question: question.question }],
163
- [`$survey_response_${question.id}`]: form.message,
164
- ...(debugLogDumpKey ? { debug_log_dump_key: debugLogDumpKey } : {}),
178
+ // https://posthog.com/docs/surveys/implementing-custom-surveys
179
+ const question = survey.questions[0];
180
+ const result = posthog.capture('survey sent', {
181
+ $survey_id: survey.id,
182
+ $survey_questions: [{ id: question.id, question: question.question }],
183
+ [`$survey_response_${question.id}`]: form.message,
184
+ debug_log_dump_key: debugLogDumpKey,
185
+ });
186
+ resolve(result?.uuid);
187
+ } catch (err) {
188
+ log.error('Failed to capture user feedback', { err });
189
+ reject(err);
190
+ }
191
+ })();
165
192
  });
166
193
  });
167
194
  },
@@ -5,7 +5,7 @@
5
5
  import { beforeEach, describe, expect, test, vi } from 'vitest';
6
6
 
7
7
  import { InvariantViolation } from '@dxos/invariant';
8
- import { type LogConfig, type LogEntry, LogLevel } from '@dxos/log';
8
+ import { type LogConfig, LogEntry, type LogEntryInit, LogLevel } from '@dxos/log';
9
9
 
10
10
  // Replace the posthog-js module with a stub so log-processor's top-level import resolves to our mock.
11
11
  // Dynamic imports are used so that both the mock and log-processor are loaded after vi.mock is hoisted.
@@ -23,11 +23,12 @@ const baseConfig: LogConfig = {
23
23
  processors: [],
24
24
  };
25
25
 
26
- const createEntry = (overrides: Partial<LogEntry> = {}): LogEntry => ({
27
- level: LogLevel.ERROR,
28
- message: 'test error',
29
- ...overrides,
30
- });
26
+ const createEntry = (overrides: Partial<LogEntryInit> = {}): LogEntry =>
27
+ new LogEntry({
28
+ level: LogLevel.ERROR,
29
+ message: 'test error',
30
+ ...overrides,
31
+ });
31
32
 
32
33
  describe('logProcessor', () => {
33
34
  beforeEach(() => {
@@ -23,15 +23,16 @@ export const logProcessor: LogProcessor = (config: LogConfig, entry: LogEntry) =
23
23
  }
24
24
 
25
25
  const additionalProperties: Record<string, string | boolean | number> = {};
26
- if (entry.meta) {
27
- additionalProperties.transaction = `${getRelativeFilename(entry.meta.F)}:${entry.meta.L}`;
28
- if (entry.meta.S?.hostSessionId) {
29
- additionalProperties.service_host_issue = true;
30
- additionalProperties.service_host_session = entry.meta.S?.hostSessionId;
31
- }
32
- if (entry.meta.S?.uptimeSeconds != null) {
33
- additionalProperties.uptime_seconds = entry.meta.S.uptimeSeconds;
34
- }
26
+ const { filename, line } = entry.computedMeta;
27
+ if (filename !== undefined && line !== undefined) {
28
+ additionalProperties.transaction = `${filename}:${line}`;
29
+ }
30
+ if (entry.meta?.S?.hostSessionId) {
31
+ additionalProperties.service_host_issue = true;
32
+ additionalProperties.service_host_session = entry.meta.S.hostSessionId;
33
+ }
34
+ if (entry.meta?.S?.uptimeSeconds != null) {
35
+ additionalProperties.uptime_seconds = entry.meta.S.uptimeSeconds;
35
36
  }
36
37
 
37
38
  if (capturedError instanceof InvariantViolation) {
@@ -40,15 +41,3 @@ export const logProcessor: LogProcessor = (config: LogConfig, entry: LogEntry) =
40
41
 
41
42
  posthog.captureException(capturedError, additionalProperties);
42
43
  };
43
-
44
- const getRelativeFilename = (filename: string) => {
45
- // TODO(burdon): Hack uses "packages" as an anchor (pre-parse NX?)
46
- // Including `packages/` part of the path so that excluded paths (e.g. from dist) are clickable in vscode.
47
- const match = filename.match(/.+\/(packages\/.+\/.+)/);
48
- if (match) {
49
- const [, filePath] = match;
50
- return filePath;
51
- }
52
-
53
- return filename;
54
- };
package/src/index.ts CHANGED
@@ -2,7 +2,6 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './log-buffer';
6
5
  export * as Observability from './observability';
7
6
  export * as ObservabilityExtension from './observability-extension';
8
7
  export * as ObservabilityProvider from './providers';
@@ -53,7 +53,7 @@ export type Events = {
53
53
  * Feedback extension API (kind-specific methods only).
54
54
  */
55
55
  export type Feedback = {
56
- captureUserFeedback(form: FeedbackForm): void;
56
+ captureUserFeedback(form: FeedbackForm): Promise<string | undefined>;
57
57
  };
58
58
 
59
59
  export type ExtensionApi =
@@ -363,7 +363,7 @@ describe('Observability', () => {
363
363
  Observability.addExtension(Effect.succeed(feedbackExt)),
364
364
  Observability.initialize,
365
365
  );
366
- obs.feedback.captureUserFeedback({ message: 'great app' });
366
+ void obs.feedback.captureUserFeedback({ message: 'great app' });
367
367
  expect(captureUserFeedback).toHaveBeenCalledWith({ message: 'great app' });
368
368
  }),
369
369
  );
@@ -214,10 +214,12 @@ class ObservabilityImpl implements Observability {
214
214
 
215
215
  get feedback(): Feedback {
216
216
  return {
217
- captureUserFeedback: (form) => {
217
+ captureUserFeedback: async (form) => {
218
+ let eventUuid: string | undefined;
218
219
  for (const extension of this._getExtensions('feedback')) {
219
- extension.captureUserFeedback(form);
220
+ eventUuid = (await extension.captureUserFeedback(form)) ?? eventUuid;
220
221
  }
222
+ return eventUuid;
221
223
  },
222
224
  };
223
225
  }
@@ -119,7 +119,9 @@ export const networkMetricsProvider = (clientServices: Partial<ClientServices>):
119
119
  export const runtimeMetricsProvider = (clientServices: Partial<ClientServices>): DataProvider =>
120
120
  Effect.fn(function* (observability) {
121
121
  const ctx = new Context();
122
+ log('runtimeMetricsProvider: requesting platform from SystemService');
122
123
  const platform = yield* Effect.promise(() => clientServices.SystemService!.getPlatform());
124
+ log('runtimeMetricsProvider: platform received');
123
125
  invariant(platform, 'platform is required');
124
126
 
125
127
  observability.setTags({
@@ -34,6 +34,9 @@ type CachedIPData = {
34
34
  const getIPData = Effect.fn(function* (config: Config) {
35
35
  const httpClient = yield* HttpClient.HttpClient;
36
36
 
37
+ // Disable tracing to avoid CORS errors from traceparent header on cross-origin requests.
38
+ const httpClientNoTrace = httpClient.pipe(HttpClient.withTracerDisabledWhen(() => true));
39
+
37
40
  // Check cache first.
38
41
  const cachedData = yield* Effect.promise(() => localForage.getItem<CachedIPData>('dxos:observability:ipdata'));
39
42
  if (cachedData && cachedData.timestamp > Date.now() - IP_DATA_CACHE_TIMEOUT) {
@@ -44,7 +47,7 @@ const getIPData = Effect.fn(function* (config: Config) {
44
47
  const IPDATA_API_KEY = config.get('runtime.app.env.DX_IPDATA_API_KEY');
45
48
  if (IPDATA_API_KEY) {
46
49
  const data = yield* HttpClientRequest.get(`https://api.ipdata.co?api-key=${IPDATA_API_KEY}`).pipe(
47
- httpClient.execute,
50
+ httpClientNoTrace.execute,
48
51
  Effect.flatMap((res) => res.json),
49
52
  Effect.flatMap(Schema.decodeUnknown(IPData)),
50
53
  );
@@ -82,7 +85,7 @@ export const provider =
82
85
  Effect.provide(FetchHttpClient.layer),
83
86
  Effect.catchAll((err) =>
84
87
  Effect.sync(() => {
85
- log.catch(err);
88
+ log.verbose('ipdata fetch failed', { err });
86
89
  }),
87
90
  ),
88
91
  );
@@ -6,6 +6,7 @@
6
6
  import * as localForage from 'localforage';
7
7
 
8
8
  import { log } from '@dxos/log';
9
+ import { compositeKey } from '@dxos/util';
9
10
 
10
11
  const OBSERVABILITY_DISABLED_KEY = 'observability-disabled';
11
12
  const OBSERVABILITY_GROUP_KEY = 'observability-group';
@@ -20,7 +21,7 @@ export const showObservabilityBanner = () => {
20
21
  */
21
22
  export const isObservabilityDisabled = async (namespace: string): Promise<boolean> => {
22
23
  try {
23
- return (await localForage.getItem(`${namespace}:${OBSERVABILITY_DISABLED_KEY}`)) === 'true';
24
+ return (await localForage.getItem(compositeKey(namespace, OBSERVABILITY_DISABLED_KEY))) === 'true';
24
25
  } catch (err) {
25
26
  log.catch('Failed to check if observability is disabled, assuming it is', err);
26
27
  return true;
@@ -32,10 +33,19 @@ export const isObservabilityDisabled = async (namespace: string): Promise<boolea
32
33
  */
33
34
  export const storeObservabilityDisabled = async (namespace: string, value: boolean) => {
34
35
  try {
35
- await localForage.setItem(`${namespace}:${OBSERVABILITY_DISABLED_KEY}`, String(value));
36
+ await localForage.setItem(compositeKey(namespace, OBSERVABILITY_DISABLED_KEY), String(value));
36
37
  } catch (err) {
37
38
  log.catch('Failed to store observability disabled', err);
38
39
  }
40
+ // Mirror to localStorage so the synchronous opt-out check in the OTEL extension picks it up
41
+ // without waiting for an async IndexedDB read.
42
+ try {
43
+ if (typeof localStorage !== 'undefined') {
44
+ localStorage.setItem(`${namespace}/${OBSERVABILITY_DISABLED_KEY}`, String(value));
45
+ }
46
+ } catch {
47
+ // localStorage not available (e.g., in workers).
48
+ }
39
49
  };
40
50
 
41
51
  /**
@@ -43,7 +53,7 @@ export const storeObservabilityDisabled = async (namespace: string, value: boole
43
53
  */
44
54
  export const getObservabilityGroup = async (namespace: string): Promise<string | undefined> => {
45
55
  try {
46
- return (await localForage.getItem(`${namespace}:${OBSERVABILITY_GROUP_KEY}`)) ?? undefined;
56
+ return (await localForage.getItem(compositeKey(namespace, OBSERVABILITY_GROUP_KEY))) ?? undefined;
47
57
  } catch (err) {
48
58
  log.catch('Failed to get observability group', err);
49
59
  }
@@ -54,7 +64,7 @@ export const getObservabilityGroup = async (namespace: string): Promise<string |
54
64
  */
55
65
  export const storeObservabilityGroup = async (namespace: string, value: string) => {
56
66
  try {
57
- await localForage.setItem(`${namespace}:${OBSERVABILITY_GROUP_KEY}`, value);
67
+ await localForage.setItem(compositeKey(namespace, OBSERVABILITY_GROUP_KEY), value);
58
68
  } catch (err) {
59
69
  log.catch('Failed to store observability group', err);
60
70
  }
@@ -2,12 +2,11 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import yaml from 'js-yaml';
5
6
  import { existsSync } from 'node:fs';
6
7
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
7
8
  import { tmpdir } from 'node:os';
8
9
  import { join } from 'node:path';
9
-
10
- import yaml from 'js-yaml';
11
10
  import { validate as validateUuid } from 'uuid';
12
11
  import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
13
12
 
@@ -1,11 +1,10 @@
1
+ import yaml from 'js-yaml';
1
2
  //
2
3
  // Copyright 2022 DXOS.org
3
4
  //
4
5
  import { existsSync, statSync } from 'node:fs';
5
6
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
6
7
  import { join } from 'node:path';
7
-
8
- import yaml from 'js-yaml';
9
8
  import { v4 as uuid, validate as validateUuid } from 'uuid';
10
9
 
11
10
  /**
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ /**
6
+ * Vite replaces `import.meta.env` at build time. Aligns with `vite/client` without taking a Vite dependency.
7
+ * @see https://vitejs.dev/guide/env-and-mode.html
8
+ */
9
+ interface ImportMetaEnv {
10
+ readonly DEV: boolean;
11
+ readonly PROD: boolean;
12
+ readonly MODE: string;
13
+ readonly SSR: boolean;
14
+ }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/extensions/otel/otel.ts"],
4
- "sourcesContent": ["//\n// Copyright 2024 DXOS.org\n//\n\nimport { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api';\nimport { type Resource } from '@opentelemetry/resources';\n\nexport type OtelOptions = {\n endpoint: string;\n headers: Record<string, string>;\n resource: Resource;\n getTags: () => { [key: string]: string };\n consoleDiagLogLevel?: string;\n};\n\nexport const setDiagLogger = (level?: string) => {\n const logLevel = DiagLogLevel[level as keyof typeof DiagLogLevel];\n if (logLevel) {\n diag.setLogger(new DiagConsoleLogger(), logLevel);\n }\n};\n"],
5
- "mappings": ";AAIA,SAASA,mBAAmBC,cAAcC,YAAY;AAW/C,IAAMC,gBAAgB,CAACC,UAAAA;AAC5B,QAAMC,WAAWC,aAAaF,KAAAA;AAC9B,MAAIC,UAAU;AACZE,SAAKC,UAAU,IAAIC,kBAAAA,GAAqBJ,QAAAA;EAC1C;AACF;",
6
- "names": ["DiagConsoleLogger", "DiagLogLevel", "diag", "setDiagLogger", "level", "logLevel", "DiagLogLevel", "diag", "setLogger", "DiagConsoleLogger"]
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/extensions/posthog/log-processor.ts"],
4
- "sourcesContent": ["//\n// Copyright 2025 DXOS.org\n//\n\nimport posthog from 'posthog-js';\n\nimport { InvariantViolation } from '@dxos/invariant';\nimport { type LogConfig, type LogEntry, LogLevel, type LogProcessor, shouldLog } from '@dxos/log';\n\nexport const logProcessor: LogProcessor = (config: LogConfig, entry: LogEntry) => {\n // Don't forward logs from remote sessions.\n if (!shouldLog(entry, config.captureFilters) || entry.meta?.S?.remoteSessionId) {\n return;\n }\n\n let capturedError = entry.error;\n if (capturedError == null && entry.level === LogLevel.ERROR) {\n capturedError = Object.values(entry.context ?? {}).find((v): v is Error => v instanceof Error);\n }\n\n if (!capturedError) {\n return;\n }\n\n const additionalProperties: Record<string, string | boolean | number> = {};\n if (entry.meta) {\n additionalProperties.transaction = `${getRelativeFilename(entry.meta.F)}:${entry.meta.L}`;\n if (entry.meta.S?.hostSessionId) {\n additionalProperties.service_host_issue = true;\n additionalProperties.service_host_session = entry.meta.S?.hostSessionId;\n }\n if (entry.meta.S?.uptimeSeconds != null) {\n additionalProperties.uptime_seconds = entry.meta.S.uptimeSeconds;\n }\n }\n\n if (capturedError instanceof InvariantViolation) {\n additionalProperties.invariant_violation = true;\n }\n\n posthog.captureException(capturedError, additionalProperties);\n};\n\nconst getRelativeFilename = (filename: string) => {\n // TODO(burdon): Hack uses \"packages\" as an anchor (pre-parse NX?)\n // Including `packages/` part of the path so that excluded paths (e.g. from dist) are clickable in vscode.\n const match = filename.match(/.+\\/(packages\\/.+\\/.+)/);\n if (match) {\n const [, filePath] = match;\n return filePath;\n }\n\n return filename;\n};\n"],
5
- "mappings": ";;;AAIA,OAAOA,aAAa;AAEpB,SAASC,0BAA0B;AACnC,SAAwCC,UAA6BC,iBAAiB;AAE/E,IAAMC,eAA6B,CAACC,QAAmBC,UAAAA;AAE5D,MAAI,CAACC,UAAUD,OAAOD,OAAOG,cAAc,KAAKF,MAAMG,MAAMC,GAAGC,iBAAiB;AAC9E;EACF;AAEA,MAAIC,gBAAgBN,MAAMO;AAC1B,MAAID,iBAAiB,QAAQN,MAAMQ,UAAUC,SAASC,OAAO;AAC3DJ,oBAAgBK,OAAOC,OAAOZ,MAAMa,WAAW,CAAC,CAAA,EAAGC,KAAK,CAACC,MAAkBA,aAAaC,KAAAA;EAC1F;AAEA,MAAI,CAACV,eAAe;AAClB;EACF;AAEA,QAAMW,uBAAkE,CAAC;AACzE,MAAIjB,MAAMG,MAAM;AACdc,yBAAqBC,cAAc,GAAGC,oBAAoBnB,MAAMG,KAAKiB,CAAC,CAAA,IAAKpB,MAAMG,KAAKkB,CAAC;AACvF,QAAIrB,MAAMG,KAAKC,GAAGkB,eAAe;AAC/BL,2BAAqBM,qBAAqB;AAC1CN,2BAAqBO,uBAAuBxB,MAAMG,KAAKC,GAAGkB;IAC5D;AACA,QAAItB,MAAMG,KAAKC,GAAGqB,iBAAiB,MAAM;AACvCR,2BAAqBS,iBAAiB1B,MAAMG,KAAKC,EAAEqB;IACrD;EACF;AAEA,MAAInB,yBAAyBqB,oBAAoB;AAC/CV,yBAAqBW,sBAAsB;EAC7C;AAEAC,UAAQC,iBAAiBxB,eAAeW,oBAAAA;AAC1C;AAEA,IAAME,sBAAsB,CAACY,aAAAA;AAG3B,QAAMC,QAAQD,SAASC,MAAM,wBAAA;AAC7B,MAAIA,OAAO;AACT,UAAM,CAAA,EAAGC,QAAAA,IAAYD;AACrB,WAAOC;EACT;AAEA,SAAOF;AACT;",
6
- "names": ["posthog", "InvariantViolation", "LogLevel", "shouldLog", "logProcessor", "config", "entry", "shouldLog", "captureFilters", "meta", "S", "remoteSessionId", "capturedError", "error", "level", "LogLevel", "ERROR", "Object", "values", "context", "find", "v", "Error", "additionalProperties", "transaction", "getRelativeFilename", "F", "L", "hostSessionId", "service_host_issue", "service_host_session", "uptimeSeconds", "uptime_seconds", "InvariantViolation", "invariant_violation", "posthog", "captureException", "filename", "match", "filePath"]
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/extensions/otel/logs.ts"],
4
- "sourcesContent": ["//\n// Copyright 2024 DXOS.org\n//\n\nimport { SeverityNumber } from '@opentelemetry/api-logs';\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';\nimport { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs';\nimport { ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions';\n\nimport {\n type LogConfig,\n type LogEntry,\n LogLevel,\n type LogProcessor,\n getContextFromEntry,\n getRelativeFilename,\n} from '@dxos/log';\n\nimport { type OtelOptions, setDiagLogger } from './otel';\n\nconst FLATTEN_DEPTH = 1;\n\nexport type OtelLogOptions = OtelOptions & {\n logLevel: LogLevel;\n /**\n * Include logs forwarded from the shared worker via LoggingService.\n *\n * @deprecated\n */\n includeSharedWorkerLogs?: boolean;\n};\n\nexport class OtelLogs {\n private _loggerProvider: LoggerProvider;\n constructor(private readonly options: OtelLogOptions) {\n setDiagLogger(options.consoleDiagLogLevel);\n const logExporter = new OTLPLogExporter({\n url: this.options.endpoint + '/v1/logs',\n headers: this.options.headers,\n concurrencyLimit: 10, // an optional limit on pending requests\n });\n this._loggerProvider = new LoggerProvider({\n resource: this.options.resource,\n processors: [new BatchLogRecordProcessor(logExporter)],\n });\n }\n\n public readonly logProcessor: LogProcessor = (_config: LogConfig, entry: LogEntry) => {\n const logger = this._loggerProvider.getLogger(\n 'dxos-observability',\n this.options.resource.attributes[ATTR_SERVICE_VERSION]?.toString(),\n );\n\n if (\n entry.level < this.options.logLevel ||\n (!this.options.includeSharedWorkerLogs && entry.meta?.S?.remoteSessionId)\n ) {\n return;\n }\n\n const attributes = {\n ...this.options.getTags(),\n ...(entry.meta ? { meta: { file: getRelativeFilename(entry.meta.F), line: entry.meta.L } } : {}),\n ...(entry.error ? { error: entry.error.stack } : {}),\n ...stringifyValues(getContextFromEntry(entry), 'ctx_'),\n };\n\n logger.emit({\n severityNumber: convertLevel(entry.level),\n body: entry.message,\n attributes,\n });\n };\n\n flush(): Promise<void> {\n return this._loggerProvider.forceFlush();\n }\n\n close(): Promise<void> {\n return this._loggerProvider.shutdown();\n }\n}\n\nconst convertLevel = (level: LogLevel): SeverityNumber => {\n switch (level) {\n case LogLevel.DEBUG:\n return SeverityNumber.DEBUG;\n case LogLevel.VERBOSE:\n return SeverityNumber.INFO;\n case LogLevel.INFO:\n return SeverityNumber.INFO;\n case LogLevel.WARN:\n return SeverityNumber.WARN;\n case LogLevel.ERROR:\n return SeverityNumber.ERROR;\n default:\n return SeverityNumber.ERROR;\n }\n};\n\nconst safeStringify = (value: unknown): string => {\n try {\n return JSON.stringify(value);\n } catch {\n return '[Circular]';\n }\n};\n\n// TODO(wittjosiah): Reconcile logging utils w/ EDGE.\nconst stringifyValues = (object: object | undefined, keyPrefix?: string, depth: number = 1) => {\n if (!object) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const [key, value] of Object.entries(object)) {\n if (value === undefined) {\n continue;\n }\n const newKey = keyPrefix ? `${keyPrefix}${key}` : key;\n if (typeof value === 'object') {\n if (!value || Array.isArray(value) || depth > FLATTEN_DEPTH) {\n result[newKey] = safeStringify(value);\n } else {\n const flattened = stringifyValues(value, `${newKey}_`, depth + 1);\n for (const [flattenedKey, flattenedValue] of Object.entries(flattened)) {\n result[flattenedKey] = flattenedValue;\n }\n }\n } else {\n result[newKey] = String(value);\n }\n }\n return result;\n};\n"],
5
- "mappings": "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",
6
- "names": ["SeverityNumber", "OTLPLogExporter", "BatchLogRecordProcessor", "LoggerProvider", "ATTR_SERVICE_VERSION", "LogLevel", "getContextFromEntry", "getRelativeFilename", "FLATTEN_DEPTH", "OtelLogs", "_loggerProvider", "options", "setDiagLogger", "consoleDiagLogLevel", "logExporter", "OTLPLogExporter", "url", "endpoint", "headers", "concurrencyLimit", "LoggerProvider", "resource", "processors", "BatchLogRecordProcessor", "logProcessor", "_config", "entry", "logger", "getLogger", "attributes", "ATTR_SERVICE_VERSION", "toString", "level", "logLevel", "includeSharedWorkerLogs", "meta", "S", "remoteSessionId", "getTags", "file", "getRelativeFilename", "F", "line", "L", "error", "stack", "stringifyValues", "getContextFromEntry", "emit", "severityNumber", "convertLevel", "body", "message", "flush", "forceFlush", "close", "shutdown", "LogLevel", "DEBUG", "SeverityNumber", "VERBOSE", "INFO", "WARN", "ERROR", "safeStringify", "value", "JSON", "stringify", "object", "keyPrefix", "depth", "result", "key", "Object", "entries", "undefined", "newKey", "Array", "isArray", "flattened", "flattenedKey", "flattenedValue", "String"]
7
- }
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../../../src/extensions/otel/metrics.ts"],
4
- "sourcesContent": ["//\n// Copyright 2024 DXOS.org\n//\n\nimport { type Meter } from '@opentelemetry/api';\nimport { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';\nimport { MeterProvider, PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';\n\nimport { log } from '@dxos/log';\nimport { type MetricData, TRACE_PROCESSOR } from '@dxos/tracing';\n\nimport { type OtelOptions, setDiagLogger } from './otel';\n\nconst EXPORT_INTERVAL = 60 * 1000;\n\nexport class OtelMetrics {\n private _meterProvider: MeterProvider;\n private _meter: Meter;\n\n constructor(private readonly options: OtelOptions) {\n // TODO: improve error handling/logging\n // https://github.com/open-telemetry/opentelemetry-js/issues/4823\n setDiagLogger(options.consoleDiagLogLevel);\n\n const grafanaMetricReader = new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: this.options.endpoint + '/v1/metrics',\n headers: this.options.headers,\n }),\n exportIntervalMillis: EXPORT_INTERVAL,\n });\n\n this._meterProvider = new MeterProvider({\n resource: this.options.resource,\n readers: [grafanaMetricReader],\n });\n this._meter = this._meterProvider.getMeter('dxos-observability');\n\n const metrics = {\n // TODO: update metrics names and remove prefix?\n increment: (name: string, value?: number, data?: MetricData) => {\n this.increment(name, value, convertTags(data));\n },\n distribution: (name: string, value: number, data?: MetricData) => {\n this.distribution(name, value, convertTags(data));\n },\n set: (name: string, value: number | string, data?: MetricData) => {\n // Not implemented, not part of Otel spec.\n },\n gauge: (name: string, value: number, data?: MetricData) => {\n this.gauge(name, value, convertTags(data));\n },\n };\n\n TRACE_PROCESSOR.remoteMetrics.registerProcessor(metrics);\n }\n\n gauge(name: string, value: number, tags?: any): void {\n const gauge = this._meter.createGauge(name);\n log('otel gauge', { name, value, tags: { ...this.options.getTags(), ...tags } });\n gauge.record(value, { ...this.options.getTags(), ...tags });\n }\n\n increment(name: string, value?: number, tags?: any): void {\n const counter = this._meter.createCounter(name);\n log('otel counter', { name, value, tags: { ...this.options.getTags(), ...tags } });\n counter.add(value ?? 1, { ...this.options.getTags(), ...tags });\n }\n\n distribution(name: string, value: number, tags?: any): void {\n const distribution = this._meter.createHistogram(name);\n log('otel distribution', { name, value, tags: { ...this.options.getTags(), ...tags } });\n distribution.record(value, { ...this.options.getTags(), ...tags });\n }\n\n flush(): Promise<void> {\n return this._meterProvider.forceFlush();\n }\n\n close(): Promise<void> {\n return this._meterProvider.shutdown();\n }\n}\n\nconst convertTags = (data?: MetricData) => {\n if (data && data.tags) {\n return Object.entries(data.tags).reduce<{ [key: string]: any }>((obj, [key, value]) => {\n obj[key] = value;\n return obj;\n }, {});\n } else {\n return {};\n }\n};\n"],
5
- "mappings": ";;;;;;AAKA,SAASA,0BAA0B;AACnC,SAASC,eAAeC,qCAAqC;AAE7D,SAASC,WAAW;AACpB,SAA0BC,uBAAuB;;AAIjD,IAAMC,kBAAkB,KAAK;AAEtB,IAAMC,cAAN,MAAMA;;EACHC;EACAC;EAER,YAA6BC,SAAsB;SAAtBA,UAAAA;AAG3BC,kBAAcD,QAAQE,mBAAmB;AAEzC,UAAMC,sBAAsB,IAAIC,8BAA8B;MAC5DC,UAAU,IAAIC,mBAAmB;QAC/BC,KAAK,KAAKP,QAAQQ,WAAW;QAC7BC,SAAS,KAAKT,QAAQS;MACxB,CAAA;MACAC,sBAAsBd;IACxB,CAAA;AAEA,SAAKE,iBAAiB,IAAIa,cAAc;MACtCC,UAAU,KAAKZ,QAAQY;MACvBC,SAAS;QAACV;;IACZ,CAAA;AACA,SAAKJ,SAAS,KAAKD,eAAegB,SAAS,oBAAA;AAE3C,UAAMC,UAAU;;MAEdC,WAAW,CAACC,MAAcC,OAAgBC,SAAAA;AACxC,aAAKH,UAAUC,MAAMC,OAAOE,YAAYD,IAAAA,CAAAA;MAC1C;MACAE,cAAc,CAACJ,MAAcC,OAAeC,SAAAA;AAC1C,aAAKE,aAAaJ,MAAMC,OAAOE,YAAYD,IAAAA,CAAAA;MAC7C;MACAG,KAAK,CAACL,MAAcC,OAAwBC,SAAAA;MAE5C;MACAI,OAAO,CAACN,MAAcC,OAAeC,SAAAA;AACnC,aAAKI,MAAMN,MAAMC,OAAOE,YAAYD,IAAAA,CAAAA;MACtC;IACF;AAEAK,oBAAgBC,cAAcC,kBAAkBX,OAAAA;EAClD;EAEAQ,MAAMN,MAAcC,OAAeS,MAAkB;AACnD,UAAMJ,QAAQ,KAAKxB,OAAO6B,YAAYX,IAAAA;AACtCY,QAAI,cAAc;MAAEZ;MAAMC;MAAOS,MAAM;QAAE,GAAG,KAAK3B,QAAQ8B,QAAO;QAAI,GAAGH;MAAK;IAAE,GAAA;;;;;;AAC9EJ,UAAMQ,OAAOb,OAAO;MAAE,GAAG,KAAKlB,QAAQ8B,QAAO;MAAI,GAAGH;IAAK,CAAA;EAC3D;EAEAX,UAAUC,MAAcC,OAAgBS,MAAkB;AACxD,UAAMK,UAAU,KAAKjC,OAAOkC,cAAchB,IAAAA;AAC1CY,QAAI,gBAAgB;MAAEZ;MAAMC;MAAOS,MAAM;QAAE,GAAG,KAAK3B,QAAQ8B,QAAO;QAAI,GAAGH;MAAK;IAAE,GAAA;;;;;;AAChFK,YAAQE,IAAIhB,SAAS,GAAG;MAAE,GAAG,KAAKlB,QAAQ8B,QAAO;MAAI,GAAGH;IAAK,CAAA;EAC/D;EAEAN,aAAaJ,MAAcC,OAAeS,MAAkB;AAC1D,UAAMN,eAAe,KAAKtB,OAAOoC,gBAAgBlB,IAAAA;AACjDY,QAAI,qBAAqB;MAAEZ;MAAMC;MAAOS,MAAM;QAAE,GAAG,KAAK3B,QAAQ8B,QAAO;QAAI,GAAGH;MAAK;IAAE,GAAA;;;;;;AACrFN,iBAAaU,OAAOb,OAAO;MAAE,GAAG,KAAKlB,QAAQ8B,QAAO;MAAI,GAAGH;IAAK,CAAA;EAClE;EAEAS,QAAuB;AACrB,WAAO,KAAKtC,eAAeuC,WAAU;EACvC;EAEAC,QAAuB;AACrB,WAAO,KAAKxC,eAAeyC,SAAQ;EACrC;AACF;AAEA,IAAMnB,cAAc,CAACD,SAAAA;AACnB,MAAIA,QAAQA,KAAKQ,MAAM;AACrB,WAAOa,OAAOC,QAAQtB,KAAKQ,IAAI,EAAEe,OAA+B,CAACC,KAAK,CAACC,KAAK1B,KAAAA,MAAM;AAChFyB,UAAIC,GAAAA,IAAO1B;AACX,aAAOyB;IACT,GAAG,CAAC,CAAA;EACN,OAAO;AACL,WAAO,CAAC;EACV;AACF;",
6
- "names": ["OTLPMetricExporter", "MeterProvider", "PeriodicExportingMetricReader", "log", "TRACE_PROCESSOR", "EXPORT_INTERVAL", "OtelMetrics", "_meterProvider", "_meter", "options", "setDiagLogger", "consoleDiagLogLevel", "grafanaMetricReader", "PeriodicExportingMetricReader", "exporter", "OTLPMetricExporter", "url", "endpoint", "headers", "exportIntervalMillis", "MeterProvider", "resource", "readers", "getMeter", "metrics", "increment", "name", "value", "data", "convertTags", "distribution", "set", "gauge", "TRACE_PROCESSOR", "remoteMetrics", "registerProcessor", "tags", "createGauge", "log", "getTags", "record", "counter", "createCounter", "add", "createHistogram", "flush", "forceFlush", "close", "shutdown", "Object", "entries", "reduce", "obj", "key"]
7
- }
@@ -1,62 +0,0 @@
1
- import "./chunk-J5LGTIGS.mjs";
2
-
3
- // src/extensions/otel/traces-browser.ts
4
- import { trace } from "@opentelemetry/api";
5
- import { getWebAutoInstrumentations } from "@opentelemetry/auto-instrumentations-web";
6
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
7
- import { registerInstrumentations } from "@opentelemetry/instrumentation";
8
- import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
9
- import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
10
- import { ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
11
- import { log } from "@dxos/log";
12
- import { TRACE_PROCESSOR } from "@dxos/tracing";
13
- var __dxlog_file = "/__w/dxos/dxos/packages/sdk/observability/src/extensions/otel/traces-browser.ts";
14
- var OtelTraces = class {
15
- options;
16
- _tracer;
17
- constructor(options) {
18
- this.options = options;
19
- const tracerProvider = new WebTracerProvider({
20
- resource: this.options.resource,
21
- spanProcessors: [
22
- new BatchSpanProcessor(new OTLPTraceExporter({
23
- url: this.options.endpoint + "/v1/traces",
24
- headers: this.options.headers,
25
- concurrencyLimit: 10
26
- }))
27
- ]
28
- });
29
- trace.setGlobalTracerProvider(tracerProvider);
30
- this._tracer = trace.getTracer("dxos-observability", this.options.resource.attributes[ATTR_SERVICE_VERSION]?.toString());
31
- }
32
- start() {
33
- log("trace processor registered", void 0, {
34
- F: __dxlog_file,
35
- L: 44,
36
- S: this,
37
- C: (f, a) => f(...a)
38
- });
39
- registerInstrumentations({
40
- instrumentations: [
41
- getWebAutoInstrumentations()
42
- ]
43
- });
44
- TRACE_PROCESSOR.remoteTracing.registerProcessor({
45
- startSpan: (options) => {
46
- log("begin otel trace", {
47
- options
48
- }, {
49
- F: __dxlog_file,
50
- L: 52,
51
- S: this,
52
- C: (f, a) => f(...a)
53
- });
54
- return this._tracer.startSpan(options.name, options);
55
- }
56
- });
57
- }
58
- };
59
- export {
60
- OtelTraces
61
- };
62
- //# sourceMappingURL=traces-browser-XYXBF5ZX.mjs.map