@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.
- package/dist/lib/Observability.mjs +7 -2
- package/dist/lib/Observability.mjs.map +1 -1
- package/dist/lib/chunk-ObservabilityExtension.mjs +12 -10
- package/dist/lib/chunk-ObservabilityExtension.mjs.map +1 -1
- package/dist/lib/chunk-OtelSpanSink.mjs +5 -1
- package/dist/lib/chunk-OtelSpanSink.mjs.map +1 -1
- package/dist/types/src/Observability.d.ts.map +1 -1
- package/dist/types/src/Observability.js +7 -0
- package/dist/types/src/Observability.js.map +1 -1
- package/dist/types/src/Observability.test.js +13 -1
- package/dist/types/src/Observability.test.js.map +1 -1
- package/dist/types/src/ObservabilityExtension.d.ts +4 -0
- package/dist/types/src/ObservabilityExtension.d.ts.map +1 -1
- package/dist/types/src/extensions/otel/tail-sampling.d.ts +2 -0
- package/dist/types/src/extensions/otel/tail-sampling.d.ts.map +1 -1
- package/dist/types/src/extensions/otel/tail-sampling.js +5 -0
- package/dist/types/src/extensions/otel/tail-sampling.js.map +1 -1
- package/dist/types/src/extensions/otel/tail-sampling.test.js +5 -0
- package/dist/types/src/extensions/otel/tail-sampling.test.js.map +1 -1
- package/dist/types/src/extensions/posthog/extension.d.ts.map +1 -1
- package/dist/types/src/extensions/posthog/extension.js +5 -2
- package/dist/types/src/extensions/posthog/extension.js.map +1 -1
- package/dist/types/src/version.d.ts +1 -1
- package/dist/types/src/version.js +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +19 -19
- package/src/Observability.test.ts +15 -1
- package/src/Observability.ts +7 -0
- package/src/ObservabilityExtension.ts +5 -0
- package/src/extensions/otel/tail-sampling.test.ts +6 -0
- package/src/extensions/otel/tail-sampling.ts +7 -0
- package/src/extensions/posthog/extension.ts +10 -2
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/observability",
|
|
3
|
-
"version": "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.
|
|
113
|
-
"@dxos/client": "0.
|
|
114
|
-
"@dxos/client-protocol": "0.
|
|
115
|
-
"@dxos/config": "0.
|
|
116
|
-
"@dxos/context": "0.
|
|
117
|
-
"@dxos/echo": "0.
|
|
118
|
-
"@dxos/effect": "0.
|
|
119
|
-
"@dxos/invariant": "0.
|
|
120
|
-
"@dxos/log": "0.
|
|
121
|
-
"@dxos/log-store-idb": "0.
|
|
122
|
-
"@dxos/node-std": "0.
|
|
123
|
-
"@dxos/protocols": "0.
|
|
124
|
-
"@dxos/tracing": "0.
|
|
125
|
-
"@dxos/util": "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.
|
|
130
|
-
"@dxos/client-services": "0.
|
|
131
|
-
"@dxos/errors": "0.
|
|
132
|
-
"@dxos/network-manager": "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: [
|
|
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' });
|
package/src/Observability.ts
CHANGED
|
@@ -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 (
|
|
87
|
+
const uploadLogs = async (
|
|
88
|
+
endpoint: string,
|
|
89
|
+
ndjson: string,
|
|
90
|
+
kind: ObservabilityExtension.SupportUploadKind = 'logs',
|
|
91
|
+
): Promise<string | undefined> => {
|
|
88
92
|
try {
|
|
89
|
-
|
|
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