@dxos/observability 0.13.0 → 0.14.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 (33) hide show
  1. package/dist/lib/Observability.mjs +7 -2
  2. package/dist/lib/Observability.mjs.map +1 -1
  3. package/dist/lib/chunk-ObservabilityExtension.mjs +12 -10
  4. package/dist/lib/chunk-ObservabilityExtension.mjs.map +1 -1
  5. package/dist/lib/chunk-OtelSpanSink.mjs +5 -1
  6. package/dist/lib/chunk-OtelSpanSink.mjs.map +1 -1
  7. package/dist/types/src/Observability.d.ts.map +1 -1
  8. package/dist/types/src/Observability.js +7 -0
  9. package/dist/types/src/Observability.js.map +1 -1
  10. package/dist/types/src/Observability.test.js +13 -1
  11. package/dist/types/src/Observability.test.js.map +1 -1
  12. package/dist/types/src/ObservabilityExtension.d.ts +4 -0
  13. package/dist/types/src/ObservabilityExtension.d.ts.map +1 -1
  14. package/dist/types/src/extensions/otel/tail-sampling.d.ts +2 -0
  15. package/dist/types/src/extensions/otel/tail-sampling.d.ts.map +1 -1
  16. package/dist/types/src/extensions/otel/tail-sampling.js +5 -0
  17. package/dist/types/src/extensions/otel/tail-sampling.js.map +1 -1
  18. package/dist/types/src/extensions/otel/tail-sampling.test.js +5 -0
  19. package/dist/types/src/extensions/otel/tail-sampling.test.js.map +1 -1
  20. package/dist/types/src/extensions/posthog/extension.d.ts.map +1 -1
  21. package/dist/types/src/extensions/posthog/extension.js +5 -2
  22. package/dist/types/src/extensions/posthog/extension.js.map +1 -1
  23. package/dist/types/src/version.d.ts +1 -1
  24. package/dist/types/src/version.js +1 -1
  25. package/dist/types/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +19 -19
  27. package/src/Observability.test.ts +15 -1
  28. package/src/Observability.ts +7 -0
  29. package/src/ObservabilityExtension.ts +5 -0
  30. package/src/extensions/otel/tail-sampling.test.ts +6 -0
  31. package/src/extensions/otel/tail-sampling.ts +7 -0
  32. package/src/extensions/posthog/extension.ts +10 -2
  33. package/src/version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/observability",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Provides a common interface for app and platform observability",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -109,27 +109,27 @@
109
109
  "posthog-node": "^5.51.4",
110
110
  "uuid": "^9.0.0",
111
111
  "zone.js": "^0.15.0",
112
- "@dxos/async": "0.13.0",
113
- "@dxos/client": "0.13.0",
114
- "@dxos/client-protocol": "0.13.0",
115
- "@dxos/config": "0.13.0",
116
- "@dxos/context": "0.13.0",
117
- "@dxos/echo": "0.13.0",
118
- "@dxos/effect": "0.13.0",
119
- "@dxos/invariant": "0.13.0",
120
- "@dxos/log": "0.13.0",
121
- "@dxos/log-store-idb": "0.13.0",
122
- "@dxos/node-std": "0.13.0",
123
- "@dxos/protocols": "0.13.0",
124
- "@dxos/tracing": "0.13.0",
125
- "@dxos/util": "0.13.0"
112
+ "@dxos/async": "0.14.0",
113
+ "@dxos/client": "0.14.0",
114
+ "@dxos/client-protocol": "0.14.0",
115
+ "@dxos/config": "0.14.0",
116
+ "@dxos/context": "0.14.0",
117
+ "@dxos/echo": "0.14.0",
118
+ "@dxos/effect": "0.14.0",
119
+ "@dxos/invariant": "0.14.0",
120
+ "@dxos/log": "0.14.0",
121
+ "@dxos/log-store-idb": "0.14.0",
122
+ "@dxos/node-std": "0.14.0",
123
+ "@dxos/protocols": "0.14.0",
124
+ "@dxos/tracing": "0.14.0",
125
+ "@dxos/util": "0.14.0"
126
126
  },
127
127
  "devDependencies": {
128
128
  "@types/js-yaml": "^4.0.5",
129
- "@dxos/ai": "0.13.0",
130
- "@dxos/client-services": "0.13.0",
131
- "@dxos/errors": "0.13.0",
132
- "@dxos/network-manager": "0.13.0"
129
+ "@dxos/ai": "0.14.0",
130
+ "@dxos/client-services": "0.14.0",
131
+ "@dxos/errors": "0.14.0",
132
+ "@dxos/network-manager": "0.14.0"
133
133
  },
134
134
  "peerDependencies": {
135
135
  "effect": "4.0.0"
@@ -362,8 +362,18 @@ describe('Observability', () => {
362
362
  const uploadLogs = vi.fn(async () => 'logs/1.ndjson');
363
363
  const sessionContext = vi.fn(() => ({ distinctId: 'd', widgetSessionId: 'w' }));
364
364
  const flushLogs = vi.fn(async () => {});
365
+ const uploadNdjson = vi.fn(async () => 'trajectories/1.ndjson.gz');
365
366
  const supportExt = createMockExtension({
366
- apis: [{ kind: 'support', isAvailable: () => Effect.succeed(true), uploadLogs, sessionContext, flushLogs }],
367
+ apis: [
368
+ {
369
+ kind: 'support',
370
+ isAvailable: () => Effect.succeed(true),
371
+ uploadLogs,
372
+ uploadNdjson,
373
+ sessionContext,
374
+ flushLogs,
375
+ },
376
+ ],
367
377
  });
368
378
  const obs = yield* Function.pipe(
369
379
  Observability.make(),
@@ -371,6 +381,10 @@ describe('Observability', () => {
371
381
  Observability.initialize,
372
382
  );
373
383
  expect(yield* Effect.promise(() => obs.support.uploadLogs())).toBe('logs/1.ndjson');
384
+ expect(yield* Effect.promise(() => obs.support.uploadNdjson('{}', 'trajectory'))).toBe(
385
+ 'trajectories/1.ndjson.gz',
386
+ );
387
+ expect(uploadNdjson).toHaveBeenCalledWith('{}', 'trajectory');
374
388
  expect(obs.support.sessionContext()).toEqual({ distinctId: 'd', widgetSessionId: 'w' });
375
389
  yield* Effect.promise(() => obs.support.flushLogs({ ticketId: 'ticket-1' }));
376
390
  expect(flushLogs).toHaveBeenCalledWith({ ticketId: 'ticket-1' });
@@ -277,6 +277,13 @@ class ObservabilityImpl implements Observability {
277
277
  }
278
278
  return key;
279
279
  },
280
+ uploadNdjson: async (ndjson, kind) => {
281
+ let key: string | undefined;
282
+ for (const extension of this._getExtensions('support')) {
283
+ key = (await extension.uploadNdjson(ndjson, kind)) ?? key;
284
+ }
285
+ return key;
286
+ },
280
287
  sessionContext: () => {
281
288
  for (const extension of this._getExtensions('support')) {
282
289
  const context = extension.sessionContext();
@@ -184,8 +184,13 @@ export type Mcp = {
184
184
  */
185
185
  export type SupportLogTags = { ticketId: string } | { reportId: string };
186
186
 
187
+ /** What an uploaded NDJSON bundle is, which decides where it is filed in storage. */
188
+ export type SupportUploadKind = 'logs' | 'trajectory';
189
+
187
190
  export type Support = {
188
191
  uploadLogs(): Promise<string | undefined>;
192
+ /** Uploads an NDJSON bundle the caller built (e.g. an agent trajectory), returning its storage key. */
193
+ uploadNdjson(ndjson: string, kind: SupportUploadKind): Promise<string | undefined>;
189
194
  sessionContext(): SupportSessionContext | undefined;
190
195
  flushLogs(attributes: SupportLogTags): Promise<void>;
191
196
  };
@@ -46,6 +46,12 @@ describe('TailSampler', () => {
46
46
  expect(new TailSampler().keep(span({ attributes: { 'dxos.ai.kind': 'tool' } }))).toEqual(true);
47
47
  });
48
48
 
49
+ test('keeps a span marked to keep, under either attribute namespace', ({ expect }) => {
50
+ expect(new TailSampler().keep(span({ attributes: { 'dxos.sampling.keep': true } }))).toEqual(true);
51
+ expect(new TailSampler().keep(span({ attributes: { 'ctx.dxos.sampling.keep': true } }))).toEqual(true);
52
+ expect(new TailSampler().keep(span({ attributes: { 'ctx.dxos.sampling.keep': false } }))).toEqual(false);
53
+ });
54
+
49
55
  test('keeps a span slower than the threshold', ({ expect }) => {
50
56
  expect(new TailSampler().keep(span({ durationMs: DEFAULT_SLOW_MS + 1 }))).toEqual(true);
51
57
  });
@@ -30,6 +30,8 @@ import * as SpanAttributes from '@dxos/effect/SpanAttributes';
30
30
  * In order:
31
31
  * - a span that **errored** is kept, and its trace is promoted;
32
32
  * - a span slower than {@link DEFAULT_SLOW_MS} is kept, and its trace is promoted;
33
+ * - a span marked `dxos.sampling.keep` — a rare flow such as an invitation, which a dashboard counts
34
+ * one by one and a 30% sample would mostly hide — is kept, and its trace is promoted;
33
35
  * - a span carrying `gen_ai.*` or `dxos.ai.kind` — a model call, or the turn and tool calls
34
36
  * around it that the AI analytics sink reports — is kept, and its trace is promoted, so a model call is never a
35
37
  * fraction of the calls that happened. This one is ours rather than canonical: the AI events are
@@ -109,6 +111,7 @@ export class TailSampler {
109
111
  return (
110
112
  span.status.code === SpanStatusCode.ERROR ||
111
113
  span.durationMs > this._slowMs ||
114
+ isKeepMarked(span.attributes) ||
112
115
  Object.keys(span.attributes).some(isAiAttribute)
113
116
  );
114
117
  }
@@ -136,6 +139,10 @@ const GEN_AI_PREFIX = 'gen_ai.';
136
139
  /** `gen_ai.*` marks a model call; `dxos.ai.kind` the turn around it and the tool calls inside it. */
137
140
  const isAiAttribute = (key: string): boolean => key.startsWith(GEN_AI_PREFIX) || key === SpanAttributes.AI.kind;
138
141
 
142
+ /** Manual `@dxos/tracing` spans namespace their attributes under `ctx.`, so the marker arrives either way. */
143
+ const isKeepMarked = (attributes: Attributes): boolean =>
144
+ attributes[SpanAttributes.SAMPLING.keep] === true || attributes[`ctx.${SpanAttributes.SAMPLING.keep}`] === true;
145
+
139
146
  /**
140
147
  * Deterministic per trace, so every span of a trace decides the same way. Reads the low 8 hex digits
141
148
  * of the 32-hex-digit trace id — the id is random, so any window is uniform, and 32 bits is well
@@ -84,9 +84,15 @@ export type NodeOptions = {
84
84
  const DEFAULT_FEEDBACK_LOGS_ENDPOINT = '/api/feedback-logs';
85
85
 
86
86
  /** Upload serialized logs, gzipped, to the feedback-logs endpoint. Returns the R2 key on success. */
87
- const uploadLogs = async (endpoint: string, ndjson: string): Promise<string | undefined> => {
87
+ const uploadLogs = async (
88
+ endpoint: string,
89
+ ndjson: string,
90
+ kind: ObservabilityExtension.SupportUploadKind = 'logs',
91
+ ): Promise<string | undefined> => {
88
92
  try {
89
- const response = await fetch(endpoint, {
93
+ // The endpoint may be relative (same-origin) or absolute (native builds), so append rather than parse.
94
+ const url = kind === 'logs' ? endpoint : `${endpoint}${endpoint.includes('?') ? '&' : '?'}kind=${kind}`;
95
+ const response = await fetch(url, {
90
96
  method: 'POST',
91
97
  headers: { 'Content-Type': 'application/gzip' },
92
98
  body: await gzip(ndjson),
@@ -225,6 +231,8 @@ export const extensions: (options: ExtensionsOptions) => Effect.Effect<Observabi
225
231
  }
226
232
  return await uploadLogs(feedbackLogsEndpoint, ndjson);
227
233
  },
234
+ uploadNdjson: async (ndjson, kind) =>
235
+ ndjson.length === 0 ? undefined : await uploadLogs(feedbackLogsEndpoint, ndjson, kind),
228
236
  sessionContext: () => {
229
237
  if (!posthog.__loaded) {
230
238
  return undefined;
package/src/version.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
- export const DXOS_VERSION = '0.13.0';
5
+ export const DXOS_VERSION = '0.14.0';