@dxos/observability 0.14.0 → 0.16.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 +1 -248
- package/dist/lib/ai/AiObservability.mjs +1 -238
- package/dist/lib/chunk-AiObservability.mjs +238 -0
- package/dist/lib/chunk-AiObservability.mjs.map +1 -0
- package/dist/lib/chunk-Observability.mjs +248 -0
- package/dist/lib/chunk-Observability.mjs.map +1 -0
- package/dist/lib/chunk-ObservabilityClientProvider.mjs.map +1 -1
- package/dist/lib/chunk-ObservabilityExtension.mjs +38 -11
- package/dist/lib/chunk-ObservabilityExtension.mjs.map +1 -1
- package/dist/lib/chunk-ObservabilityProvider.mjs.map +1 -1
- package/dist/lib/chunk-OtelLogSink.mjs +105 -0
- package/dist/lib/{extensions/otel/OtelLogSink.mjs.map → chunk-OtelLogSink.mjs.map} +1 -1
- package/dist/lib/chunk-OtelMetricsSink.mjs +49 -0
- package/dist/lib/{extensions/otel/OtelMetricsSink.mjs.map → chunk-OtelMetricsSink.mjs.map} +1 -1
- package/dist/lib/chunk-OtelSpanSink.mjs.map +1 -1
- package/dist/lib/chunk-extensions.mjs.map +1 -1
- package/dist/lib/chunk-logs.mjs.map +1 -1
- package/dist/lib/chunk-support-logs.mjs +1 -1
- package/dist/lib/chunk-support-logs.mjs.map +1 -1
- package/dist/lib/extensions/otel/OtelLogSink.mjs +1 -105
- package/dist/lib/extensions/otel/OtelMetricsSink.mjs +1 -49
- package/dist/lib/extensions/otel/OtelSpanSink.mjs +1 -2
- package/dist/lib/index.mjs +4 -4
- package/dist/lib/posthog-transport.node.mjs.map +1 -1
- package/dist/lib/providers/ObservabilityClientProvider.mjs +1 -2
- package/dist/lib/storage.node.mjs.map +1 -1
- package/dist/lib/traces.mjs.map +1 -1
- package/dist/lib/traces.node.mjs.map +1 -1
- package/dist/types/src/ObservabilityExtension.d.ts +1 -1
- package/dist/types/src/ObservabilityExtension.d.ts.map +1 -1
- package/dist/types/src/ai/AiObservability.js +2 -2
- package/dist/types/src/ai/AiObservability.js.map +1 -1
- package/dist/types/src/ai/AiObservability.test.js +11 -11
- package/dist/types/src/ai/AiObservability.test.js.map +1 -1
- package/dist/types/src/extensions/otel/tail-sampling.test.js +1 -1
- package/dist/types/src/extensions/otel/tail-sampling.test.js.map +1 -1
- package/dist/types/src/extensions/posthog/before-send.d.ts +4 -0
- package/dist/types/src/extensions/posthog/before-send.d.ts.map +1 -0
- package/dist/types/src/extensions/posthog/before-send.js +19 -0
- package/dist/types/src/extensions/posthog/before-send.js.map +1 -0
- package/dist/types/src/extensions/posthog/before-send.test.d.ts +2 -0
- package/dist/types/src/extensions/posthog/before-send.test.d.ts.map +1 -0
- package/dist/types/src/extensions/posthog/before-send.test.js +30 -0
- package/dist/types/src/extensions/posthog/before-send.test.js.map +1 -0
- package/dist/types/src/extensions/posthog/extension.d.ts +10 -0
- package/dist/types/src/extensions/posthog/extension.d.ts.map +1 -1
- package/dist/types/src/extensions/posthog/extension.js +22 -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 +20 -20
- package/src/ObservabilityExtension.ts +1 -1
- package/src/ai/AiObservability.test.ts +12 -11
- package/src/ai/AiObservability.ts +2 -2
- package/src/extensions/otel/tail-sampling.test.ts +1 -1
- package/src/extensions/posthog/before-send.test.ts +36 -0
- package/src/extensions/posthog/before-send.ts +26 -0
- package/src/extensions/posthog/extension.ts +30 -1
- package/src/version.ts +1 -1
- package/dist/lib/Observability.mjs.map +0 -1
- package/dist/lib/ai/AiObservability.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunk-ObservabilityClientProvider.mjs","names":[],"sources":["../../src/providers/sync-episodes.ts","../../src/providers/sync-state.ts","../../src/providers/client-observability.ts","../../src/providers/ObservabilityClientProvider.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n/** A sync episode that just ended, ready to be recorded. */\nexport type ClosedEpisode = {\n durationMs: number;\n /**\n * True when the episode was already in flight at the first observation, so its start is a lower\n * bound. Reported anyway — it under-states duration, which the stall gauge covers.\n */\n truncated: boolean;\n};\n\n/**\n * Folds observations of the outstanding sync backlog into episode durations and stall age.\n *\n * Pure over `(now, pendingWorkCount)` so both instruments are testable without a client or a clock.\n */\nexport class SyncEpisodeTracker {\n #openedAt: number | undefined;\n #lastProgressAt = 0;\n #lowWaterMark = 0;\n #truncated = false;\n #sawCaughtUp = false;\n\n get isOpen(): boolean {\n return this.#openedAt !== undefined;\n }\n\n /**\n * Feeds one observation, returning the episode this observation closed, if any.\n * Progress is a decrease in the backlog's low-water mark rather than any emission, so a client\n * re-reporting the same backlog does not look healthy and concurrent local writes raising the\n * count do not reset the stall clock.\n */\n observe(now: number, pendingWorkCount: number): ClosedEpisode | undefined {\n if (pendingWorkCount <= 0) {\n this.#sawCaughtUp = true;\n if (this.#openedAt === undefined) {\n return undefined;\n }\n\n const closed: ClosedEpisode = { durationMs: now - this.#openedAt, truncated: this.#truncated };\n this.#openedAt = undefined;\n this.#truncated = false;\n return closed;\n }\n\n if (this.#openedAt === undefined) {\n this.#openedAt = now;\n this.#lastProgressAt = now;\n this.#lowWaterMark = pendingWorkCount;\n // Nothing observed the 0 -> non-zero transition, so the backlog may predate this process.\n this.#truncated = !this.#sawCaughtUp;\n return undefined;\n }\n\n if (pendingWorkCount < this.#lowWaterMark) {\n this.#lowWaterMark = pendingWorkCount;\n this.#lastProgressAt = now;\n }\n\n return undefined;\n }\n\n /**\n * Age of the last observed progress while an episode is open; `0` when caught up.\n * This is the stuck-detector: an episode that never closes never records a duration, so a\n * permanently stuck client is invisible in the duration histogram by construction.\n */\n stalledForMs(now: number): number {\n return this.#openedAt === undefined ? 0 : Math.max(0, now - this.#lastProgressAt);\n }\n}\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type CleanupFn } from '@dxos/async';\nimport { type Client } from '@dxos/client';\nimport { type Space } from '@dxos/client/echo';\nimport { type Context } from '@dxos/context';\nimport { type Database } from '@dxos/echo';\nimport { log } from '@dxos/log';\n\n/** Sync backlog folded across every space. Feed blocks arrive as strings and are summed numerically. */\nexport type SyncSummary = {\n localDocumentCount: number;\n remoteDocumentCount: number;\n unsyncedDocumentCount: number;\n blocksToPull: number;\n blocksToPush: number;\n /**\n * Total outstanding work, documents and feed blocks together.\n * A client with a stalled feed backlog is not synced even when its documents converged, so this\n * (not `unsyncedDocumentCount`) is what \"is this client caught up\" means.\n */\n pendingWorkCount: number;\n};\n\nconst EMPTY: SyncSummary = {\n localDocumentCount: 0,\n remoteDocumentCount: 0,\n unsyncedDocumentCount: 0,\n blocksToPull: 0,\n blocksToPush: 0,\n pendingWorkCount: 0,\n};\n\n/**\n * Holds the latest sync state per space.\n * Separate from the client subscription so the add/remove/fold behaviour is testable on its own.\n */\nexport class SyncStateTracker {\n readonly #states = new Map<string, Database.SyncState>();\n readonly #cleanups = new Map<string, CleanupFn>();\n readonly #onChange: ((summary: SyncSummary) => void) | undefined;\n\n constructor(onChange?: (summary: SyncSummary) => void) {\n this.#onChange = onChange;\n }\n\n get trackedIds(): string[] {\n return [...this.#cleanups.keys()];\n }\n\n isTracked(spaceId: string): boolean {\n return this.#cleanups.has(spaceId);\n }\n\n /** Registers a space, ignoring one already tracked so a repeated update does not double-subscribe. */\n add(spaceId: string, subscribe: (onState: (state: Database.SyncState) => void) => CleanupFn): void {\n if (this.#cleanups.has(spaceId)) {\n return;\n }\n\n this.#cleanups.set(\n spaceId,\n subscribe((state) => {\n this.#states.set(spaceId, state);\n this.#onChange?.(this.summary());\n }),\n );\n }\n\n remove(spaceId: string): void {\n this.#cleanups.get(spaceId)?.();\n this.#cleanups.delete(spaceId);\n this.#states.delete(spaceId);\n }\n\n /** Drops every space absent from `spaceIds`, whose last backlog would otherwise persist in the summary. */\n retainOnly(spaceIds: Iterable<string>): void {\n const present = new Set(spaceIds);\n for (const spaceId of this.trackedIds) {\n if (!present.has(spaceId)) {\n this.remove(spaceId);\n }\n }\n }\n\n clear(): void {\n for (const spaceId of this.trackedIds) {\n this.remove(spaceId);\n }\n }\n\n summary(): SyncSummary {\n return foldSyncStates(this.#states.values());\n }\n}\n\n/**\n * Tracks the sync backlog of every space the client knows about.\n *\n * Reads `db.subscribeToSyncState`, which already selects the EDGE peer and owns a no-change poll\n * backoff, so this adds no timer of its own.\n */\nexport const subscribeSyncSummary = (\n client: Client,\n ctx: Context,\n onChange?: (summary: SyncSummary) => void,\n): { summary: () => SyncSummary } => {\n const tracker = new SyncStateTracker(onChange);\n\n const reconcile = (spaces: Space[]) => {\n tracker.retainOnly(spaces.map((space) => space.id));\n for (const space of spaces) {\n if (tracker.isTracked(space.id)) {\n continue;\n }\n\n try {\n tracker.add(space.id, (onState) => space.db.subscribeToSyncState(onState));\n } catch (err) {\n // A space that is not ready has no database yet; the subscription below re-runs on every\n // update, so it is picked up once it opens.\n log('sync state subscription deferred', { space: space.id, err });\n }\n }\n };\n\n reconcile(client.spaces.get());\n const subscription = client.spaces.subscribe({ next: reconcile });\n\n ctx.onDispose(() => {\n subscription.unsubscribe();\n tracker.clear();\n });\n\n return { summary: () => tracker.summary() };\n};\n\n/** Folds per-space sync state into one device-wide summary. */\nexport const foldSyncStates = (states: Iterable<Database.SyncState>): SyncSummary => {\n const summary = { ...EMPTY };\n for (const state of states) {\n summary.localDocumentCount += state.localDocumentCount;\n summary.remoteDocumentCount += state.remoteDocumentCount;\n summary.unsyncedDocumentCount += state.unsyncedDocumentCount;\n summary.blocksToPull += Number(state.blocksToPull);\n summary.blocksToPush += Number(state.blocksToPush);\n }\n summary.pendingWorkCount = summary.unsyncedDocumentCount + summary.blocksToPull + summary.blocksToPush;\n return summary;\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { anyUnpack } from '@bufbuild/protobuf/wkt';\nimport * as Effect from 'effect/Effect';\n\nimport { Event, scheduleTaskInterval } from '@dxos/async';\nimport { type Client, type ClientServices } from '@dxos/client';\nimport { type Space } from '@dxos/client/echo';\nimport { Context } from '@dxos/context';\nimport { invariant } from '@dxos/invariant';\nimport { log } from '@dxos/log';\nimport { toDate, toPublicKey, toTimeframe } from '@dxos/protocols/buf';\nimport { SpaceState } from '@dxos/protocols/buf/dxos/client/invitation_pb';\nimport { type NetworkStatus, type NetworkStatus_Signal } from '@dxos/protocols/buf/dxos/client/services_pb';\n// Value imports come straight from protocols: reaching them through the `@dxos/client` barrels\n// puts echo-client (and wa-sqlite, automerge-repo with it) in the app's eager boot graph.\nimport { ConnectionState, DeviceKind } from '@dxos/protocols/buf/dxos/client/services_pb';\nimport { type Platform, Platform_PLATFORM_TYPE } from '@dxos/protocols/buf/dxos/client/services_pb';\nimport { EpochSchema } from '@dxos/protocols/buf/dxos/halo/credentials_pb';\n\nimport * as Observability from '../Observability.ts';\nimport { type CrossRealmMemory, measureCrossRealmMemory, readHeap, supportsCrossRealmMemory } from './memory.ts';\nimport { SyncEpisodeTracker } from './sync-episodes.ts';\nimport { subscribeSyncSummary } from './sync-state.ts';\n\nconst SPACE_METRICS_MIN_INTERVAL = 1000 * 60 * 10; // 10 minutes\nconst NETWORK_METRICS_MIN_INTERVAL = 1000 * 60 * 10; // 10 minutes\nconst RUNTIME_METRICS_MIN_INTERVAL = 1000 * 60 * 10; // 10 minutes\n\n/** Under the 60s export interval, so an observed gauge never reports a stale sample. */\nconst MEMORY_SAMPLE_INTERVAL = 1000 * 30;\n\nconst BYTES = { unit: 'By' } as const;\nconst SPACES = { unit: '{space}' } as const;\nconst DOCUMENTS = { unit: '{document}' } as const;\nconst SECONDS = { unit: 's' } as const;\n\n// TODO(wittjosiah): Improve privacy of telemetry identifiers.\n// - Identifier should be generated client-side with no attachment to identity.\n// - Identifier can then be reset by user.\n// - Identifier can be synced via HALO to allow for correlation of events bewteen devices.\n// - Identifier should also be stored outside of HALO such that it is available immediately on startup.\n/** Subscribes to identity and device changes and sets observability tags accordingly. */\nexport const identityProvider = (clientServices: Partial<ClientServices>): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n // TODO(wittjosiah): RPC subscribe returns void; cleanup requires upstream API change.\n clientServices.IdentityService!.queryIdentity().subscribe((idqr) => {\n if (!idqr?.identity?.did) {\n return;\n }\n\n observability.identify(idqr.identity.did);\n observability.setTags({ did: idqr.identity.did });\n });\n\n // TODO(wittjosiah): RPC subscribe returns void; cleanup requires upstream API change.\n clientServices.DevicesService!.queryDevices().subscribe((dqr) => {\n if (!dqr?.devices || dqr.devices.length === 0) {\n return;\n }\n\n const thisDevice = dqr.devices.find((device) => device.kind === DeviceKind.CURRENT);\n if (!thisDevice) {\n return;\n }\n\n observability.setTags({ deviceKey: toPublicKey(thisDevice.deviceKey)?.truncate() ?? '' });\n if (thisDevice.profile?.label) {\n observability.setTags({ deviceProfile: thisDevice.profile.label });\n }\n });\n });\n\n/**\n * What {@link identityManagerProvider} reads: the host-side identity manager, in a realm that has no\n * client proxy to subscribe through.\n */\nexport type IdentitySource = {\n readonly identity: { readonly did: string } | undefined;\n readonly stateUpdate: { on(listener: () => void): unknown };\n};\n\n/**\n * Tags every span and log of a realm with the identity, read from the services host itself. For the\n * dedicated worker, whose tracer and tags are its own: the tab's {@link identityProvider} only tags\n * the tab. Tags only — the tab already identifies the user with the analytics backend.\n */\nexport const identityManagerProvider = (identityManager: IdentitySource): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const apply = () => {\n const did = identityManager.identity?.did;\n if (did) {\n observability.setTags({ did });\n }\n };\n identityManager.stateUpdate.on(apply);\n apply();\n });\n\n/** Periodically publishes network connection and buffer metrics. */\nexport const networkMetricsProvider = (clientServices: Partial<ClientServices>): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n let lastNetworkStatus: NetworkStatus | undefined;\n\n // TODO(nf): support type in debounce()\n const updateSignalMetrics = new Event<NetworkStatus>().debounce(NETWORK_METRICS_MIN_INTERVAL);\n updateSignalMetrics.on(ctx, async () => {\n log('send signal metrics');\n (lastNetworkStatus?.signaling as NetworkStatus_Signal[])?.forEach(({ server, state }) => {\n observability.metrics.gauge('dxos.client.network.signal.connectionState', state, { server });\n });\n\n let swarmCount = 0;\n const connectionStates = new Map<string, number>();\n for (const state in ConnectionState) {\n connectionStates.set(state, 0);\n }\n\n let totalReadBufferSize = 0;\n let totalWriteBufferSize = 0;\n let totalChannelBufferSize = 0;\n\n lastNetworkStatus?.connectionInfo?.forEach((connectionInfo) => {\n swarmCount++;\n\n for (const conn of connectionInfo.connections ?? []) {\n connectionStates.set(conn.state, (connectionStates.get(conn.state) ?? 0) + 1);\n totalReadBufferSize += conn.readBufferSize ?? 0;\n totalWriteBufferSize += conn.writeBufferSize ?? 0;\n for (const stream of conn.streams ?? []) {\n totalChannelBufferSize += stream.writeBufferSize ?? 0;\n }\n }\n\n observability.metrics.gauge('dxos.client.network.swarm.count', swarmCount);\n for (const state in ConnectionState) {\n observability.metrics.gauge('dxos.client.network.connection.count', connectionStates.get(state) ?? 0, {\n state,\n });\n }\n observability.metrics.gauge('dxos.client.network.totalReadBufferSize', totalReadBufferSize);\n observability.metrics.gauge('dxos.client.network.totalWriteBufferSize', totalWriteBufferSize);\n observability.metrics.gauge('dxos.client.network.totalChannelBufferSize', totalChannelBufferSize);\n });\n });\n\n clientServices.NetworkService!.queryStatus().subscribe((networkStatus) => {\n lastNetworkStatus = networkStatus;\n updateSignalMetrics.emit();\n });\n\n scheduleTaskInterval(ctx, async () => updateSignalMetrics.emit(), NETWORK_METRICS_MIN_INTERVAL);\n\n return async () => {\n await ctx.dispose();\n };\n });\n\n/** Periodically publishes platform and heap memory metrics. */\nexport const runtimeMetricsProvider = (clientServices: Partial<ClientServices>): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n log('runtimeMetricsProvider: requesting platform from SystemService');\n const platform = yield* Effect.promise(() => clientServices.SystemService!.getPlatform());\n log('runtimeMetricsProvider: platform received');\n invariant(platform, 'platform is required');\n\n observability.setTags({\n platformType: Platform_PLATFORM_TYPE[platform.type].toLowerCase(),\n platform: platform.platform,\n arch: platform.arch,\n runtime: platform.runtime,\n });\n\n // Heap is a synchronous read, so the gauge reads it directly at collection time.\n const heapGauges = [\n ['dxos.client.runtime.heapUsed', () => readHeap().used],\n ['dxos.client.runtime.heapTotal', () => readHeap().total],\n ['dxos.client.runtime.heapSizeLimit', () => readHeap().limit],\n ] as const;\n for (const [name, read] of heapGauges) {\n ctx.onDispose(observability.metrics.observe(name, read, undefined, BYTES));\n }\n\n // The platform reading is an RPC and cross-realm memory waits for a GC, so both are sampled on\n // their own cadence and the gauges read the latest sample.\n let platformMemory: Platform['memory'];\n\n // The platform reports memory as a `google.protobuf.Struct`, so a gauge reading a byte count\n // has to check the JSON value rather than assume it is numeric.\n const memoryNumber = (key: string): number | undefined => {\n const value = platformMemory?.[key];\n return typeof value === 'number' ? value : undefined;\n };\n let crossRealmMemory: CrossRealmMemory | undefined;\n\n const servicesGauges = [\n ['dxos.client.services.runtime.heapUsed', () => memoryNumber('heapUsed')],\n ['dxos.client.services.runtime.heapTotal', () => memoryNumber('heapTotal')],\n ['dxos.client.services.runtime.rss', () => memoryNumber('rss')],\n ] as const;\n for (const [name, read] of servicesGauges) {\n ctx.onDispose(observability.metrics.observe(name, read, undefined, BYTES));\n }\n\n if (supportsCrossRealmMemory()) {\n for (const scope of ['window', 'shared-worker', 'dedicated-worker', 'other'] as const) {\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.client.runtime.memory.bytes',\n () => crossRealmMemory?.[scope],\n { scope },\n BYTES,\n ),\n );\n }\n }\n\n const sample = async () => {\n try {\n const platform = await clientServices.SystemService?.getPlatform();\n platformMemory = platform?.memory;\n } catch (error) {\n log('platform error', { error });\n }\n\n try {\n crossRealmMemory = await measureCrossRealmMemory();\n } catch (error) {\n // Not cross-origin isolated, or a measurement is already in flight; neither is retryable here.\n log('cross-realm memory unavailable', { error });\n }\n };\n\n scheduleTaskInterval(ctx, sample, MEMORY_SAMPLE_INTERVAL);\n void sample();\n\n return async () => {\n await ctx.dispose();\n };\n });\n\n/** Periodically publishes space membership, object count, and pipeline progress metrics. */\nexport const spacesMetricsProvider = (client: Client): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n // Pipeline subscriptions only; the gauges below read the live space list at collection time.\n const spaces = client.spaces.get();\n const subscriptions = new Map<string, { unsubscribe: () => void }>();\n ctx.onDispose(() => subscriptions.forEach((subscription) => subscription.unsubscribe()));\n\n // Read at collection time. The gap between the two is the \"known but not opened\" signal.\n ctx.onDispose(\n observability.metrics.observe('dxos.client.spaces.count', () => client.spaces.get().length, undefined, SPACES),\n );\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.client.spaces.ready.count',\n () => client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY).length,\n undefined,\n SPACES,\n ),\n );\n\n const updateSpaceMetrics = new Event<Space>().debounce(SPACE_METRICS_MIN_INTERVAL);\n updateSpaceMetrics.on(ctx, async () => {\n log('send space metrics');\n // Reported as device-wide totals rather than per space: the previous `key` attribute cost one\n // series per space per device, unbounded in the number of spaces a user creates.\n const mapped = mapSpaces(client.spaces.get(), { truncateKeys: true });\n const total = (pick: (data: (typeof mapped)[number]) => number | undefined) =>\n mapped.reduce((sum, data) => sum + (pick(data) ?? 0), 0);\n\n observability.metrics.gauge(\n 'dxos.client.space.members',\n total((data) => data.members),\n );\n observability.metrics.gauge(\n 'dxos.client.space.objects',\n total((data) => data.objects),\n );\n observability.metrics.gauge(\n 'dxos.client.space.currentDataMutations',\n total((data) => data.currentDataMutations),\n );\n // Max, not a sum: epochs are per-space sequence numbers, so adding them means nothing.\n observability.metrics.gauge(\n 'dxos.client.space.epoch',\n mapped.reduce((max, data) => Math.max(max, data.epoch ?? 0), 0),\n );\n });\n\n const subscribeToSpaceUpdate = (space: Space) =>\n space.pipeline.subscribe({\n next: () => {\n updateSpaceMetrics.emit();\n },\n });\n\n spaces.forEach((space) => {\n subscriptions.set(space.id, subscribeToSpaceUpdate(space));\n });\n\n client.spaces.subscribe({\n next: async (spaces) => {\n spaces\n .filter((space) => !subscriptions.has(space.id))\n .forEach((space) => {\n subscriptions.set(space.id, subscribeToSpaceUpdate(space));\n });\n },\n });\n\n scheduleTaskInterval(ctx, async () => updateSpaceMetrics.emit(), SPACE_METRICS_MIN_INTERVAL);\n\n return async () => {\n await ctx.dispose();\n };\n });\n\n/** Publishes the document backlog folded across every space. */\nexport const documentsMetricsProvider = (client: Client): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n const { summary } = subscribeSyncSummary(client, ctx);\n\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.echo.documents.count',\n () => summary().localDocumentCount,\n { location: 'local' },\n DOCUMENTS,\n ),\n );\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.echo.documents.count',\n () => summary().remoteDocumentCount,\n { location: 'remote' },\n DOCUMENTS,\n ),\n );\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.echo.documents.unsynced.count',\n () => summary().unsyncedDocumentCount,\n undefined,\n DOCUMENTS,\n ),\n );\n\n return async () => {\n await ctx.dispose();\n };\n });\n\n/**\n * Publishes how long a client takes to sync, and how long it has been stuck.\n *\n * Both are needed. `episode.duration` records only when a backlog clears, so a client that never\n * finishes syncing contributes nothing to it — `stalled.duration` is what makes that client visible.\n */\nexport const syncMetricsProvider = (client: Client): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n const episodes = new SyncEpisodeTracker();\n let pending = 0;\n\n // Fed on every sync-state emission rather than at collection time: an episode that opens and\n // closes inside one 60s export window would otherwise never be seen at all.\n subscribeSyncSummary(client, ctx, (summary) => {\n pending = summary.pendingWorkCount;\n const closed = episodes.observe(Date.now(), summary.pendingWorkCount);\n if (closed) {\n log('sync episode closed', { durationMs: closed.durationMs, truncated: closed.truncated });\n observability.metrics.distribution(\n 'dxos.echo.sync.episode.duration',\n closed.durationMs / 1_000,\n undefined,\n SECONDS,\n );\n }\n });\n\n ctx.onDispose(\n observability.metrics.observe('dxos.echo.sync.pending.count', () => pending, undefined, { unit: '{item}' }),\n );\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.echo.sync.stalled.duration',\n () => episodes.stalledForMs(Date.now()) / 1_000,\n undefined,\n SECONDS,\n ),\n );\n\n return async () => {\n await ctx.dispose();\n };\n });\n\ntype MapSpacesOptions = {\n verbose?: boolean;\n truncateKeys?: boolean;\n};\n\nconst mapSpaces = (spaces: Space[], options: MapSpacesOptions = { verbose: false, truncateKeys: false }) => {\n return spaces.map((space) => {\n // TODO(burdon): Factor out.\n // TODO(burdon): Agent needs to restart before `ready` is available.\n const { open, ready } = space.internal.data.metrics ?? {};\n const openedAt = toDate(open);\n const readyAt = toDate(ready);\n const startup = openedAt && readyAt && readyAt.getTime() - openedAt.getTime();\n\n // TODO(burdon): Get feeds from client-services if verbose (factor out from devtools/diagnostics).\n // const host = client.services.services.DevtoolsHost!;\n const pipeline = space.internal.data.pipeline;\n const assertion = pipeline?.currentEpoch?.subject?.assertion;\n const currentEpoch = assertion && anyUnpack(assertion, EpochSchema);\n const startDataMutations = toTimeframe(currentEpoch?.timeframe).totalMessages();\n const epoch = currentEpoch?.number;\n const currentDataMutations = toTimeframe(pipeline?.currentDataTimeframe).totalMessages();\n const totalDataMutations = toTimeframe(pipeline?.targetDataTimeframe).totalMessages();\n\n return {\n // TODO(nf): truncate keys for DD?\n key: space.key.truncate(),\n open: space.isOpen,\n members: space.members.get().length,\n objects: space.internal.db.getAllObjectIds().length,\n startup,\n epoch,\n // appliedEpoch,\n startDataMutations,\n currentDataMutations,\n totalDataMutations,\n\n // TODO(burdon): Negative?\n progress: (\n Math.min(Math.abs((currentDataMutations - startDataMutations) / (totalDataMutations - startDataMutations)), 1) *\n 100\n ).toFixed(0),\n };\n });\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\n// Kept out of `ObservabilityProvider` because these reach `@dxos/client` and `@dxos/protocols`.\n// The boot set is the parse graph, so a namespace re-export puts them in the eager graph of every\n// consumer of that namespace even where treeshaking would later drop them.\nexport * as Client from './client-observability.ts';\nexport * as SyncState from './sync-state.ts';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chunk-ObservabilityClientProvider.mjs","names":["__dxlog_file"],"sources":["../../src/providers/sync-episodes.ts","../../src/providers/sync-state.ts","../../src/providers/client-observability.ts","../../src/providers/ObservabilityClientProvider.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\n/** A sync episode that just ended, ready to be recorded. */\nexport type ClosedEpisode = {\n durationMs: number;\n /**\n * True when the episode was already in flight at the first observation, so its start is a lower\n * bound. Reported anyway — it under-states duration, which the stall gauge covers.\n */\n truncated: boolean;\n};\n\n/**\n * Folds observations of the outstanding sync backlog into episode durations and stall age.\n *\n * Pure over `(now, pendingWorkCount)` so both instruments are testable without a client or a clock.\n */\nexport class SyncEpisodeTracker {\n #openedAt: number | undefined;\n #lastProgressAt = 0;\n #lowWaterMark = 0;\n #truncated = false;\n #sawCaughtUp = false;\n\n get isOpen(): boolean {\n return this.#openedAt !== undefined;\n }\n\n /**\n * Feeds one observation, returning the episode this observation closed, if any.\n * Progress is a decrease in the backlog's low-water mark rather than any emission, so a client\n * re-reporting the same backlog does not look healthy and concurrent local writes raising the\n * count do not reset the stall clock.\n */\n observe(now: number, pendingWorkCount: number): ClosedEpisode | undefined {\n if (pendingWorkCount <= 0) {\n this.#sawCaughtUp = true;\n if (this.#openedAt === undefined) {\n return undefined;\n }\n\n const closed: ClosedEpisode = { durationMs: now - this.#openedAt, truncated: this.#truncated };\n this.#openedAt = undefined;\n this.#truncated = false;\n return closed;\n }\n\n if (this.#openedAt === undefined) {\n this.#openedAt = now;\n this.#lastProgressAt = now;\n this.#lowWaterMark = pendingWorkCount;\n // Nothing observed the 0 -> non-zero transition, so the backlog may predate this process.\n this.#truncated = !this.#sawCaughtUp;\n return undefined;\n }\n\n if (pendingWorkCount < this.#lowWaterMark) {\n this.#lowWaterMark = pendingWorkCount;\n this.#lastProgressAt = now;\n }\n\n return undefined;\n }\n\n /**\n * Age of the last observed progress while an episode is open; `0` when caught up.\n * This is the stuck-detector: an episode that never closes never records a duration, so a\n * permanently stuck client is invisible in the duration histogram by construction.\n */\n stalledForMs(now: number): number {\n return this.#openedAt === undefined ? 0 : Math.max(0, now - this.#lastProgressAt);\n }\n}\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type CleanupFn } from '@dxos/async';\nimport { type Client } from '@dxos/client';\nimport { type Space } from '@dxos/client/echo';\nimport { type Context } from '@dxos/context';\nimport { type Database } from '@dxos/echo';\nimport { log } from '@dxos/log';\n\n/** Sync backlog folded across every space. Feed blocks arrive as strings and are summed numerically. */\nexport type SyncSummary = {\n localDocumentCount: number;\n remoteDocumentCount: number;\n unsyncedDocumentCount: number;\n blocksToPull: number;\n blocksToPush: number;\n /**\n * Total outstanding work, documents and feed blocks together.\n * A client with a stalled feed backlog is not synced even when its documents converged, so this\n * (not `unsyncedDocumentCount`) is what \"is this client caught up\" means.\n */\n pendingWorkCount: number;\n};\n\nconst EMPTY: SyncSummary = {\n localDocumentCount: 0,\n remoteDocumentCount: 0,\n unsyncedDocumentCount: 0,\n blocksToPull: 0,\n blocksToPush: 0,\n pendingWorkCount: 0,\n};\n\n/**\n * Holds the latest sync state per space.\n * Separate from the client subscription so the add/remove/fold behaviour is testable on its own.\n */\nexport class SyncStateTracker {\n readonly #states = new Map<string, Database.SyncState>();\n readonly #cleanups = new Map<string, CleanupFn>();\n readonly #onChange: ((summary: SyncSummary) => void) | undefined;\n\n constructor(onChange?: (summary: SyncSummary) => void) {\n this.#onChange = onChange;\n }\n\n get trackedIds(): string[] {\n return [...this.#cleanups.keys()];\n }\n\n isTracked(spaceId: string): boolean {\n return this.#cleanups.has(spaceId);\n }\n\n /** Registers a space, ignoring one already tracked so a repeated update does not double-subscribe. */\n add(spaceId: string, subscribe: (onState: (state: Database.SyncState) => void) => CleanupFn): void {\n if (this.#cleanups.has(spaceId)) {\n return;\n }\n\n this.#cleanups.set(\n spaceId,\n subscribe((state) => {\n this.#states.set(spaceId, state);\n this.#onChange?.(this.summary());\n }),\n );\n }\n\n remove(spaceId: string): void {\n this.#cleanups.get(spaceId)?.();\n this.#cleanups.delete(spaceId);\n this.#states.delete(spaceId);\n }\n\n /** Drops every space absent from `spaceIds`, whose last backlog would otherwise persist in the summary. */\n retainOnly(spaceIds: Iterable<string>): void {\n const present = new Set(spaceIds);\n for (const spaceId of this.trackedIds) {\n if (!present.has(spaceId)) {\n this.remove(spaceId);\n }\n }\n }\n\n clear(): void {\n for (const spaceId of this.trackedIds) {\n this.remove(spaceId);\n }\n }\n\n summary(): SyncSummary {\n return foldSyncStates(this.#states.values());\n }\n}\n\n/**\n * Tracks the sync backlog of every space the client knows about.\n *\n * Reads `db.subscribeToSyncState`, which already selects the EDGE peer and owns a no-change poll\n * backoff, so this adds no timer of its own.\n */\nexport const subscribeSyncSummary = (\n client: Client,\n ctx: Context,\n onChange?: (summary: SyncSummary) => void,\n): { summary: () => SyncSummary } => {\n const tracker = new SyncStateTracker(onChange);\n\n const reconcile = (spaces: Space[]) => {\n tracker.retainOnly(spaces.map((space) => space.id));\n for (const space of spaces) {\n if (tracker.isTracked(space.id)) {\n continue;\n }\n\n try {\n tracker.add(space.id, (onState) => space.db.subscribeToSyncState(onState));\n } catch (err) {\n // A space that is not ready has no database yet; the subscription below re-runs on every\n // update, so it is picked up once it opens.\n log('sync state subscription deferred', { space: space.id, err });\n }\n }\n };\n\n reconcile(client.spaces.get());\n const subscription = client.spaces.subscribe({ next: reconcile });\n\n ctx.onDispose(() => {\n subscription.unsubscribe();\n tracker.clear();\n });\n\n return { summary: () => tracker.summary() };\n};\n\n/** Folds per-space sync state into one device-wide summary. */\nexport const foldSyncStates = (states: Iterable<Database.SyncState>): SyncSummary => {\n const summary = { ...EMPTY };\n for (const state of states) {\n summary.localDocumentCount += state.localDocumentCount;\n summary.remoteDocumentCount += state.remoteDocumentCount;\n summary.unsyncedDocumentCount += state.unsyncedDocumentCount;\n summary.blocksToPull += Number(state.blocksToPull);\n summary.blocksToPush += Number(state.blocksToPush);\n }\n summary.pendingWorkCount = summary.unsyncedDocumentCount + summary.blocksToPull + summary.blocksToPush;\n return summary;\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { anyUnpack } from '@bufbuild/protobuf/wkt';\nimport * as Effect from 'effect/Effect';\n\nimport { Event, scheduleTaskInterval } from '@dxos/async';\nimport { type Client, type ClientServices } from '@dxos/client';\nimport { type Space } from '@dxos/client/echo';\nimport { Context } from '@dxos/context';\nimport { invariant } from '@dxos/invariant';\nimport { log } from '@dxos/log';\nimport { toDate, toPublicKey, toTimeframe } from '@dxos/protocols/buf';\nimport { SpaceState } from '@dxos/protocols/buf/dxos/client/invitation_pb';\nimport { type NetworkStatus, type NetworkStatus_Signal } from '@dxos/protocols/buf/dxos/client/services_pb';\n// Value imports come straight from protocols: reaching them through the `@dxos/client` barrels\n// puts echo-client (and wa-sqlite, automerge-repo with it) in the app's eager boot graph.\nimport { ConnectionState, DeviceKind } from '@dxos/protocols/buf/dxos/client/services_pb';\nimport { type Platform, Platform_PLATFORM_TYPE } from '@dxos/protocols/buf/dxos/client/services_pb';\nimport { EpochSchema } from '@dxos/protocols/buf/dxos/halo/credentials_pb';\n\nimport * as Observability from '../Observability.ts';\nimport { type CrossRealmMemory, measureCrossRealmMemory, readHeap, supportsCrossRealmMemory } from './memory.ts';\nimport { SyncEpisodeTracker } from './sync-episodes.ts';\nimport { subscribeSyncSummary } from './sync-state.ts';\n\nconst SPACE_METRICS_MIN_INTERVAL = 1000 * 60 * 10; // 10 minutes\nconst NETWORK_METRICS_MIN_INTERVAL = 1000 * 60 * 10; // 10 minutes\nconst RUNTIME_METRICS_MIN_INTERVAL = 1000 * 60 * 10; // 10 minutes\n\n/** Under the 60s export interval, so an observed gauge never reports a stale sample. */\nconst MEMORY_SAMPLE_INTERVAL = 1000 * 30;\n\nconst BYTES = { unit: 'By' } as const;\nconst SPACES = { unit: '{space}' } as const;\nconst DOCUMENTS = { unit: '{document}' } as const;\nconst SECONDS = { unit: 's' } as const;\n\n// TODO(wittjosiah): Improve privacy of telemetry identifiers.\n// - Identifier should be generated client-side with no attachment to identity.\n// - Identifier can then be reset by user.\n// - Identifier can be synced via HALO to allow for correlation of events bewteen devices.\n// - Identifier should also be stored outside of HALO such that it is available immediately on startup.\n/** Subscribes to identity and device changes and sets observability tags accordingly. */\nexport const identityProvider = (clientServices: Partial<ClientServices>): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n // TODO(wittjosiah): RPC subscribe returns void; cleanup requires upstream API change.\n clientServices.IdentityService!.queryIdentity().subscribe((idqr) => {\n if (!idqr?.identity?.did) {\n return;\n }\n\n observability.identify(idqr.identity.did);\n observability.setTags({ did: idqr.identity.did });\n });\n\n // TODO(wittjosiah): RPC subscribe returns void; cleanup requires upstream API change.\n clientServices.DevicesService!.queryDevices().subscribe((dqr) => {\n if (!dqr?.devices || dqr.devices.length === 0) {\n return;\n }\n\n const thisDevice = dqr.devices.find((device) => device.kind === DeviceKind.CURRENT);\n if (!thisDevice) {\n return;\n }\n\n observability.setTags({ deviceKey: toPublicKey(thisDevice.deviceKey)?.truncate() ?? '' });\n if (thisDevice.profile?.label) {\n observability.setTags({ deviceProfile: thisDevice.profile.label });\n }\n });\n });\n\n/**\n * What {@link identityManagerProvider} reads: the host-side identity manager, in a realm that has no\n * client proxy to subscribe through.\n */\nexport type IdentitySource = {\n readonly identity: { readonly did: string } | undefined;\n readonly stateUpdate: { on(listener: () => void): unknown };\n};\n\n/**\n * Tags every span and log of a realm with the identity, read from the services host itself. For the\n * dedicated worker, whose tracer and tags are its own: the tab's {@link identityProvider} only tags\n * the tab. Tags only — the tab already identifies the user with the analytics backend.\n */\nexport const identityManagerProvider = (identityManager: IdentitySource): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const apply = () => {\n const did = identityManager.identity?.did;\n if (did) {\n observability.setTags({ did });\n }\n };\n identityManager.stateUpdate.on(apply);\n apply();\n });\n\n/** Periodically publishes network connection and buffer metrics. */\nexport const networkMetricsProvider = (clientServices: Partial<ClientServices>): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n let lastNetworkStatus: NetworkStatus | undefined;\n\n // TODO(nf): support type in debounce()\n const updateSignalMetrics = new Event<NetworkStatus>().debounce(NETWORK_METRICS_MIN_INTERVAL);\n updateSignalMetrics.on(ctx, async () => {\n log('send signal metrics');\n (lastNetworkStatus?.signaling as NetworkStatus_Signal[])?.forEach(({ server, state }) => {\n observability.metrics.gauge('dxos.client.network.signal.connectionState', state, { server });\n });\n\n let swarmCount = 0;\n const connectionStates = new Map<string, number>();\n for (const state in ConnectionState) {\n connectionStates.set(state, 0);\n }\n\n let totalReadBufferSize = 0;\n let totalWriteBufferSize = 0;\n let totalChannelBufferSize = 0;\n\n lastNetworkStatus?.connectionInfo?.forEach((connectionInfo) => {\n swarmCount++;\n\n for (const conn of connectionInfo.connections ?? []) {\n connectionStates.set(conn.state, (connectionStates.get(conn.state) ?? 0) + 1);\n totalReadBufferSize += conn.readBufferSize ?? 0;\n totalWriteBufferSize += conn.writeBufferSize ?? 0;\n for (const stream of conn.streams ?? []) {\n totalChannelBufferSize += stream.writeBufferSize ?? 0;\n }\n }\n\n observability.metrics.gauge('dxos.client.network.swarm.count', swarmCount);\n for (const state in ConnectionState) {\n observability.metrics.gauge('dxos.client.network.connection.count', connectionStates.get(state) ?? 0, {\n state,\n });\n }\n observability.metrics.gauge('dxos.client.network.totalReadBufferSize', totalReadBufferSize);\n observability.metrics.gauge('dxos.client.network.totalWriteBufferSize', totalWriteBufferSize);\n observability.metrics.gauge('dxos.client.network.totalChannelBufferSize', totalChannelBufferSize);\n });\n });\n\n clientServices.NetworkService!.queryStatus().subscribe((networkStatus) => {\n lastNetworkStatus = networkStatus;\n updateSignalMetrics.emit();\n });\n\n scheduleTaskInterval(ctx, async () => updateSignalMetrics.emit(), NETWORK_METRICS_MIN_INTERVAL);\n\n return async () => {\n await ctx.dispose();\n };\n });\n\n/** Periodically publishes platform and heap memory metrics. */\nexport const runtimeMetricsProvider = (clientServices: Partial<ClientServices>): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n log('runtimeMetricsProvider: requesting platform from SystemService');\n const platform = yield* Effect.promise(() => clientServices.SystemService!.getPlatform());\n log('runtimeMetricsProvider: platform received');\n invariant(platform, 'platform is required');\n\n observability.setTags({\n platformType: Platform_PLATFORM_TYPE[platform.type].toLowerCase(),\n platform: platform.platform,\n arch: platform.arch,\n runtime: platform.runtime,\n });\n\n // Heap is a synchronous read, so the gauge reads it directly at collection time.\n const heapGauges = [\n ['dxos.client.runtime.heapUsed', () => readHeap().used],\n ['dxos.client.runtime.heapTotal', () => readHeap().total],\n ['dxos.client.runtime.heapSizeLimit', () => readHeap().limit],\n ] as const;\n for (const [name, read] of heapGauges) {\n ctx.onDispose(observability.metrics.observe(name, read, undefined, BYTES));\n }\n\n // The platform reading is an RPC and cross-realm memory waits for a GC, so both are sampled on\n // their own cadence and the gauges read the latest sample.\n let platformMemory: Platform['memory'];\n\n // The platform reports memory as a `google.protobuf.Struct`, so a gauge reading a byte count\n // has to check the JSON value rather than assume it is numeric.\n const memoryNumber = (key: string): number | undefined => {\n const value = platformMemory?.[key];\n return typeof value === 'number' ? value : undefined;\n };\n let crossRealmMemory: CrossRealmMemory | undefined;\n\n const servicesGauges = [\n ['dxos.client.services.runtime.heapUsed', () => memoryNumber('heapUsed')],\n ['dxos.client.services.runtime.heapTotal', () => memoryNumber('heapTotal')],\n ['dxos.client.services.runtime.rss', () => memoryNumber('rss')],\n ] as const;\n for (const [name, read] of servicesGauges) {\n ctx.onDispose(observability.metrics.observe(name, read, undefined, BYTES));\n }\n\n if (supportsCrossRealmMemory()) {\n for (const scope of ['window', 'shared-worker', 'dedicated-worker', 'other'] as const) {\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.client.runtime.memory.bytes',\n () => crossRealmMemory?.[scope],\n { scope },\n BYTES,\n ),\n );\n }\n }\n\n const sample = async () => {\n try {\n const platform = await clientServices.SystemService?.getPlatform();\n platformMemory = platform?.memory;\n } catch (error) {\n log('platform error', { error });\n }\n\n try {\n crossRealmMemory = await measureCrossRealmMemory();\n } catch (error) {\n // Not cross-origin isolated, or a measurement is already in flight; neither is retryable here.\n log('cross-realm memory unavailable', { error });\n }\n };\n\n scheduleTaskInterval(ctx, sample, MEMORY_SAMPLE_INTERVAL);\n void sample();\n\n return async () => {\n await ctx.dispose();\n };\n });\n\n/** Periodically publishes space membership, object count, and pipeline progress metrics. */\nexport const spacesMetricsProvider = (client: Client): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n // Pipeline subscriptions only; the gauges below read the live space list at collection time.\n const spaces = client.spaces.get();\n const subscriptions = new Map<string, { unsubscribe: () => void }>();\n ctx.onDispose(() => subscriptions.forEach((subscription) => subscription.unsubscribe()));\n\n // Read at collection time. The gap between the two is the \"known but not opened\" signal.\n ctx.onDispose(\n observability.metrics.observe('dxos.client.spaces.count', () => client.spaces.get().length, undefined, SPACES),\n );\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.client.spaces.ready.count',\n () => client.spaces.get().filter((space) => space.state.get() === SpaceState.SPACE_READY).length,\n undefined,\n SPACES,\n ),\n );\n\n const updateSpaceMetrics = new Event<Space>().debounce(SPACE_METRICS_MIN_INTERVAL);\n updateSpaceMetrics.on(ctx, async () => {\n log('send space metrics');\n // Reported as device-wide totals rather than per space: the previous `key` attribute cost one\n // series per space per device, unbounded in the number of spaces a user creates.\n const mapped = mapSpaces(client.spaces.get(), { truncateKeys: true });\n const total = (pick: (data: (typeof mapped)[number]) => number | undefined) =>\n mapped.reduce((sum, data) => sum + (pick(data) ?? 0), 0);\n\n observability.metrics.gauge(\n 'dxos.client.space.members',\n total((data) => data.members),\n );\n observability.metrics.gauge(\n 'dxos.client.space.objects',\n total((data) => data.objects),\n );\n observability.metrics.gauge(\n 'dxos.client.space.currentDataMutations',\n total((data) => data.currentDataMutations),\n );\n // Max, not a sum: epochs are per-space sequence numbers, so adding them means nothing.\n observability.metrics.gauge(\n 'dxos.client.space.epoch',\n mapped.reduce((max, data) => Math.max(max, data.epoch ?? 0), 0),\n );\n });\n\n const subscribeToSpaceUpdate = (space: Space) =>\n space.pipeline.subscribe({\n next: () => {\n updateSpaceMetrics.emit();\n },\n });\n\n spaces.forEach((space) => {\n subscriptions.set(space.id, subscribeToSpaceUpdate(space));\n });\n\n client.spaces.subscribe({\n next: async (spaces) => {\n spaces\n .filter((space) => !subscriptions.has(space.id))\n .forEach((space) => {\n subscriptions.set(space.id, subscribeToSpaceUpdate(space));\n });\n },\n });\n\n scheduleTaskInterval(ctx, async () => updateSpaceMetrics.emit(), SPACE_METRICS_MIN_INTERVAL);\n\n return async () => {\n await ctx.dispose();\n };\n });\n\n/** Publishes the document backlog folded across every space. */\nexport const documentsMetricsProvider = (client: Client): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n const { summary } = subscribeSyncSummary(client, ctx);\n\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.echo.documents.count',\n () => summary().localDocumentCount,\n { location: 'local' },\n DOCUMENTS,\n ),\n );\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.echo.documents.count',\n () => summary().remoteDocumentCount,\n { location: 'remote' },\n DOCUMENTS,\n ),\n );\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.echo.documents.unsynced.count',\n () => summary().unsyncedDocumentCount,\n undefined,\n DOCUMENTS,\n ),\n );\n\n return async () => {\n await ctx.dispose();\n };\n });\n\n/**\n * Publishes how long a client takes to sync, and how long it has been stuck.\n *\n * Both are needed. `episode.duration` records only when a backlog clears, so a client that never\n * finishes syncing contributes nothing to it — `stalled.duration` is what makes that client visible.\n */\nexport const syncMetricsProvider = (client: Client): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n const episodes = new SyncEpisodeTracker();\n let pending = 0;\n\n // Fed on every sync-state emission rather than at collection time: an episode that opens and\n // closes inside one 60s export window would otherwise never be seen at all.\n subscribeSyncSummary(client, ctx, (summary) => {\n pending = summary.pendingWorkCount;\n const closed = episodes.observe(Date.now(), summary.pendingWorkCount);\n if (closed) {\n log('sync episode closed', { durationMs: closed.durationMs, truncated: closed.truncated });\n observability.metrics.distribution(\n 'dxos.echo.sync.episode.duration',\n closed.durationMs / 1_000,\n undefined,\n SECONDS,\n );\n }\n });\n\n ctx.onDispose(\n observability.metrics.observe('dxos.echo.sync.pending.count', () => pending, undefined, { unit: '{item}' }),\n );\n ctx.onDispose(\n observability.metrics.observe(\n 'dxos.echo.sync.stalled.duration',\n () => episodes.stalledForMs(Date.now()) / 1_000,\n undefined,\n SECONDS,\n ),\n );\n\n return async () => {\n await ctx.dispose();\n };\n });\n\ntype MapSpacesOptions = {\n verbose?: boolean;\n truncateKeys?: boolean;\n};\n\nconst mapSpaces = (spaces: Space[], options: MapSpacesOptions = { verbose: false, truncateKeys: false }) => {\n return spaces.map((space) => {\n // TODO(burdon): Factor out.\n // TODO(burdon): Agent needs to restart before `ready` is available.\n const { open, ready } = space.internal.data.metrics ?? {};\n const openedAt = toDate(open);\n const readyAt = toDate(ready);\n const startup = openedAt && readyAt && readyAt.getTime() - openedAt.getTime();\n\n // TODO(burdon): Get feeds from client-services if verbose (factor out from devtools/diagnostics).\n // const host = client.services.services.DevtoolsHost!;\n const pipeline = space.internal.data.pipeline;\n const assertion = pipeline?.currentEpoch?.subject?.assertion;\n const currentEpoch = assertion && anyUnpack(assertion, EpochSchema);\n const startDataMutations = toTimeframe(currentEpoch?.timeframe).totalMessages();\n const epoch = currentEpoch?.number;\n const currentDataMutations = toTimeframe(pipeline?.currentDataTimeframe).totalMessages();\n const totalDataMutations = toTimeframe(pipeline?.targetDataTimeframe).totalMessages();\n\n return {\n // TODO(nf): truncate keys for DD?\n key: space.key.truncate(),\n open: space.isOpen,\n members: space.members.get().length,\n objects: space.internal.db.getAllObjectIds().length,\n startup,\n epoch,\n // appliedEpoch,\n startDataMutations,\n currentDataMutations,\n totalDataMutations,\n\n // TODO(burdon): Negative?\n progress: (\n Math.min(Math.abs((currentDataMutations - startDataMutations) / (totalDataMutations - startDataMutations)), 1) *\n 100\n ).toFixed(0),\n };\n });\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\n// @import-as-namespace\n\n// Kept out of `ObservabilityProvider` because these reach `@dxos/client` and `@dxos/protocols`.\n// The boot set is the parse graph, so a namespace re-export puts them in the eager graph of every\n// consumer of that namespace even where treeshaking would later drop them.\nexport * as Client from './client-observability.ts';\nexport * as SyncState from './sync-state.ts';\n"],"mappings":"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"}
|
|
@@ -63,7 +63,20 @@ var toAiGenerationProperties = (inference) => stripUndefined({
|
|
|
63
63
|
var stripUndefined = (properties) => Object.fromEntries(Object.entries(properties).filter(([, value]) => value !== void 0));
|
|
64
64
|
//#endregion
|
|
65
65
|
//#region src/version.ts
|
|
66
|
-
var DXOS_VERSION = "0.
|
|
66
|
+
var DXOS_VERSION = "0.16.0";
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/extensions/posthog/before-send.ts
|
|
69
|
+
/**
|
|
70
|
+
* Browser-raised errors with no stack and no effect on the user: a ResizeObserver callback that resized an element
|
|
71
|
+
* mid-delivery is reported this way, and the browser delivers the skipped notifications on the next frame.
|
|
72
|
+
*/
|
|
73
|
+
var BENIGN_EXCEPTIONS = [/^ResizeObserver loop/];
|
|
74
|
+
/** Drops `$exception` events whose every exception is benign. */
|
|
75
|
+
var dropBenignExceptions = (capture) => {
|
|
76
|
+
if (capture?.event !== "$exception") return capture;
|
|
77
|
+
const exceptions = capture.properties.$exception_list ?? [];
|
|
78
|
+
return exceptions.length > 0 && exceptions.every(({ value }) => typeof value === "string" && BENIGN_EXCEPTIONS.some((pattern) => pattern.test(value))) ? null : capture;
|
|
79
|
+
};
|
|
67
80
|
//#endregion
|
|
68
81
|
//#region src/extensions/posthog/otel-destination.ts
|
|
69
82
|
var __dxlog_file$1 = "/__w/dxos/dxos/packages/sdk/observability/src/extensions/posthog/otel-destination.ts";
|
|
@@ -124,7 +137,7 @@ var uploadLogs = async (endpoint, ndjson, kind = "logs") => {
|
|
|
124
137
|
}, {
|
|
125
138
|
"~LogMeta": "~LogMeta",
|
|
126
139
|
F: __dxlog_file,
|
|
127
|
-
L:
|
|
140
|
+
L: 107,
|
|
128
141
|
S: void 0
|
|
129
142
|
});
|
|
130
143
|
return;
|
|
@@ -138,14 +151,19 @@ var uploadLogs = async (endpoint, ndjson, kind = "logs") => {
|
|
|
138
151
|
}, {
|
|
139
152
|
"~LogMeta": "~LogMeta",
|
|
140
153
|
F: __dxlog_file,
|
|
141
|
-
L:
|
|
154
|
+
L: 113,
|
|
142
155
|
S: void 0
|
|
143
156
|
});
|
|
144
157
|
return;
|
|
145
158
|
}
|
|
146
159
|
};
|
|
160
|
+
/**
|
|
161
|
+
* Class on an iframe that session replay records with its content. Only for a same-origin iframe the
|
|
162
|
+
* recorder can read: it never releases an opaque one removed with an ancestor, which replays empty anyway.
|
|
163
|
+
*/
|
|
164
|
+
var REPLAY_IFRAME_CLASS = "dx-replay-iframe";
|
|
147
165
|
/** Create a PostHog-backed observability extension for events, errors, and support tickets. */
|
|
148
|
-
var extensions = Effect.fn(function* ({ config, release, environment, serviceName = "app", posthog: posthogConfig, logStore, feedbackLogMaxSize, feedbackLogsEndpoint = DEFAULT_FEEDBACK_LOGS_ENDPOINT, node }) {
|
|
166
|
+
var extensions = Effect.fn(function* ({ config, release, environment, serviceName = "app", posthog: posthogConfig, logStore, feedbackLogMaxSize, feedbackLogsEndpoint = DEFAULT_FEEDBACK_LOGS_ENDPOINT, sessionRecordingStart, node }) {
|
|
149
167
|
if (isNode()) {
|
|
150
168
|
const { extensions: nodeExtensions } = yield* Effect.promise(() => import("#posthog-transport"));
|
|
151
169
|
return yield* nodeExtensions({
|
|
@@ -159,7 +177,7 @@ var extensions = Effect.fn(function* ({ config, release, environment, serviceNam
|
|
|
159
177
|
log("PostHog is being stubbed because it is running in a worker.", void 0, {
|
|
160
178
|
"~LogMeta": "~LogMeta",
|
|
161
179
|
F: __dxlog_file,
|
|
162
|
-
L:
|
|
180
|
+
L: 143,
|
|
163
181
|
S: this
|
|
164
182
|
});
|
|
165
183
|
return stubExtension;
|
|
@@ -170,7 +188,7 @@ var extensions = Effect.fn(function* ({ config, release, environment, serviceNam
|
|
|
170
188
|
log.info("Missing POSTHOG_API_KEY or POSTHOG_API_HOST", void 0, {
|
|
171
189
|
"~LogMeta": "~LogMeta",
|
|
172
190
|
F: __dxlog_file,
|
|
173
|
-
L:
|
|
191
|
+
L: 150,
|
|
174
192
|
S: this
|
|
175
193
|
});
|
|
176
194
|
return stubExtension;
|
|
@@ -178,15 +196,23 @@ var extensions = Effect.fn(function* ({ config, release, environment, serviceNam
|
|
|
178
196
|
const { default: posthog } = yield* Effect.promise(() => import("posthog-js"));
|
|
179
197
|
const { logProcessor } = yield* Effect.promise(() => import("./chunk-log-processor.mjs"));
|
|
180
198
|
let unregisterPosthogProcessors;
|
|
199
|
+
const recordingStart = posthogConfig?.disable_session_recording ? void 0 : sessionRecordingStart;
|
|
181
200
|
return {
|
|
182
|
-
initialize: (context) => Effect.
|
|
201
|
+
initialize: (context) => Effect.gen(function* () {
|
|
183
202
|
posthog.init(apiKey, {
|
|
184
203
|
api_host,
|
|
185
204
|
mask_all_text: true,
|
|
186
205
|
capture_exceptions: true,
|
|
187
206
|
cross_subdomain_cookie: false,
|
|
188
|
-
...posthogConfig
|
|
207
|
+
...posthogConfig,
|
|
208
|
+
session_recording: {
|
|
209
|
+
blockSelector: `:not(body):has(> iframe:not(.${REPLAY_IFRAME_CLASS}))`,
|
|
210
|
+
...posthogConfig?.session_recording
|
|
211
|
+
},
|
|
212
|
+
before_send: [dropBenignExceptions, ...[posthogConfig?.before_send ?? []].flat()],
|
|
213
|
+
...recordingStart ? { disable_session_recording: true } : {}
|
|
189
214
|
});
|
|
215
|
+
if (recordingStart) yield* Effect.forkDetach(recordingStart.pipe(Effect.andThen(Effect.sync(() => posthog.startSessionRecording()))));
|
|
190
216
|
posthog.register({
|
|
191
217
|
sdkVersion: DXOS_VERSION,
|
|
192
218
|
...release ? { release } : {},
|
|
@@ -196,7 +222,7 @@ var extensions = Effect.fn(function* ({ config, release, environment, serviceNam
|
|
|
196
222
|
const removePosthogLog = log.addProcessor(logProcessor, void 0, {
|
|
197
223
|
"~LogMeta": "~LogMeta",
|
|
198
224
|
F: __dxlog_file,
|
|
199
|
-
L:
|
|
225
|
+
L: 192,
|
|
200
226
|
S: this
|
|
201
227
|
});
|
|
202
228
|
const tagSession = () => context.setTags({ sessionId: posthog.get_session_id() }, "logs");
|
|
@@ -280,7 +306,7 @@ var extensions = Effect.fn(function* ({ config, release, environment, serviceNam
|
|
|
280
306
|
log.warn("PostHog session context unavailable", { err }, {
|
|
281
307
|
"~LogMeta": "~LogMeta",
|
|
282
308
|
F: __dxlog_file,
|
|
283
|
-
L:
|
|
309
|
+
L: 278,
|
|
284
310
|
S: this
|
|
285
311
|
});
|
|
286
312
|
return;
|
|
@@ -307,7 +333,7 @@ var extensions = Effect.fn(function* ({ config, release, environment, serviceNam
|
|
|
307
333
|
}, {
|
|
308
334
|
"~LogMeta": "~LogMeta",
|
|
309
335
|
F: __dxlog_file,
|
|
310
|
-
L:
|
|
336
|
+
L: 301,
|
|
311
337
|
S: this
|
|
312
338
|
});
|
|
313
339
|
}
|
|
@@ -321,6 +347,7 @@ var posthog_exports = /* @__PURE__ */ __exportAll({
|
|
|
321
347
|
AI_GENERATION_EVENT: () => AI_GENERATION_EVENT,
|
|
322
348
|
AI_SPAN_EVENT: () => AI_SPAN_EVENT,
|
|
323
349
|
AI_TRACE_EVENT: () => AI_TRACE_EVENT,
|
|
350
|
+
REPLAY_IFRAME_CLASS: () => REPLAY_IFRAME_CLASS,
|
|
324
351
|
extensions: () => extensions,
|
|
325
352
|
otelDestination: () => otelDestination,
|
|
326
353
|
toAiGenerationProperties: () => toAiGenerationProperties,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunk-ObservabilityExtension.mjs","names":[],"sources":["../../src/extensions/posthog/llm-analytics.ts","../../src/version.ts","../../src/extensions/posthog/otel-destination.ts","../../src/extensions/posthog/extension.ts","../../src/extensions/posthog/index.ts","../../src/ObservabilityExtension.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport type * as ObservabilityExtension from '../../ObservabilityExtension.ts';\n\n/**\n * PostHog's LLM analytics schema. Three events with `$ai_*` properties are what the product reads:\n * `$ai_trace` is the top-level unit (a conversation turn here), `$ai_span` a step inside it (a tool\n * call), `$ai_generation` a model call. They are linked by `$ai_trace_id`, and a span or generation\n * names its parent with `$ai_parent_id`.\n *\n * `$ai_input_tokens` is the uncached count, which is what PostHog expects alongside the two cache\n * figures — together they price the call, and their ratio is the prompt-cache hit rate.\n */\nexport const AI_GENERATION_EVENT = '$ai_generation';\nexport const AI_TRACE_EVENT = '$ai_trace';\nexport const AI_SPAN_EVENT = '$ai_span';\n\n/** `$ai_trace`: the turn, carrying the prompt and the messages it produced as its states. */\nexport const toAiTraceProperties = (turn: ObservabilityExtension.Turn): Record<string, unknown> =>\n stripUndefined({\n ...spanProperties(turn),\n });\n\n/** `$ai_span` of type `tool`: one tool call, carrying its arguments and result as its states. */\nexport const toAiSpanProperties = (toolCall: ObservabilityExtension.ToolCall): Record<string, unknown> =>\n stripUndefined({\n ...spanProperties(toolCall),\n $ai_span_type: 'tool',\n });\n\nconst spanProperties = (span: ObservabilityExtension.AiSpanBase): Record<string, unknown> => ({\n $ai_trace_id: span.traceId,\n $ai_span_id: span.spanId,\n $ai_parent_id: span.parentSpanId,\n $ai_span_name: span.spanName,\n $ai_session_id: span.sessionId,\n $ai_latency: span.latency,\n $ai_input_state: span.content?.input,\n $ai_output_state: span.content?.output,\n $ai_content_truncated: span.content?.truncated,\n $ai_is_error: span.errorClass ? true : undefined,\n $ai_error: span.errorClass,\n});\n\nexport const toAiGenerationProperties = (inference: ObservabilityExtension.Inference): Record<string, unknown> =>\n stripUndefined({\n $ai_trace_id: inference.traceId,\n $ai_span_id: inference.spanId,\n $ai_parent_id: inference.parentSpanId,\n $ai_span_name: inference.spanName,\n $ai_provider: inference.provider,\n $ai_model: inference.model,\n $ai_session_id: inference.sessionId,\n $ai_model_parameters: inference.parameters,\n $ai_input_tokens: inference.inputTokens,\n $ai_output_tokens: inference.outputTokens,\n $ai_cache_read_input_tokens: inference.cacheReadTokens,\n $ai_cache_creation_input_tokens: inference.cacheWriteTokens,\n $ai_latency: inference.latency,\n $ai_stream: inference.streaming,\n $ai_input: inference.content?.input,\n $ai_output_choices: inference.content?.output,\n $ai_tools: inference.content?.tools,\n $ai_content_truncated: inference.content?.truncated,\n $ai_is_error: inference.errorClass ? true : undefined,\n $ai_error: inference.errorClass,\n });\n\nconst stripUndefined = (properties: Record<string, unknown>): Record<string, unknown> =>\n Object.fromEntries(Object.entries(properties).filter(([, value]) => value !== undefined));\n","//\n// Copyright 2026 DXOS.org\n//\n\nexport const DXOS_VERSION = '0.14.0';\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Config, getEnvString } from '@dxos/config';\nimport { log } from '@dxos/log';\n\nimport { type OtelDestination } from '../otel/otel.ts';\n\n/** PostHog serves OTLP under `/i` of the same host it ingests product analytics on, not at the root. */\nconst OTLP_PATH_PREFIX = '/i';\n\n/**\n * PostHog as an OTLP backend, authenticated with the public `phc_` project token rather than a\n * secret, so the browser can post to it directly.\n */\nexport const otelDestination = (config: Config): OtelDestination | undefined => {\n const host = getEnvString(config, 'DX_POSTHOG_API_HOST');\n const apiKey = getEnvString(config, 'DX_POSTHOG_API_KEY');\n if (!host) {\n return undefined;\n }\n if (!apiKey) {\n log.info('DX_POSTHOG_API_HOST is set without DX_POSTHOG_API_KEY; skipping PostHog OTLP export');\n return undefined;\n }\n\n return {\n endpoint: `${host.replace(/\\/$/, '')}${OTLP_PATH_PREFIX}`,\n headers: { Authorization: `Bearer ${apiKey}` },\n };\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport { type PostHogConfig } from 'posthog-js';\n\nimport { type Config, getEnvString } from '@dxos/config';\nimport { log } from '@dxos/log';\nimport { type IdbLogStore } from '@dxos/log-store-idb';\nimport { gzip, isNode } from '@dxos/util';\n\nimport * as ObservabilityExtension from '../../ObservabilityExtension.ts';\nimport { DXOS_VERSION } from '../../version.ts';\nimport { stubExtension } from '../stub.ts';\nimport {\n AI_GENERATION_EVENT,\n AI_SPAN_EVENT,\n AI_TRACE_EVENT,\n toAiGenerationProperties,\n toAiSpanProperties,\n toAiTraceProperties,\n} from './llm-analytics.ts';\nimport { otelDestination } from './otel-destination.ts';\n\nconst WIDGET_SESSION_STORAGE_KEY = 'dxos.support.widgetSessionId';\n\nconst widgetSessionId = (): string => {\n try {\n const existing = localStorage.getItem(WIDGET_SESSION_STORAGE_KEY);\n if (existing) {\n return existing;\n }\n const minted = crypto.randomUUID();\n localStorage.setItem(WIDGET_SESSION_STORAGE_KEY, minted);\n return minted;\n } catch {\n return crypto.randomUUID();\n }\n};\n\nexport type ExtensionsOptions = {\n config: Config;\n /** Release identifier, e.g. `composer@0.8.3`. */\n release?: string;\n /** Deployment environment, e.g. `production` or `staging`. */\n environment?: string;\n serviceName?: string;\n posthog?: Partial<PostHogConfig>;\n /**\n * Shared persistent log store for debug log dumps.\n * The owning app is expected to register `logStore.processor` with `log` itself —\n * this extension only consumes the buffered logs (via `export()`).\n */\n logStore?: IdbLogStore;\n /**\n * Maximum byte size passed to `logStore.export()` when uploading feedback logs.\n * Should match the upload limit enforced by the server receiving the logs.\n * When omitted the full store is exported without trimming.\n */\n feedbackLogMaxSize?: number;\n /**\n * Where to POST feedback logs, defaulting to {@link DEFAULT_FEEDBACK_LOGS_ENDPOINT}.\n * A native build serves its frontend from its own origin and has no such route there, so it must\n * pass the absolute URL of a deployment that does.\n */\n feedbackLogsEndpoint?: string;\n /** What the `posthog-node` transport needs; a browser host has posthog-js and reads none of it. */\n node?: NodeOptions;\n};\n\nexport type NodeOptions = {\n /** Pins the project instead of reading `DX_POSTHOG_API_KEY`. */\n apiKey?: string;\n /** Ingestion host — a region, or a proxy on your own domain. */\n host?: string;\n /** Attribution for events captured before `identify`, since there is no ambient person. */\n distinctId?: string;\n /** Which MCP server this host is, stamped on every `$mcp_*` event. */\n mcpServer?: { name: string; version: string };\n};\n\n/** Same-origin route of the web deployment, which proxies the upload to object storage. */\nconst DEFAULT_FEEDBACK_LOGS_ENDPOINT = '/api/feedback-logs';\n\n/** Upload serialized logs, gzipped, to the feedback-logs endpoint. Returns the R2 key on success. */\nconst uploadLogs = async (\n endpoint: string,\n ndjson: string,\n kind: ObservabilityExtension.SupportUploadKind = 'logs',\n): Promise<string | undefined> => {\n try {\n // The endpoint may be relative (same-origin) or absolute (native builds), so append rather than parse.\n const url = kind === 'logs' ? endpoint : `${endpoint}${endpoint.includes('?') ? '&' : '?'}kind=${kind}`;\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/gzip' },\n body: await gzip(ndjson),\n });\n if (!response.ok) {\n log.warn('feedback log upload failed', { endpoint, status: response.status });\n return undefined;\n }\n const { key } = await response.json();\n return key;\n } catch (err) {\n log.warn('feedback log upload error', { endpoint, error: err });\n return undefined;\n }\n};\n\n/** Create a PostHog-backed observability extension for events, errors, and support tickets. */\nexport const extensions: (options: ExtensionsOptions) => Effect.Effect<ObservabilityExtension.Extension> = Effect.fn(\n function* ({\n config,\n release,\n environment,\n serviceName = 'app',\n posthog: posthogConfig,\n logStore,\n feedbackLogMaxSize,\n feedbackLogsEndpoint = DEFAULT_FEEDBACK_LOGS_ENDPOINT,\n node,\n }) {\n if (isNode()) {\n const { extensions: nodeExtensions } = yield* Effect.promise(() => import('#posthog-transport'));\n return yield* nodeExtensions({ config, release, environment, node });\n }\n if (typeof window === 'undefined') {\n log('PostHog is being stubbed because it is running in a worker.');\n return stubExtension;\n }\n\n const apiKey = getEnvString(config, 'DX_POSTHOG_API_KEY');\n const api_host = getEnvString(config, 'DX_POSTHOG_API_HOST');\n if (!apiKey || !api_host) {\n log.info('Missing POSTHOG_API_KEY or POSTHOG_API_HOST');\n return stubExtension;\n }\n\n const { default: posthog } = yield* Effect.promise(() => import('posthog-js'));\n const { logProcessor } = yield* Effect.promise(() => import('./log-processor.ts'));\n let unregisterPosthogProcessors: (() => void) | undefined;\n\n return {\n initialize: (context) =>\n Effect.sync(() => {\n // https://posthog.com/docs/libraries/js/config\n posthog.init(apiKey, {\n api_host,\n mask_all_text: true,\n capture_exceptions: true,\n // Cookies stay scoped to the exact host; the cross-subdomain dmn_chk_* probe fails on public-suffix hosts (e.g. *.pages.dev) and spams the console.\n cross_subdomain_cookie: false,\n ...posthogConfig,\n });\n posthog.register({\n sdkVersion: DXOS_VERSION,\n ...(release ? { release } : {}),\n ...(environment ? { environment } : {}),\n });\n unregisterPosthogProcessors?.();\n const removePosthogLog = log.addProcessor(logProcessor);\n // PostHog joins a log record to its session replay on a log attribute literally named\n // `sessionId` — not the OTel `session.id` resource attribute — and rotates that id.\n const tagSession = () => context.setTags({ sessionId: posthog.get_session_id() }, 'logs');\n const removeSessionListener = posthog.onSessionId(tagSession);\n tagSession();\n unregisterPosthogProcessors = () => {\n removePosthogLog();\n removeSessionListener();\n };\n }),\n close: () =>\n Effect.sync(() => {\n unregisterPosthogProcessors?.();\n unregisterPosthogProcessors = undefined;\n }),\n enable: () => Effect.sync(() => posthog.opt_in_capturing()),\n disable: () => Effect.sync(() => posthog.opt_out_capturing()),\n identify: (distinctId, attributes, setOnceAttributes) => {\n posthog.identify(distinctId, attributes, setOnceAttributes);\n },\n alias: (distinctId, previousId) => {\n posthog.alias(distinctId, previousId);\n },\n setTags: (tags) => {\n posthog.register_for_session(tags);\n },\n get enabled(): boolean {\n return posthog.is_capturing();\n },\n apis: [\n {\n kind: 'events',\n isAvailable: () => Effect.succeed(true),\n captureEvent: (event, attributes) => {\n posthog.capture(event, attributes);\n },\n },\n {\n kind: 'errors',\n isAvailable: () => Effect.succeed(true),\n captureException: (error, attributes) => {\n posthog.captureException(error, attributes);\n },\n },\n {\n kind: 'ai',\n isAvailable: () => Effect.succeed(true),\n captureInference: (inference) => {\n posthog.capture(AI_GENERATION_EVENT, toAiGenerationProperties(inference));\n },\n captureTurn: (turn) => {\n posthog.capture(AI_TRACE_EVENT, toAiTraceProperties(turn));\n },\n captureToolCall: (toolCall) => {\n posthog.capture(AI_SPAN_EVENT, toAiSpanProperties(toolCall));\n },\n },\n {\n kind: 'support',\n isAvailable: () => Effect.succeed(logStore !== undefined),\n uploadLogs: async () => {\n if (logStore === undefined) {\n return undefined;\n }\n const ndjson = await logStore.export({ maxSize: feedbackLogMaxSize });\n if (ndjson.length === 0) {\n return undefined;\n }\n return await uploadLogs(feedbackLogsEndpoint, ndjson);\n },\n uploadNdjson: async (ndjson, kind) =>\n ndjson.length === 0 ? undefined : await uploadLogs(feedbackLogsEndpoint, ndjson, kind),\n sessionContext: () => {\n if (!posthog.__loaded) {\n return undefined;\n }\n try {\n return {\n distinctId: posthog.get_distinct_id(),\n widgetSessionId: widgetSessionId(),\n sessionId: posthog.get_session_id(),\n replayUrl: posthog.get_session_replay_url({ withTimestamp: true, timestampLookBack: 30 }),\n currentUrl: `${window.location.origin}${window.location.pathname}`,\n };\n } catch (err) {\n log.warn('PostHog session context unavailable', { err });\n return undefined;\n }\n },\n flushLogs: async (attributes) => {\n const destination = otelDestination(config);\n if (!destination) {\n return;\n }\n const ndjson = await logStore?.export({ maxSize: feedbackLogMaxSize });\n if (!ndjson || ndjson.length === 0) {\n return;\n }\n const { flushSupportLogs } = await import('./support-logs.ts');\n const count = await flushSupportLogs(ndjson, {\n destination,\n resourceAttributes: {\n 'service.name': serviceName,\n ...(release ? { 'service.version': release } : {}),\n ...(environment ? { 'deployment.environment': environment } : {}),\n },\n attributes,\n });\n log.info('support logs flushed to PostHog', { ...attributes, count });\n },\n },\n ],\n };\n },\n);\n","//\n// Copyright 2025 DXOS.org\n//\n\nexport * from './llm-analytics.ts';\nexport * from './extension.ts';\nexport * from './otel-destination.ts';\n","//\n// Copyright 2025 DXOS.org\n//\n\n// @import-as-namespace\n\nimport type * as Effect from 'effect/Effect';\n\nimport { type CleanupFn } from '@dxos/async';\nimport { type MetricObserver } from '@dxos/tracing';\n\nexport * from './extensions/index.ts';\n\n/**\n * Kind of observability extension.\n *\n * - errors: Error tracking (e.g., PostHog)\n * - events: Product usage event tracking (e.g., PostHog)\n * - support: Support tickets anchoring user reports and telemetry (e.g., PostHog)\n * - ai: Model inferences, tool calls, and turns (e.g., PostHog LLM analytics, OTel gen_ai)\n * - logs: Structured logging (e.g., OTEL)\n * - mcp: MCP server sessions and tool calls (e.g., PostHog)\n * - metrics: Metric data (e.g., OTEL)\n * - traces: Distributed tracing (e.g., OTEL)\n */\nexport type Kind = 'ai' | 'errors' | 'events' | 'support' | 'logs' | 'mcp' | 'metrics' | 'traces';\n\n/**\n * Base for every extension API variant. All kinds implement availability the same way.\n */\nexport type ExtensionApiBase<K extends Kind = Kind> = {\n kind: K;\n isAvailable(): Effect.Effect<boolean>;\n};\n\n/**\n * Instrument-level metadata, declared once by the first caller for a given metric name.\n * Units must be UCUM as OTel requires — `s`, `By`, `{thing}`.\n */\nexport type MetricMeta = { unit?: string; description?: string };\n\n/**\n * Metrics extension API (kind-specific methods only).\n */\nexport type Metrics = {\n gauge(name: string, value: number, tags?: Attributes, meta?: MetricMeta): void;\n increment(name: string, value?: number, tags?: Attributes, meta?: MetricMeta): void;\n distribution(name: string, value: number, tags?: Attributes, meta?: MetricMeta): void;\n /**\n * Registers a callback read once per export interval.\n * Prefer this over {@link Metrics.gauge} for any \"current value\" metric, since a pushed gauge\n * only lands in the export windows its producer happens to tick in.\n */\n observe(name: string, callback: MetricObserver, tags?: Attributes, meta?: MetricMeta): CleanupFn;\n};\n\n/**\n * Errors extension API (kind-specific methods only).\n */\nexport type Errors = {\n captureException(error: Error, attributes?: Attributes): void;\n};\n\n/**\n * Wider than {@link Attributes}: event properties carry structured values (message arrays,\n * nested objects), not just scalars.\n */\nexport type EventAttributes = Record<string, unknown>;\n\n/**\n * Events extension API (kind-specific methods only).\n */\nexport type Events = {\n captureEvent(event: string, attributes?: EventAttributes): void;\n};\n\n/**\n * One model call, already filtered by the capture policy and scrubbed (see `AiObservability`). Shaped\n * after the OTel GenAI conventions rather than any vendor's schema — an extension maps it onto\n * whatever its backend calls these things.\n */\nexport type Inference = {\n traceId: string;\n spanId: string;\n parentSpanId?: string;\n /** Provider span name, e.g. `LanguageModel.streamText`. */\n spanName: string;\n /** `gen_ai.system` — the serving product, not the wire dialect. */\n provider?: string;\n model?: string;\n /** Conversation the call belongs to, when the call site named one. */\n sessionId?: string;\n /** Request parameters the provider reported (temperature, max_tokens, …). */\n parameters?: Record<string, unknown>;\n /** Excludes tokens served from the prompt cache; add {@link cacheReadTokens} for the total. */\n inputTokens?: number;\n outputTokens?: number;\n /** Absent rather than zero when the provider does not report prompt caching. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n /** Wall-clock duration, in seconds. */\n latency: number;\n streaming: boolean;\n /** Absent entirely when the capture policy denied it. */\n content?: GenerationContent;\n /** Exception class name only — a provider's message can embed request or response fragments. */\n errorClass?: string;\n};\n\nexport type GenerationContent = {\n /** Parsed prompt messages, or the raw string when {@link truncated} left it unparseable. */\n input?: unknown;\n output?: unknown;\n tools?: unknown;\n /** Some field above was cut to fit, so it is a fragment rather than the whole. */\n truncated?: boolean;\n};\n\n/** Fields a turn and a tool call share with an inference: identity, timing, and gated content. */\nexport type AiSpanBase = {\n traceId: string;\n spanId: string;\n parentSpanId?: string;\n /** For a tool call, the tool's name rather than the span's. */\n spanName: string;\n /** Conversation the span belongs to, when the call site named one. */\n sessionId?: string;\n /** Wall-clock duration, in seconds. */\n latency: number;\n /** Absent entirely when the capture policy denied it. */\n content?: AiSpanContent;\n /** Exception class name only, as on {@link Inference}. */\n errorClass?: string;\n};\n\nexport type AiSpanContent = {\n input?: unknown;\n output?: unknown;\n /** Some field above was cut to fit, so it is a fragment rather than the whole. */\n truncated?: boolean;\n};\n\n/**\n * One conversation turn: the unit a backend groups a turn's model calls and tool calls under (what\n * PostHog calls a trace). Its input is the user prompt and its output the messages the turn produced.\n */\nexport type Turn = AiSpanBase;\n\n/** One tool call inside a turn (what PostHog calls a span), with the arguments and the result. */\nexport type ToolCall = AiSpanBase;\n\n/**\n * AI extension API (kind-specific methods only).\n */\nexport type Ai = {\n captureInference(inference: Inference): void;\n captureTurn(turn: Turn): void;\n captureToolCall(toolCall: ToolCall): void;\n};\n\n/** The client name an MCP event carries when no request named its client; `@posthog/mcp` drops a falsy one. */\nexport const UNKNOWN_MCP_CLIENT = 'unknown';\n\n/** What every MCP event carries: the session it groups under and the client it came from. */\nexport type McpSession = {\n /** Groups the session's events; one server process is one session over a stdio transport. */\n sessionId: string;\n clientName?: string;\n clientVersion?: string;\n protocolVersion?: string;\n};\n\n/** MCP extension API (kind-specific methods only). */\nexport type Mcp = {\n /** A client connected successfully. */\n captureInitialize(session: McpSession): void;\n captureToolCall(\n call: McpSession & { toolName: string; parameters?: unknown; durationMs: number; isError: boolean },\n ): void;\n};\n\n/**\n * Support extension API (kind-specific methods only).\n */\nexport type SupportLogTags = { ticketId: string } | { reportId: string };\n\n/** What an uploaded NDJSON bundle is, which decides where it is filed in storage. */\nexport type SupportUploadKind = 'logs' | 'trajectory';\n\nexport type Support = {\n uploadLogs(): Promise<string | undefined>;\n /** Uploads an NDJSON bundle the caller built (e.g. an agent trajectory), returning its storage key. */\n uploadNdjson(ndjson: string, kind: SupportUploadKind): Promise<string | undefined>;\n sessionContext(): SupportSessionContext | undefined;\n flushLogs(attributes: SupportLogTags): Promise<void>;\n};\n\nexport type ExtensionApi =\n | (ExtensionApiBase<'errors'> & Errors)\n | (ExtensionApiBase<'events'> & Events)\n | (ExtensionApiBase<'support'> & Support)\n | (ExtensionApiBase<'ai'> & Ai)\n // TODO(wittjosiah): Direct logs api?\n | (ExtensionApiBase<'mcp'> & Mcp)\n | ExtensionApiBase<'logs'>\n | (ExtensionApiBase<'metrics'> & Metrics)\n // TODO(wittjosiah): Direct traces api?\n | ExtensionApiBase<'traces'>;\n\n/**\n * What a browser knows about its own telemetry session. Shaped after what posthog-js's own widget\n * sends.\n */\nexport type SupportSessionContext = {\n distinctId: string;\n /** Browser-minted id the widget API uses for access control on anonymous tickets. */\n widgetSessionId: string;\n sessionId?: string;\n replayUrl?: string;\n currentUrl?: string;\n};\n\n/**\n * Attributes to be attached to observability events.\n */\nexport type Attributes = Record<string, string | number | boolean | undefined>;\n\nexport type ExtensionContext = {\n setTags(tags: Attributes, kind?: Kind): void;\n};\n\n/**\n * Implementation of an observability extension API.\n */\nexport type Extension = {\n initialize?(context: ExtensionContext): Effect.Effect<void, Error>;\n close?(): Effect.Effect<void>;\n enable?(): Effect.Effect<void>;\n disable?(): Effect.Effect<void>;\n flush?(): Effect.Effect<void>;\n identify?(distinctId: string, attributes?: Attributes, setOnceAttributes?: Attributes): void;\n alias?(distinctId: string, previousId?: string): void;\n setTags?(tags: Record<string, string>, kind?: Kind): void;\n enabled: boolean;\n apis: ExtensionApi[];\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chunk-ObservabilityExtension.mjs","names":["__dxlog_file"],"sources":["../../src/extensions/posthog/llm-analytics.ts","../../src/version.ts","../../src/extensions/posthog/before-send.ts","../../src/extensions/posthog/otel-destination.ts","../../src/extensions/posthog/extension.ts","../../src/extensions/posthog/index.ts","../../src/ObservabilityExtension.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport type * as ObservabilityExtension from '../../ObservabilityExtension.ts';\n\n/**\n * PostHog's LLM analytics schema. Three events with `$ai_*` properties are what the product reads:\n * `$ai_trace` is the top-level unit (a conversation turn here), `$ai_span` a step inside it (a tool\n * call), `$ai_generation` a model call. They are linked by `$ai_trace_id`, and a span or generation\n * names its parent with `$ai_parent_id`.\n *\n * `$ai_input_tokens` is the uncached count, which is what PostHog expects alongside the two cache\n * figures — together they price the call, and their ratio is the prompt-cache hit rate.\n */\nexport const AI_GENERATION_EVENT = '$ai_generation';\nexport const AI_TRACE_EVENT = '$ai_trace';\nexport const AI_SPAN_EVENT = '$ai_span';\n\n/** `$ai_trace`: the turn, carrying the prompt and the messages it produced as its states. */\nexport const toAiTraceProperties = (turn: ObservabilityExtension.Turn): Record<string, unknown> =>\n stripUndefined({\n ...spanProperties(turn),\n });\n\n/** `$ai_span` of type `tool`: one tool call, carrying its arguments and result as its states. */\nexport const toAiSpanProperties = (toolCall: ObservabilityExtension.ToolCall): Record<string, unknown> =>\n stripUndefined({\n ...spanProperties(toolCall),\n $ai_span_type: 'tool',\n });\n\nconst spanProperties = (span: ObservabilityExtension.AiSpanBase): Record<string, unknown> => ({\n $ai_trace_id: span.traceId,\n $ai_span_id: span.spanId,\n $ai_parent_id: span.parentSpanId,\n $ai_span_name: span.spanName,\n $ai_session_id: span.sessionId,\n $ai_latency: span.latency,\n $ai_input_state: span.content?.input,\n $ai_output_state: span.content?.output,\n $ai_content_truncated: span.content?.truncated,\n $ai_is_error: span.errorClass ? true : undefined,\n $ai_error: span.errorClass,\n});\n\nexport const toAiGenerationProperties = (inference: ObservabilityExtension.Inference): Record<string, unknown> =>\n stripUndefined({\n $ai_trace_id: inference.traceId,\n $ai_span_id: inference.spanId,\n $ai_parent_id: inference.parentSpanId,\n $ai_span_name: inference.spanName,\n $ai_provider: inference.provider,\n $ai_model: inference.model,\n $ai_session_id: inference.sessionId,\n $ai_model_parameters: inference.parameters,\n $ai_input_tokens: inference.inputTokens,\n $ai_output_tokens: inference.outputTokens,\n $ai_cache_read_input_tokens: inference.cacheReadTokens,\n $ai_cache_creation_input_tokens: inference.cacheWriteTokens,\n $ai_latency: inference.latency,\n $ai_stream: inference.streaming,\n $ai_input: inference.content?.input,\n $ai_output_choices: inference.content?.output,\n $ai_tools: inference.content?.tools,\n $ai_content_truncated: inference.content?.truncated,\n $ai_is_error: inference.errorClass ? true : undefined,\n $ai_error: inference.errorClass,\n });\n\nconst stripUndefined = (properties: Record<string, unknown>): Record<string, unknown> =>\n Object.fromEntries(Object.entries(properties).filter(([, value]) => value !== undefined));\n","//\n// Copyright 2026 DXOS.org\n//\n\nexport const DXOS_VERSION = '0.16.0';\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type BeforeSendFn } from 'posthog-js';\n\n/**\n * Browser-raised errors with no stack and no effect on the user: a ResizeObserver callback that resized an element\n * mid-delivery is reported this way, and the browser delivers the skipped notifications on the next frame.\n */\nconst BENIGN_EXCEPTIONS = [/^ResizeObserver loop/];\n\n/** Drops `$exception` events whose every exception is benign. */\nexport const dropBenignExceptions: BeforeSendFn = (capture) => {\n if (capture?.event !== '$exception') {\n return capture;\n }\n\n const exceptions: { value?: unknown }[] = capture.properties.$exception_list ?? [];\n const benign =\n exceptions.length > 0 &&\n exceptions.every(\n ({ value }) => typeof value === 'string' && BENIGN_EXCEPTIONS.some((pattern) => pattern.test(value)),\n );\n return benign ? null : capture;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport { type Config, getEnvString } from '@dxos/config';\nimport { log } from '@dxos/log';\n\nimport { type OtelDestination } from '../otel/otel.ts';\n\n/** PostHog serves OTLP under `/i` of the same host it ingests product analytics on, not at the root. */\nconst OTLP_PATH_PREFIX = '/i';\n\n/**\n * PostHog as an OTLP backend, authenticated with the public `phc_` project token rather than a\n * secret, so the browser can post to it directly.\n */\nexport const otelDestination = (config: Config): OtelDestination | undefined => {\n const host = getEnvString(config, 'DX_POSTHOG_API_HOST');\n const apiKey = getEnvString(config, 'DX_POSTHOG_API_KEY');\n if (!host) {\n return undefined;\n }\n if (!apiKey) {\n log.info('DX_POSTHOG_API_HOST is set without DX_POSTHOG_API_KEY; skipping PostHog OTLP export');\n return undefined;\n }\n\n return {\n endpoint: `${host.replace(/\\/$/, '')}${OTLP_PATH_PREFIX}`,\n headers: { Authorization: `Bearer ${apiKey}` },\n };\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport { type PostHogConfig } from 'posthog-js';\n\nimport { type Config, getEnvString } from '@dxos/config';\nimport { log } from '@dxos/log';\nimport { type IdbLogStore } from '@dxos/log-store-idb';\nimport { gzip, isNode } from '@dxos/util';\n\nimport * as ObservabilityExtension from '../../ObservabilityExtension.ts';\nimport { DXOS_VERSION } from '../../version.ts';\nimport { stubExtension } from '../stub.ts';\nimport { dropBenignExceptions } from './before-send.ts';\nimport {\n AI_GENERATION_EVENT,\n AI_SPAN_EVENT,\n AI_TRACE_EVENT,\n toAiGenerationProperties,\n toAiSpanProperties,\n toAiTraceProperties,\n} from './llm-analytics.ts';\nimport { otelDestination } from './otel-destination.ts';\n\nconst WIDGET_SESSION_STORAGE_KEY = 'dxos.support.widgetSessionId';\n\nconst widgetSessionId = (): string => {\n try {\n const existing = localStorage.getItem(WIDGET_SESSION_STORAGE_KEY);\n if (existing) {\n return existing;\n }\n const minted = crypto.randomUUID();\n localStorage.setItem(WIDGET_SESSION_STORAGE_KEY, minted);\n return minted;\n } catch {\n return crypto.randomUUID();\n }\n};\n\nexport type ExtensionsOptions = {\n config: Config;\n /** Release identifier, e.g. `composer@0.8.3`. */\n release?: string;\n /** Deployment environment, e.g. `production` or `staging`. */\n environment?: string;\n serviceName?: string;\n posthog?: Partial<PostHogConfig>;\n /**\n * Shared persistent log store for debug log dumps.\n * The owning app is expected to register `logStore.processor` with `log` itself —\n * this extension only consumes the buffered logs (via `export()`).\n */\n logStore?: IdbLogStore;\n /**\n * Maximum byte size passed to `logStore.export()` when uploading feedback logs.\n * Should match the upload limit enforced by the server receiving the logs.\n * When omitted the full store is exported without trimming.\n */\n feedbackLogMaxSize?: number;\n /**\n * Where to POST feedback logs, defaulting to {@link DEFAULT_FEEDBACK_LOGS_ENDPOINT}.\n * A native build serves its frontend from its own origin and has no such route there, so it must\n * pass the absolute URL of a deployment that does.\n */\n feedbackLogsEndpoint?: string;\n /**\n * Holds session recording back until this settles, so the recorder's DOM snapshot and compression\n * stay off the main thread during app startup. Ignored when `posthog.disable_session_recording` is set.\n */\n sessionRecordingStart?: Effect.Effect<void>;\n /** What the `posthog-node` transport needs; a browser host has posthog-js and reads none of it. */\n node?: NodeOptions;\n};\n\nexport type NodeOptions = {\n /** Pins the project instead of reading `DX_POSTHOG_API_KEY`. */\n apiKey?: string;\n /** Ingestion host — a region, or a proxy on your own domain. */\n host?: string;\n /** Attribution for events captured before `identify`, since there is no ambient person. */\n distinctId?: string;\n /** Which MCP server this host is, stamped on every `$mcp_*` event. */\n mcpServer?: { name: string; version: string };\n};\n\n/** Same-origin route of the web deployment, which proxies the upload to object storage. */\nconst DEFAULT_FEEDBACK_LOGS_ENDPOINT = '/api/feedback-logs';\n\n/** Upload serialized logs, gzipped, to the feedback-logs endpoint. Returns the R2 key on success. */\nconst uploadLogs = async (\n endpoint: string,\n ndjson: string,\n kind: ObservabilityExtension.SupportUploadKind = 'logs',\n): Promise<string | undefined> => {\n try {\n // The endpoint may be relative (same-origin) or absolute (native builds), so append rather than parse.\n const url = kind === 'logs' ? endpoint : `${endpoint}${endpoint.includes('?') ? '&' : '?'}kind=${kind}`;\n const response = await fetch(url, {\n method: 'POST',\n headers: { 'Content-Type': 'application/gzip' },\n body: await gzip(ndjson),\n });\n if (!response.ok) {\n log.warn('feedback log upload failed', { endpoint, status: response.status });\n return undefined;\n }\n const { key } = await response.json();\n return key;\n } catch (err) {\n log.warn('feedback log upload error', { endpoint, error: err });\n return undefined;\n }\n};\n\n/**\n * Class on an iframe that session replay records with its content. Only for a same-origin iframe the\n * recorder can read: it never releases an opaque one removed with an ancestor, which replays empty anyway.\n */\nexport const REPLAY_IFRAME_CLASS = 'dx-replay-iframe';\n\n/** Create a PostHog-backed observability extension for events, errors, and support tickets. */\nexport const extensions: (options: ExtensionsOptions) => Effect.Effect<ObservabilityExtension.Extension> = Effect.fn(\n function* ({\n config,\n release,\n environment,\n serviceName = 'app',\n posthog: posthogConfig,\n logStore,\n feedbackLogMaxSize,\n feedbackLogsEndpoint = DEFAULT_FEEDBACK_LOGS_ENDPOINT,\n sessionRecordingStart,\n node,\n }) {\n if (isNode()) {\n const { extensions: nodeExtensions } = yield* Effect.promise(() => import('#posthog-transport'));\n return yield* nodeExtensions({ config, release, environment, node });\n }\n if (typeof window === 'undefined') {\n log('PostHog is being stubbed because it is running in a worker.');\n return stubExtension;\n }\n\n const apiKey = getEnvString(config, 'DX_POSTHOG_API_KEY');\n const api_host = getEnvString(config, 'DX_POSTHOG_API_HOST');\n if (!apiKey || !api_host) {\n log.info('Missing POSTHOG_API_KEY or POSTHOG_API_HOST');\n return stubExtension;\n }\n\n const { default: posthog } = yield* Effect.promise(() => import('posthog-js'));\n const { logProcessor } = yield* Effect.promise(() => import('./log-processor.ts'));\n let unregisterPosthogProcessors: (() => void) | undefined;\n const recordingStart = posthogConfig?.disable_session_recording ? undefined : sessionRecordingStart;\n\n return {\n initialize: (context) =>\n Effect.gen(function* () {\n // https://posthog.com/docs/libraries/js/config\n posthog.init(apiKey, {\n api_host,\n mask_all_text: true,\n capture_exceptions: true,\n // Cookies stay scoped to the exact host; the cross-subdomain dmn_chk_* probe fails on public-suffix hosts (e.g. *.pages.dev) and spams the console.\n cross_subdomain_cookie: false,\n ...posthogConfig,\n session_recording: {\n // The recorder never releases a cross-origin iframe removed with an ancestor, and that\n // iframe pins the whole detached tree; children of a blocked element are never recorded.\n // `<body>` is exempt, or a hidden iframe appended to it would blank the whole replay.\n blockSelector: `:not(body):has(> iframe:not(.${REPLAY_IFRAME_CLASS}))`,\n ...posthogConfig?.session_recording,\n },\n before_send: [dropBenignExceptions, ...[posthogConfig?.before_send ?? []].flat()],\n ...(recordingStart ? { disable_session_recording: true } : {}),\n });\n if (recordingStart) {\n // Without an override this only lifts the local block; remote replay settings and sampling still apply.\n yield* Effect.forkDetach(\n recordingStart.pipe(Effect.andThen(Effect.sync(() => posthog.startSessionRecording()))),\n );\n }\n posthog.register({\n sdkVersion: DXOS_VERSION,\n ...(release ? { release } : {}),\n ...(environment ? { environment } : {}),\n });\n unregisterPosthogProcessors?.();\n const removePosthogLog = log.addProcessor(logProcessor);\n // PostHog joins a log record to its session replay on a log attribute literally named\n // `sessionId` — not the OTel `session.id` resource attribute — and rotates that id.\n const tagSession = () => context.setTags({ sessionId: posthog.get_session_id() }, 'logs');\n const removeSessionListener = posthog.onSessionId(tagSession);\n tagSession();\n unregisterPosthogProcessors = () => {\n removePosthogLog();\n removeSessionListener();\n };\n }),\n close: () =>\n Effect.sync(() => {\n unregisterPosthogProcessors?.();\n unregisterPosthogProcessors = undefined;\n }),\n enable: () => Effect.sync(() => posthog.opt_in_capturing()),\n disable: () => Effect.sync(() => posthog.opt_out_capturing()),\n identify: (distinctId, attributes, setOnceAttributes) => {\n posthog.identify(distinctId, attributes, setOnceAttributes);\n },\n alias: (distinctId, previousId) => {\n posthog.alias(distinctId, previousId);\n },\n setTags: (tags) => {\n posthog.register_for_session(tags);\n },\n get enabled(): boolean {\n return posthog.is_capturing();\n },\n apis: [\n {\n kind: 'events',\n isAvailable: () => Effect.succeed(true),\n captureEvent: (event, attributes) => {\n posthog.capture(event, attributes);\n },\n },\n {\n kind: 'errors',\n isAvailable: () => Effect.succeed(true),\n captureException: (error, attributes) => {\n posthog.captureException(error, attributes);\n },\n },\n {\n kind: 'ai',\n isAvailable: () => Effect.succeed(true),\n captureInference: (inference) => {\n posthog.capture(AI_GENERATION_EVENT, toAiGenerationProperties(inference));\n },\n captureTurn: (turn) => {\n posthog.capture(AI_TRACE_EVENT, toAiTraceProperties(turn));\n },\n captureToolCall: (toolCall) => {\n posthog.capture(AI_SPAN_EVENT, toAiSpanProperties(toolCall));\n },\n },\n {\n kind: 'support',\n isAvailable: () => Effect.succeed(logStore !== undefined),\n uploadLogs: async () => {\n if (logStore === undefined) {\n return undefined;\n }\n const ndjson = await logStore.export({ maxSize: feedbackLogMaxSize });\n if (ndjson.length === 0) {\n return undefined;\n }\n return await uploadLogs(feedbackLogsEndpoint, ndjson);\n },\n uploadNdjson: async (ndjson, kind) =>\n ndjson.length === 0 ? undefined : await uploadLogs(feedbackLogsEndpoint, ndjson, kind),\n sessionContext: () => {\n if (!posthog.__loaded) {\n return undefined;\n }\n try {\n return {\n distinctId: posthog.get_distinct_id(),\n widgetSessionId: widgetSessionId(),\n sessionId: posthog.get_session_id(),\n replayUrl: posthog.get_session_replay_url({ withTimestamp: true, timestampLookBack: 30 }),\n currentUrl: `${window.location.origin}${window.location.pathname}`,\n };\n } catch (err) {\n log.warn('PostHog session context unavailable', { err });\n return undefined;\n }\n },\n flushLogs: async (attributes) => {\n const destination = otelDestination(config);\n if (!destination) {\n return;\n }\n const ndjson = await logStore?.export({ maxSize: feedbackLogMaxSize });\n if (!ndjson || ndjson.length === 0) {\n return;\n }\n const { flushSupportLogs } = await import('./support-logs.ts');\n const count = await flushSupportLogs(ndjson, {\n destination,\n resourceAttributes: {\n 'service.name': serviceName,\n ...(release ? { 'service.version': release } : {}),\n ...(environment ? { 'deployment.environment': environment } : {}),\n },\n attributes,\n });\n log.info('support logs flushed to PostHog', { ...attributes, count });\n },\n },\n ],\n };\n },\n);\n","//\n// Copyright 2025 DXOS.org\n//\n\nexport * from './llm-analytics.ts';\nexport * from './extension.ts';\nexport * from './otel-destination.ts';\n","//\n// Copyright 2025 DXOS.org\n//\n\n// @import-as-namespace\n\nimport type * as Effect from 'effect/Effect';\n\nimport { type CleanupFn } from '@dxos/async';\nimport { type MetricObserver } from '@dxos/tracing';\n\nexport * from './extensions/index.ts';\n\n/**\n * Kind of observability extension.\n *\n * - errors: Error tracking (e.g., PostHog)\n * - events: Product usage event tracking (e.g., PostHog)\n * - support: Support tickets anchoring user reports and telemetry (e.g., PostHog)\n * - ai: Model inferences, tool calls, and turns (e.g., PostHog LLM analytics, OTel gen_ai)\n * - logs: Structured logging (e.g., OTEL)\n * - mcp: MCP server sessions and tool calls (e.g., PostHog)\n * - metrics: Metric data (e.g., OTEL)\n * - traces: Distributed tracing (e.g., OTEL)\n */\nexport type Kind = 'ai' | 'errors' | 'events' | 'support' | 'logs' | 'mcp' | 'metrics' | 'traces';\n\n/**\n * Base for every extension API variant. All kinds implement availability the same way.\n */\nexport type ExtensionApiBase<K extends Kind = Kind> = {\n kind: K;\n isAvailable(): Effect.Effect<boolean>;\n};\n\n/**\n * Instrument-level metadata, declared once by the first caller for a given metric name.\n * Units must be UCUM as OTel requires — `s`, `By`, `{thing}`.\n */\nexport type MetricMeta = { unit?: string; description?: string };\n\n/**\n * Metrics extension API (kind-specific methods only).\n */\nexport type Metrics = {\n gauge(name: string, value: number, tags?: Attributes, meta?: MetricMeta): void;\n increment(name: string, value?: number, tags?: Attributes, meta?: MetricMeta): void;\n distribution(name: string, value: number, tags?: Attributes, meta?: MetricMeta): void;\n /**\n * Registers a callback read once per export interval.\n * Prefer this over {@link Metrics.gauge} for any \"current value\" metric, since a pushed gauge\n * only lands in the export windows its producer happens to tick in.\n */\n observe(name: string, callback: MetricObserver, tags?: Attributes, meta?: MetricMeta): CleanupFn;\n};\n\n/**\n * Errors extension API (kind-specific methods only).\n */\nexport type Errors = {\n captureException(error: Error, attributes?: Attributes): void;\n};\n\n/**\n * Wider than {@link Attributes}: event properties carry structured values (message arrays,\n * nested objects), not just scalars.\n */\nexport type EventAttributes = Record<string, unknown>;\n\n/**\n * Events extension API (kind-specific methods only).\n */\nexport type Events = {\n captureEvent(event: string, attributes?: EventAttributes): void;\n};\n\n/**\n * One model call, already filtered by the capture policy and scrubbed (see `AiObservability`). Shaped\n * after the OTel GenAI conventions rather than any vendor's schema — an extension maps it onto\n * whatever its backend calls these things.\n */\nexport type Inference = {\n traceId: string;\n spanId: string;\n parentSpanId?: string;\n /** Provider span name, e.g. `LanguageModel.streamText`. */\n spanName: string;\n /** `gen_ai.provider.name` — the serving product, not the wire dialect. */\n provider?: string;\n model?: string;\n /** Conversation the call belongs to, when the call site named one. */\n sessionId?: string;\n /** Request parameters the provider reported (temperature, max_tokens, …). */\n parameters?: Record<string, unknown>;\n /** Excludes tokens served from the prompt cache; add {@link cacheReadTokens} for the total. */\n inputTokens?: number;\n outputTokens?: number;\n /** Absent rather than zero when the provider does not report prompt caching. */\n cacheReadTokens?: number;\n cacheWriteTokens?: number;\n /** Wall-clock duration, in seconds. */\n latency: number;\n streaming: boolean;\n /** Absent entirely when the capture policy denied it. */\n content?: GenerationContent;\n /** Exception class name only — a provider's message can embed request or response fragments. */\n errorClass?: string;\n};\n\nexport type GenerationContent = {\n /** Parsed prompt messages, or the raw string when {@link truncated} left it unparseable. */\n input?: unknown;\n output?: unknown;\n tools?: unknown;\n /** Some field above was cut to fit, so it is a fragment rather than the whole. */\n truncated?: boolean;\n};\n\n/** Fields a turn and a tool call share with an inference: identity, timing, and gated content. */\nexport type AiSpanBase = {\n traceId: string;\n spanId: string;\n parentSpanId?: string;\n /** For a tool call, the tool's name rather than the span's. */\n spanName: string;\n /** Conversation the span belongs to, when the call site named one. */\n sessionId?: string;\n /** Wall-clock duration, in seconds. */\n latency: number;\n /** Absent entirely when the capture policy denied it. */\n content?: AiSpanContent;\n /** Exception class name only, as on {@link Inference}. */\n errorClass?: string;\n};\n\nexport type AiSpanContent = {\n input?: unknown;\n output?: unknown;\n /** Some field above was cut to fit, so it is a fragment rather than the whole. */\n truncated?: boolean;\n};\n\n/**\n * One conversation turn: the unit a backend groups a turn's model calls and tool calls under (what\n * PostHog calls a trace). Its input is the user prompt and its output the messages the turn produced.\n */\nexport type Turn = AiSpanBase;\n\n/** One tool call inside a turn (what PostHog calls a span), with the arguments and the result. */\nexport type ToolCall = AiSpanBase;\n\n/**\n * AI extension API (kind-specific methods only).\n */\nexport type Ai = {\n captureInference(inference: Inference): void;\n captureTurn(turn: Turn): void;\n captureToolCall(toolCall: ToolCall): void;\n};\n\n/** The client name an MCP event carries when no request named its client; `@posthog/mcp` drops a falsy one. */\nexport const UNKNOWN_MCP_CLIENT = 'unknown';\n\n/** What every MCP event carries: the session it groups under and the client it came from. */\nexport type McpSession = {\n /** Groups the session's events; one server process is one session over a stdio transport. */\n sessionId: string;\n clientName?: string;\n clientVersion?: string;\n protocolVersion?: string;\n};\n\n/** MCP extension API (kind-specific methods only). */\nexport type Mcp = {\n /** A client connected successfully. */\n captureInitialize(session: McpSession): void;\n captureToolCall(\n call: McpSession & { toolName: string; parameters?: unknown; durationMs: number; isError: boolean },\n ): void;\n};\n\n/**\n * Support extension API (kind-specific methods only).\n */\nexport type SupportLogTags = { ticketId: string } | { reportId: string };\n\n/** What an uploaded NDJSON bundle is, which decides where it is filed in storage. */\nexport type SupportUploadKind = 'logs' | 'trajectory';\n\nexport type Support = {\n uploadLogs(): Promise<string | undefined>;\n /** Uploads an NDJSON bundle the caller built (e.g. an agent trajectory), returning its storage key. */\n uploadNdjson(ndjson: string, kind: SupportUploadKind): Promise<string | undefined>;\n sessionContext(): SupportSessionContext | undefined;\n flushLogs(attributes: SupportLogTags): Promise<void>;\n};\n\nexport type ExtensionApi =\n | (ExtensionApiBase<'errors'> & Errors)\n | (ExtensionApiBase<'events'> & Events)\n | (ExtensionApiBase<'support'> & Support)\n | (ExtensionApiBase<'ai'> & Ai)\n // TODO(wittjosiah): Direct logs api?\n | (ExtensionApiBase<'mcp'> & Mcp)\n | ExtensionApiBase<'logs'>\n | (ExtensionApiBase<'metrics'> & Metrics)\n // TODO(wittjosiah): Direct traces api?\n | ExtensionApiBase<'traces'>;\n\n/**\n * What a browser knows about its own telemetry session. Shaped after what posthog-js's own widget\n * sends.\n */\nexport type SupportSessionContext = {\n distinctId: string;\n /** Browser-minted id the widget API uses for access control on anonymous tickets. */\n widgetSessionId: string;\n sessionId?: string;\n replayUrl?: string;\n currentUrl?: string;\n};\n\n/**\n * Attributes to be attached to observability events.\n */\nexport type Attributes = Record<string, string | number | boolean | undefined>;\n\nexport type ExtensionContext = {\n setTags(tags: Attributes, kind?: Kind): void;\n};\n\n/**\n * Implementation of an observability extension API.\n */\nexport type Extension = {\n initialize?(context: ExtensionContext): Effect.Effect<void, Error>;\n close?(): Effect.Effect<void>;\n enable?(): Effect.Effect<void>;\n disable?(): Effect.Effect<void>;\n flush?(): Effect.Effect<void>;\n identify?(distinctId: string, attributes?: Attributes, setOnceAttributes?: Attributes): void;\n alias?(distinctId: string, previousId?: string): void;\n setTags?(tags: Record<string, string>, kind?: Kind): void;\n enabled: boolean;\n apis: ExtensionApi[];\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chunk-ObservabilityProvider.mjs","names":[],"sources":["../../src/providers/event-loop-lag.ts","../../src/providers/ip-data.ts","../../src/providers/object-events.ts","../../src/providers/space-events.ts","../../src/providers/storage.ts","../../src/providers/ObservabilityProvider.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { scheduleTaskInterval } from '@dxos/async';\nimport { Context } from '@dxos/context';\nimport { log } from '@dxos/log';\n\nimport type * as Observability from '../Observability.ts';\n\nconst SECONDS = { unit: 's' } as const;\n\n/** How often the loop is probed. Short enough to catch a stall inside one export window. */\nexport const LAG_SAMPLE_INTERVAL_MS = 500;\n\n/** Window the reported peak covers. Matches the metric export interval. */\nexport const LAG_WINDOW_MS = 60_000;\n\n/**\n * Gap beyond which a late probe is read as the realm having been suspended rather than blocked.\n *\n * Browsers throttle or freeze timers in a hidden tab — and in the workers that tab owns — and the\n * machine can sleep, so an arbitrarily long gap is always possible for reasons that are not jank.\n * 10s because a main thread genuinely blocked that long means the app is hung rather than janky:\n * past this point the distinction stops paying, and a false reading is worse than a missed one.\n */\nexport const MAX_PLAUSIBLE_LAG_MS = 10_000;\n\n/**\n * Tracks how far behind schedule a timer fires, which is how long the realm's event loop was busy.\n *\n * Reports the peak of the last completed window rather than an instantaneous value: a gauge read\n * only at collection time can never observe a stall, because a blocked loop delays the collection\n * callback itself.\n *\n * The window is rotated by {@link rotate} rather than by the read, so {@link peakMs} is a plain\n * idempotent getter. A read that consumed the value would break as soon as it ran more than once\n * per window — which `RemoteMetrics` fan-out to a second processor, or a `flush()` landing next to\n * a periodic collection, would both cause.\n */\nexport class EventLoopLagTracker {\n #maxInWindowMs = 0;\n #peakMs = 0;\n #lastSampleAt: number | undefined;\n\n constructor(\n private readonly _intervalMs: number = LAG_SAMPLE_INTERVAL_MS,\n private readonly _maxPlausibleMs: number = MAX_PLAUSIBLE_LAG_MS,\n ) {}\n\n /**\n * Discards the reference timestamp so the next probe cannot report lag.\n * Called when the realm was legitimately not running — a hidden tab has its timers clamped to\n * once per second or worse, which would otherwise read as tens of seconds of event-loop lag.\n */\n suspend(): void {\n this.#lastSampleAt = undefined;\n }\n\n /**\n * Records one probe. Lag is elapsed time beyond the scheduled interval.\n *\n * A gap past {@link MAX_PLAUSIBLE_LAG_MS} is discarded, not clamped: clamping would still report\n * a ceiling-valued stall that never happened. This has to hold without help from a visibility\n * listener, because when timers are frozen outright **no probe fires during the freeze** — so\n * nothing can mark the suspension before the first probe after it observes the entire gap.\n */\n sample(now: number): void {\n if (this.#lastSampleAt !== undefined) {\n const lagMs = Math.max(0, now - this.#lastSampleAt - this._intervalMs);\n if (lagMs > this._maxPlausibleMs) {\n log('discarding implausible event loop lag', { lagMs, maxPlausibleMs: this._maxPlausibleMs });\n } else {\n this.#maxInWindowMs = Math.max(this.#maxInWindowMs, lagMs);\n }\n }\n this.#lastSampleAt = now;\n }\n\n /** Peak lag observed during the last completed window. Safe to read any number of times. */\n get peakMs(): number {\n return this.#peakMs;\n }\n\n /** Closes the current window, publishing its peak. Driven on the export cadence. */\n rotate(): void {\n this.#peakMs = this.#maxInWindowMs;\n this.#maxInWindowMs = 0;\n }\n}\n/**\n * Publishes how long this realm's event loop was blocked.\n *\n * Reports peak lag per export window, tagged only by the `dxos.process.type` resource attribute —\n * so the same provider distinguishes the tab from the shared and dedicated workers without any\n * per-realm wiring.\n */\nexport const eventLoopLagProvider = (): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n const lag = new EventLoopLagTracker(LAG_SAMPLE_INTERVAL_MS);\n\n scheduleTaskInterval(ctx, async () => lag.sample(Date.now()), LAG_SAMPLE_INTERVAL_MS);\n\n // Belt to the tracker's braces. The clamp inside `sample` is what actually guarantees a frozen\n // tab is not reported as lag — checking visibility when the probe fires cannot, since a frozen\n // timer does not fire until the tab is visible again. This listener additionally drops the\n // reference timestamp the moment visibility changes, so a gap under the clamp is discarded too.\n const doc = (globalThis as { document?: EventTarget & { visibilityState?: string } }).document;\n if (doc) {\n const onVisibilityChange = () => lag.suspend();\n doc.addEventListener('visibilitychange', onVisibilityChange);\n ctx.onDispose(() => doc.removeEventListener('visibilitychange', onVisibilityChange));\n }\n\n // Window rotation is driven here rather than by the read, so the gauge callback stays a plain\n // idempotent getter — see EventLoopLagTracker.\n scheduleTaskInterval(ctx, async () => lag.rotate(), LAG_WINDOW_MS);\n\n ctx.onDispose(\n observability.metrics.observe('dxos.client.runtime.eventLoop.lag', () => lag.peakMs / 1_000, undefined, SECONDS),\n );\n\n return async () => {\n await ctx.dispose();\n };\n });\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport * as FetchHttpClient from 'effect/http/FetchHttpClient';\nimport * as HttpClient from 'effect/http/HttpClient';\nimport * as HttpClientRequest from 'effect/http/HttpClientRequest';\nimport * as Schema from 'effect/Schema';\n// NOTE: localStorage is not available in web workers.\nimport * as localForage from 'localforage';\n\nimport { type Config } from '@dxos/config';\nimport { log } from '@dxos/log';\n\nimport * as Observability from '../Observability.ts';\n\nconst IP_DATA_CACHE_TIMEOUT = 6 * 60 * 60 * 1000; // 6 hours\n\n// ipdata.co v1 response — city/region/latitude/longitude are nullable for some IPs (VPNs, CDNs, etc.),\n// and the country field is named `country_name`, not `country`.\nconst IPData = Schema.Struct({\n city: Schema.NullOr(Schema.String),\n region: Schema.NullOr(Schema.String),\n country_name: Schema.String,\n latitude: Schema.NullOr(Schema.Number),\n longitude: Schema.NullOr(Schema.Number),\n});\ntype IPData = Schema.Schema.Type<typeof IPData>;\n\ntype CachedIPData = {\n data: IPData;\n timestamp: number;\n};\n\nconst getIPData = Effect.fn(function* (config: Config) {\n const httpClient = yield* HttpClient.HttpClient;\n\n // Disable tracing to avoid CORS errors from traceparent header on cross-origin requests.\n // v4 dropped the combinator; the predicate is a context reference provided around the response.\n const httpClientNoTrace = httpClient.pipe(\n HttpClient.transformResponse(Effect.provideService(HttpClient.TracerDisabledWhen, () => true)),\n );\n\n // Check cache first.\n // v2 key discards entries cached by the previous schema (which used `country` instead of `country_name`).\n const cachedData = yield* Effect.promise(() => localForage.getItem<CachedIPData>('dxos:observability:ipdata:v2'));\n if (cachedData && cachedData.timestamp > Date.now() - IP_DATA_CACHE_TIMEOUT) {\n return cachedData.data;\n }\n\n // Fetch data if not cached.\n const IPDATA_API_KEY = config.get('runtime.app.env.DX_IPDATA_API_KEY');\n if (!IPDATA_API_KEY) {\n log.warn('DX_IPDATA_API_KEY is not configured; IP geolocation tags will be absent from telemetry');\n return cachedData?.data;\n }\n\n const data = yield* HttpClientRequest.get(`https://api.ipdata.co?api-key=${IPDATA_API_KEY}`).pipe(\n httpClientNoTrace.execute,\n Effect.flatMap((res) => res.json),\n Effect.flatMap(Schema.decodeUnknownEffect(IPData)),\n // On failure fall back to stale cache rather than emitting no tags.\n Effect.catch((err) =>\n Effect.sync(() => {\n log.warn('ipdata fetch failed; IP geolocation tags will be absent or stale', { err });\n return cachedData?.data;\n }),\n ),\n );\n\n if (data) {\n yield* Effect.promise(() => localForage.setItem('dxos:observability:ipdata:v2', { data, timestamp: Date.now() }));\n }\n\n return data;\n});\n\n/** Fetches IP geolocation data and sets city/region/country tags on the observability instance. */\nexport const provider =\n (config: Config): Observability.DataProvider =>\n (observability) =>\n Effect.gen(function* () {\n const ipData = yield* getIPData(config);\n if (!ipData) {\n return;\n }\n\n observability.setTags({\n ...(ipData.city != null && { city: ipData.city }),\n ...(ipData.region != null && { region: ipData.region }),\n country: ipData.country_name,\n ...(ipData.latitude != null && { latitude: ipData.latitude }),\n ...(ipData.longitude != null && { longitude: ipData.longitude }),\n });\n }).pipe(\n Effect.provide(FetchHttpClient.layer),\n Effect.catch((err) =>\n Effect.sync(() => {\n log.warn('ipdata provider failed', { err });\n }),\n ),\n );\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Exit from 'effect/Exit';\nimport * as FiberMap from 'effect/FiberMap';\nimport * as Queue from 'effect/Queue';\nimport * as Scope from 'effect/Scope';\nimport * as Stream from 'effect/Stream';\n\nimport * as Database from '@dxos/echo/Database';\nimport { log } from '@dxos/log';\nimport { type EventAttributes, type RemoteEvents, TRACE_PROCESSOR } from '@dxos/tracing';\n\nimport type * as Observability from '../Observability.ts';\nimport type * as ObservabilityExtension from '../ObservabilityExtension.ts';\n\n/** The product events ECHO's trace events are reported as. */\nexport const EVENTS = {\n objectAdd: 'space.object.add',\n objectRemove: 'space.object.remove',\n relationAdd: 'space.relation.add',\n relationRemove: 'space.relation.remove',\n typeAdd: 'space.type.add',\n feedAppend: 'space.feed.append',\n} as const;\n\n/**\n * How long a new object must survive before it counts as created: the create dialog adds a draft when it\n * opens and removes it if the user cancels.\n */\nexport const DRAFT_WINDOW = Duration.seconds(30);\n\ntype TraceEvent = { name: string; attributes: EventAttributes };\n\n/**\n * Reports ECHO's trace events as product events: user-facing objects and every relation added or removed, types\n * persisted, and items appended to feeds. Every write is reported with its `origin` (`user`, `system` or `unknown`),\n * so a dashboard separates people's actions from the rest. An add that is removed within the draft window\n * reports neither. Events arrive only while registered; ECHO does not replay them.\n */\nexport const listen = (\n events: RemoteEvents,\n capture: ObservabilityExtension.Events['captureEvent'],\n draftWindow: Duration.Input = DRAFT_WINDOW,\n): Effect.Effect<void, never, Scope.Scope> =>\n Effect.gen(function* () {\n const queue = yield* Queue.unbounded<TraceEvent>();\n const processor = {\n emit: (name: string, attributes: EventAttributes) => Queue.offerUnsafe(queue, { name, attributes }),\n };\n yield* Effect.acquireRelease(\n Effect.sync(() => events.registerProcessor(processor)),\n () => Effect.sync(() => events.unregisterProcessor(processor)),\n );\n\n const send = (event: string, properties: Record<string, unknown>) =>\n Effect.try(() => capture(event, properties)).pipe(Effect.catch((error) => Effect.sync(() => log.catch(error))));\n\n // Keyed by space and object so removing a draft interrupts its own pending report and no other space's.\n const pending = yield* FiberMap.make<string>();\n yield* Stream.fromQueue(queue).pipe(\n Stream.runForEach(({ name, attributes }) =>\n Effect.gen(function* () {\n const { spaceId, objectId, typename, relation, userType, origin } = attributes;\n const draftKey = `${String(spaceId)}/${String(objectId)}`;\n switch (name) {\n case Database.TraceEvents.objectAdd: {\n if (typeof objectId !== 'string' || (relation !== true && userType !== true)) {\n return;\n }\n const event = relation === true ? EVENTS.relationAdd : EVENTS.objectAdd;\n yield* FiberMap.run(\n pending,\n draftKey,\n Effect.sleep(draftWindow).pipe(Effect.andThen(send(event, { spaceId, objectId, typename, origin }))),\n );\n return;\n }\n\n case Database.TraceEvents.objectRemove: {\n if (typeof objectId !== 'string') {\n return;\n }\n if (yield* FiberMap.has(pending, draftKey)) {\n yield* FiberMap.remove(pending, draftKey);\n return;\n }\n if (relation === true || userType === true) {\n const event = relation === true ? EVENTS.relationRemove : EVENTS.objectRemove;\n yield* send(event, { spaceId, objectId, typename, origin });\n }\n return;\n }\n\n case Database.TraceEvents.typeAdd: {\n yield* send(EVENTS.typeAdd, { spaceId, typename, version: attributes.version, origin });\n return;\n }\n\n case Database.TraceEvents.feedAppend: {\n yield* send(EVENTS.feedAppend, { spaceId, feedId: attributes.feedId, objectId, typename, origin });\n return;\n }\n }\n }),\n ),\n Effect.forkScoped,\n );\n });\n\n/**\n * Reports what is written in this realm's spaces, however it was written: from a dialog, an agent, a sync, or a raw\n * `db.add` in a view.\n */\nexport const provider: Observability.DataProvider = Effect.fn(function* (observability) {\n const scope = yield* Scope.make();\n yield* listen(TRACE_PROCESSOR.remoteEvents, (event, attributes) =>\n observability.events.captureEvent(event, attributes),\n ).pipe(Scope.provide(scope));\n return () => {\n Effect.runFork(Scope.close(scope, Exit.void));\n };\n});\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { ClientTraceEvents } from '@dxos/client-protocol';\nimport { log } from '@dxos/log';\nimport { type EventAttributes, type RemoteEvents, TRACE_PROCESSOR } from '@dxos/tracing';\n\nimport type * as Observability from '../Observability.ts';\nimport type * as ObservabilityExtension from '../ObservabilityExtension.ts';\n\n/** The product events the client's trace events are reported as, by invitation kind. */\nexport const EVENTS = {\n spaceCreate: 'space.create',\n spaceShare: 'space.share',\n spaceJoin: 'space.join',\n spaceAdmit: 'space.admit',\n deviceInvite: 'identity.device.invite',\n deviceJoin: 'identity.join',\n deviceAdmit: 'identity.device.admit',\n} as const;\n\ntype ProductEvent = { name: string; properties: Record<string, unknown> };\n\n/** Maps one of the client's trace events to the product event it stands for, if any. */\nexport const toProductEvent = (name: string, attributes: EventAttributes): ProductEvent | undefined => {\n const { spaceId, kind, authMethod, multiUse, origin } = attributes;\n const device = kind === 'device';\n switch (name) {\n case ClientTraceEvents.spaceCreate:\n return { name: EVENTS.spaceCreate, properties: { spaceId, origin } };\n case ClientTraceEvents.invitationCreate:\n return device\n ? { name: EVENTS.deviceInvite, properties: { authMethod, multiUse } }\n : { name: EVENTS.spaceShare, properties: { spaceId, authMethod, multiUse } };\n case ClientTraceEvents.invitationAccept:\n return device ? { name: EVENTS.deviceJoin, properties: {} } : { name: EVENTS.spaceJoin, properties: { spaceId } };\n case ClientTraceEvents.invitationAdmit:\n return device\n ? { name: EVENTS.deviceAdmit, properties: { multiUse } }\n : { name: EVENTS.spaceAdmit, properties: { spaceId, multiUse } };\n default:\n return undefined;\n }\n};\n\n/** Reports the client's space and invitation trace events as product events until the returned cleanup runs. */\nexport const listen = (events: RemoteEvents, capture: ObservabilityExtension.Events['captureEvent']): (() => void) => {\n const processor = {\n emit: (name: string, attributes: EventAttributes) => {\n const event = toProductEvent(name, attributes);\n if (event === undefined) {\n return;\n }\n try {\n capture(event.name, event.properties);\n } catch (error) {\n log.catch(error);\n }\n },\n };\n events.registerProcessor(processor);\n return () => events.unregisterProcessor(processor);\n};\n\n/**\n * Reports spaces this realm creates and invitations it shares, accepts and admits guests through, however they\n * were started: from a dialog, an operation, an agent or a deep link.\n */\nexport const provider: Observability.DataProvider = (observability) =>\n Effect.sync(() =>\n listen(TRACE_PROCESSOR.remoteEvents, (event, attributes) => observability.events.captureEvent(event, attributes)),\n );\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Fiber from 'effect/Fiber';\nimport * as Schedule from 'effect/Schedule';\n\nimport * as Observability from '../Observability.ts';\n\nexport const provider: Observability.DataProvider = Effect.fn(function* (observability) {\n if (typeof navigator !== 'undefined' && navigator.storage?.estimate) {\n const action = Effect.gen(function* () {\n const storageEstimate = yield* Effect.tryPromise(() => navigator.storage.estimate());\n storageEstimate.usage && observability.metrics.gauge('storageUsage', storageEstimate.usage);\n storageEstimate.quota && observability.metrics.gauge('storageQuota', storageEstimate.quota);\n });\n\n const fiber = action.pipe(Effect.repeat(Schedule.fixed(Duration.hours(1))), Effect.runFork);\n return () => Effect.runSync(Fiber.interrupt(fiber));\n }\n});\n","//\n// Copyright 2025 DXOS.org\n//\n\n// @import-as-namespace\n\nexport * as EventLoopLag from './event-loop-lag.ts';\nexport * as IPData from './ip-data.ts';\nexport * as Memory from './memory.ts';\nexport * as ObjectEvents from './object-events.ts';\nexport * as SpaceEvents from './space-events.ts';\nexport * as Storage from './storage.ts';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chunk-ObservabilityProvider.mjs","names":["__dxlog_file","__dxlog_file","provider","__dxlog_file","EVENTS","listen","provider","provider"],"sources":["../../src/providers/event-loop-lag.ts","../../src/providers/ip-data.ts","../../src/providers/object-events.ts","../../src/providers/space-events.ts","../../src/providers/storage.ts","../../src/providers/ObservabilityProvider.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { scheduleTaskInterval } from '@dxos/async';\nimport { Context } from '@dxos/context';\nimport { log } from '@dxos/log';\n\nimport type * as Observability from '../Observability.ts';\n\nconst SECONDS = { unit: 's' } as const;\n\n/** How often the loop is probed. Short enough to catch a stall inside one export window. */\nexport const LAG_SAMPLE_INTERVAL_MS = 500;\n\n/** Window the reported peak covers. Matches the metric export interval. */\nexport const LAG_WINDOW_MS = 60_000;\n\n/**\n * Gap beyond which a late probe is read as the realm having been suspended rather than blocked.\n *\n * Browsers throttle or freeze timers in a hidden tab — and in the workers that tab owns — and the\n * machine can sleep, so an arbitrarily long gap is always possible for reasons that are not jank.\n * 10s because a main thread genuinely blocked that long means the app is hung rather than janky:\n * past this point the distinction stops paying, and a false reading is worse than a missed one.\n */\nexport const MAX_PLAUSIBLE_LAG_MS = 10_000;\n\n/**\n * Tracks how far behind schedule a timer fires, which is how long the realm's event loop was busy.\n *\n * Reports the peak of the last completed window rather than an instantaneous value: a gauge read\n * only at collection time can never observe a stall, because a blocked loop delays the collection\n * callback itself.\n *\n * The window is rotated by {@link rotate} rather than by the read, so {@link peakMs} is a plain\n * idempotent getter. A read that consumed the value would break as soon as it ran more than once\n * per window — which `RemoteMetrics` fan-out to a second processor, or a `flush()` landing next to\n * a periodic collection, would both cause.\n */\nexport class EventLoopLagTracker {\n #maxInWindowMs = 0;\n #peakMs = 0;\n #lastSampleAt: number | undefined;\n\n constructor(\n private readonly _intervalMs: number = LAG_SAMPLE_INTERVAL_MS,\n private readonly _maxPlausibleMs: number = MAX_PLAUSIBLE_LAG_MS,\n ) {}\n\n /**\n * Discards the reference timestamp so the next probe cannot report lag.\n * Called when the realm was legitimately not running — a hidden tab has its timers clamped to\n * once per second or worse, which would otherwise read as tens of seconds of event-loop lag.\n */\n suspend(): void {\n this.#lastSampleAt = undefined;\n }\n\n /**\n * Records one probe. Lag is elapsed time beyond the scheduled interval.\n *\n * A gap past {@link MAX_PLAUSIBLE_LAG_MS} is discarded, not clamped: clamping would still report\n * a ceiling-valued stall that never happened. This has to hold without help from a visibility\n * listener, because when timers are frozen outright **no probe fires during the freeze** — so\n * nothing can mark the suspension before the first probe after it observes the entire gap.\n */\n sample(now: number): void {\n if (this.#lastSampleAt !== undefined) {\n const lagMs = Math.max(0, now - this.#lastSampleAt - this._intervalMs);\n if (lagMs > this._maxPlausibleMs) {\n log('discarding implausible event loop lag', { lagMs, maxPlausibleMs: this._maxPlausibleMs });\n } else {\n this.#maxInWindowMs = Math.max(this.#maxInWindowMs, lagMs);\n }\n }\n this.#lastSampleAt = now;\n }\n\n /** Peak lag observed during the last completed window. Safe to read any number of times. */\n get peakMs(): number {\n return this.#peakMs;\n }\n\n /** Closes the current window, publishing its peak. Driven on the export cadence. */\n rotate(): void {\n this.#peakMs = this.#maxInWindowMs;\n this.#maxInWindowMs = 0;\n }\n}\n/**\n * Publishes how long this realm's event loop was blocked.\n *\n * Reports peak lag per export window, tagged only by the `dxos.process.type` resource attribute —\n * so the same provider distinguishes the tab from the shared and dedicated workers without any\n * per-realm wiring.\n */\nexport const eventLoopLagProvider = (): Observability.DataProvider =>\n Effect.fn(function* (observability) {\n const ctx = new Context();\n const lag = new EventLoopLagTracker(LAG_SAMPLE_INTERVAL_MS);\n\n scheduleTaskInterval(ctx, async () => lag.sample(Date.now()), LAG_SAMPLE_INTERVAL_MS);\n\n // Belt to the tracker's braces. The clamp inside `sample` is what actually guarantees a frozen\n // tab is not reported as lag — checking visibility when the probe fires cannot, since a frozen\n // timer does not fire until the tab is visible again. This listener additionally drops the\n // reference timestamp the moment visibility changes, so a gap under the clamp is discarded too.\n const doc = (globalThis as { document?: EventTarget & { visibilityState?: string } }).document;\n if (doc) {\n const onVisibilityChange = () => lag.suspend();\n doc.addEventListener('visibilitychange', onVisibilityChange);\n ctx.onDispose(() => doc.removeEventListener('visibilitychange', onVisibilityChange));\n }\n\n // Window rotation is driven here rather than by the read, so the gauge callback stays a plain\n // idempotent getter — see EventLoopLagTracker.\n scheduleTaskInterval(ctx, async () => lag.rotate(), LAG_WINDOW_MS);\n\n ctx.onDispose(\n observability.metrics.observe('dxos.client.runtime.eventLoop.lag', () => lag.peakMs / 1_000, undefined, SECONDS),\n );\n\n return async () => {\n await ctx.dispose();\n };\n });\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\nimport * as FetchHttpClient from 'effect/http/FetchHttpClient';\nimport * as HttpClient from 'effect/http/HttpClient';\nimport * as HttpClientRequest from 'effect/http/HttpClientRequest';\nimport * as Schema from 'effect/Schema';\n// NOTE: localStorage is not available in web workers.\nimport * as localForage from 'localforage';\n\nimport { type Config } from '@dxos/config';\nimport { log } from '@dxos/log';\n\nimport * as Observability from '../Observability.ts';\n\nconst IP_DATA_CACHE_TIMEOUT = 6 * 60 * 60 * 1000; // 6 hours\n\n// ipdata.co v1 response — city/region/latitude/longitude are nullable for some IPs (VPNs, CDNs, etc.),\n// and the country field is named `country_name`, not `country`.\nconst IPData = Schema.Struct({\n city: Schema.NullOr(Schema.String),\n region: Schema.NullOr(Schema.String),\n country_name: Schema.String,\n latitude: Schema.NullOr(Schema.Number),\n longitude: Schema.NullOr(Schema.Number),\n});\ntype IPData = Schema.Schema.Type<typeof IPData>;\n\ntype CachedIPData = {\n data: IPData;\n timestamp: number;\n};\n\nconst getIPData = Effect.fn(function* (config: Config) {\n const httpClient = yield* HttpClient.HttpClient;\n\n // Disable tracing to avoid CORS errors from traceparent header on cross-origin requests.\n // v4 dropped the combinator; the predicate is a context reference provided around the response.\n const httpClientNoTrace = httpClient.pipe(\n HttpClient.transformResponse(Effect.provideService(HttpClient.TracerDisabledWhen, () => true)),\n );\n\n // Check cache first.\n // v2 key discards entries cached by the previous schema (which used `country` instead of `country_name`).\n const cachedData = yield* Effect.promise(() => localForage.getItem<CachedIPData>('dxos:observability:ipdata:v2'));\n if (cachedData && cachedData.timestamp > Date.now() - IP_DATA_CACHE_TIMEOUT) {\n return cachedData.data;\n }\n\n // Fetch data if not cached.\n const IPDATA_API_KEY = config.get('runtime.app.env.DX_IPDATA_API_KEY');\n if (!IPDATA_API_KEY) {\n log.warn('DX_IPDATA_API_KEY is not configured; IP geolocation tags will be absent from telemetry');\n return cachedData?.data;\n }\n\n const data = yield* HttpClientRequest.get(`https://api.ipdata.co?api-key=${IPDATA_API_KEY}`).pipe(\n httpClientNoTrace.execute,\n Effect.flatMap((res) => res.json),\n Effect.flatMap(Schema.decodeUnknownEffect(IPData)),\n // On failure fall back to stale cache rather than emitting no tags.\n Effect.catch((err) =>\n Effect.sync(() => {\n log.warn('ipdata fetch failed; IP geolocation tags will be absent or stale', { err });\n return cachedData?.data;\n }),\n ),\n );\n\n if (data) {\n yield* Effect.promise(() => localForage.setItem('dxos:observability:ipdata:v2', { data, timestamp: Date.now() }));\n }\n\n return data;\n});\n\n/** Fetches IP geolocation data and sets city/region/country tags on the observability instance. */\nexport const provider =\n (config: Config): Observability.DataProvider =>\n (observability) =>\n Effect.gen(function* () {\n const ipData = yield* getIPData(config);\n if (!ipData) {\n return;\n }\n\n observability.setTags({\n ...(ipData.city != null && { city: ipData.city }),\n ...(ipData.region != null && { region: ipData.region }),\n country: ipData.country_name,\n ...(ipData.latitude != null && { latitude: ipData.latitude }),\n ...(ipData.longitude != null && { longitude: ipData.longitude }),\n });\n }).pipe(\n Effect.provide(FetchHttpClient.layer),\n Effect.catch((err) =>\n Effect.sync(() => {\n log.warn('ipdata provider failed', { err });\n }),\n ),\n );\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Exit from 'effect/Exit';\nimport * as FiberMap from 'effect/FiberMap';\nimport * as Queue from 'effect/Queue';\nimport * as Scope from 'effect/Scope';\nimport * as Stream from 'effect/Stream';\n\nimport * as Database from '@dxos/echo/Database';\nimport { log } from '@dxos/log';\nimport { type EventAttributes, type RemoteEvents, TRACE_PROCESSOR } from '@dxos/tracing';\n\nimport type * as Observability from '../Observability.ts';\nimport type * as ObservabilityExtension from '../ObservabilityExtension.ts';\n\n/** The product events ECHO's trace events are reported as. */\nexport const EVENTS = {\n objectAdd: 'space.object.add',\n objectRemove: 'space.object.remove',\n relationAdd: 'space.relation.add',\n relationRemove: 'space.relation.remove',\n typeAdd: 'space.type.add',\n feedAppend: 'space.feed.append',\n} as const;\n\n/**\n * How long a new object must survive before it counts as created: the create dialog adds a draft when it\n * opens and removes it if the user cancels.\n */\nexport const DRAFT_WINDOW = Duration.seconds(30);\n\ntype TraceEvent = { name: string; attributes: EventAttributes };\n\n/**\n * Reports ECHO's trace events as product events: user-facing objects and every relation added or removed, types\n * persisted, and items appended to feeds. Every write is reported with its `origin` (`user`, `system` or `unknown`),\n * so a dashboard separates people's actions from the rest. An add that is removed within the draft window\n * reports neither. Events arrive only while registered; ECHO does not replay them.\n */\nexport const listen = (\n events: RemoteEvents,\n capture: ObservabilityExtension.Events['captureEvent'],\n draftWindow: Duration.Input = DRAFT_WINDOW,\n): Effect.Effect<void, never, Scope.Scope> =>\n Effect.gen(function* () {\n const queue = yield* Queue.unbounded<TraceEvent>();\n const processor = {\n emit: (name: string, attributes: EventAttributes) => Queue.offerUnsafe(queue, { name, attributes }),\n };\n yield* Effect.acquireRelease(\n Effect.sync(() => events.registerProcessor(processor)),\n () => Effect.sync(() => events.unregisterProcessor(processor)),\n );\n\n const send = (event: string, properties: Record<string, unknown>) =>\n Effect.try(() => capture(event, properties)).pipe(Effect.catch((error) => Effect.sync(() => log.catch(error))));\n\n // Keyed by space and object so removing a draft interrupts its own pending report and no other space's.\n const pending = yield* FiberMap.make<string>();\n yield* Stream.fromQueue(queue).pipe(\n Stream.runForEach(({ name, attributes }) =>\n Effect.gen(function* () {\n const { spaceId, objectId, typename, relation, userType, origin } = attributes;\n const draftKey = `${String(spaceId)}/${String(objectId)}`;\n switch (name) {\n case Database.TraceEvents.objectAdd: {\n if (typeof objectId !== 'string' || (relation !== true && userType !== true)) {\n return;\n }\n const event = relation === true ? EVENTS.relationAdd : EVENTS.objectAdd;\n yield* FiberMap.run(\n pending,\n draftKey,\n Effect.sleep(draftWindow).pipe(Effect.andThen(send(event, { spaceId, objectId, typename, origin }))),\n );\n return;\n }\n\n case Database.TraceEvents.objectRemove: {\n if (typeof objectId !== 'string') {\n return;\n }\n if (yield* FiberMap.has(pending, draftKey)) {\n yield* FiberMap.remove(pending, draftKey);\n return;\n }\n if (relation === true || userType === true) {\n const event = relation === true ? EVENTS.relationRemove : EVENTS.objectRemove;\n yield* send(event, { spaceId, objectId, typename, origin });\n }\n return;\n }\n\n case Database.TraceEvents.typeAdd: {\n yield* send(EVENTS.typeAdd, { spaceId, typename, version: attributes.version, origin });\n return;\n }\n\n case Database.TraceEvents.feedAppend: {\n yield* send(EVENTS.feedAppend, { spaceId, feedId: attributes.feedId, objectId, typename, origin });\n return;\n }\n }\n }),\n ),\n Effect.forkScoped,\n );\n });\n\n/**\n * Reports what is written in this realm's spaces, however it was written: from a dialog, an agent, a sync, or a raw\n * `db.add` in a view.\n */\nexport const provider: Observability.DataProvider = Effect.fn(function* (observability) {\n const scope = yield* Scope.make();\n yield* listen(TRACE_PROCESSOR.remoteEvents, (event, attributes) =>\n observability.events.captureEvent(event, attributes),\n ).pipe(Scope.provide(scope));\n return () => {\n Effect.runFork(Scope.close(scope, Exit.void));\n };\n});\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { ClientTraceEvents } from '@dxos/client-protocol';\nimport { log } from '@dxos/log';\nimport { type EventAttributes, type RemoteEvents, TRACE_PROCESSOR } from '@dxos/tracing';\n\nimport type * as Observability from '../Observability.ts';\nimport type * as ObservabilityExtension from '../ObservabilityExtension.ts';\n\n/** The product events the client's trace events are reported as, by invitation kind. */\nexport const EVENTS = {\n spaceCreate: 'space.create',\n spaceShare: 'space.share',\n spaceJoin: 'space.join',\n spaceAdmit: 'space.admit',\n deviceInvite: 'identity.device.invite',\n deviceJoin: 'identity.join',\n deviceAdmit: 'identity.device.admit',\n} as const;\n\ntype ProductEvent = { name: string; properties: Record<string, unknown> };\n\n/** Maps one of the client's trace events to the product event it stands for, if any. */\nexport const toProductEvent = (name: string, attributes: EventAttributes): ProductEvent | undefined => {\n const { spaceId, kind, authMethod, multiUse, origin } = attributes;\n const device = kind === 'device';\n switch (name) {\n case ClientTraceEvents.spaceCreate:\n return { name: EVENTS.spaceCreate, properties: { spaceId, origin } };\n case ClientTraceEvents.invitationCreate:\n return device\n ? { name: EVENTS.deviceInvite, properties: { authMethod, multiUse } }\n : { name: EVENTS.spaceShare, properties: { spaceId, authMethod, multiUse } };\n case ClientTraceEvents.invitationAccept:\n return device ? { name: EVENTS.deviceJoin, properties: {} } : { name: EVENTS.spaceJoin, properties: { spaceId } };\n case ClientTraceEvents.invitationAdmit:\n return device\n ? { name: EVENTS.deviceAdmit, properties: { multiUse } }\n : { name: EVENTS.spaceAdmit, properties: { spaceId, multiUse } };\n default:\n return undefined;\n }\n};\n\n/** Reports the client's space and invitation trace events as product events until the returned cleanup runs. */\nexport const listen = (events: RemoteEvents, capture: ObservabilityExtension.Events['captureEvent']): (() => void) => {\n const processor = {\n emit: (name: string, attributes: EventAttributes) => {\n const event = toProductEvent(name, attributes);\n if (event === undefined) {\n return;\n }\n try {\n capture(event.name, event.properties);\n } catch (error) {\n log.catch(error);\n }\n },\n };\n events.registerProcessor(processor);\n return () => events.unregisterProcessor(processor);\n};\n\n/**\n * Reports spaces this realm creates and invitations it shares, accepts and admits guests through, however they\n * were started: from a dialog, an operation, an agent or a deep link.\n */\nexport const provider: Observability.DataProvider = (observability) =>\n Effect.sync(() =>\n listen(TRACE_PROCESSOR.remoteEvents, (event, attributes) => observability.events.captureEvent(event, attributes)),\n );\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport * as Duration from 'effect/Duration';\nimport * as Effect from 'effect/Effect';\nimport * as Fiber from 'effect/Fiber';\nimport * as Schedule from 'effect/Schedule';\n\nimport * as Observability from '../Observability.ts';\n\nexport const provider: Observability.DataProvider = Effect.fn(function* (observability) {\n if (typeof navigator !== 'undefined' && navigator.storage?.estimate) {\n const action = Effect.gen(function* () {\n const storageEstimate = yield* Effect.tryPromise(() => navigator.storage.estimate());\n storageEstimate.usage && observability.metrics.gauge('storageUsage', storageEstimate.usage);\n storageEstimate.quota && observability.metrics.gauge('storageQuota', storageEstimate.quota);\n });\n\n const fiber = action.pipe(Effect.repeat(Schedule.fixed(Duration.hours(1))), Effect.runFork);\n return () => Effect.runSync(Fiber.interrupt(fiber));\n }\n});\n","//\n// Copyright 2025 DXOS.org\n//\n\n// @import-as-namespace\n\nexport * as EventLoopLag from './event-loop-lag.ts';\nexport * as IPData from './ip-data.ts';\nexport * as Memory from './memory.ts';\nexport * as ObjectEvents from './object-events.ts';\nexport * as SpaceEvents from './space-events.ts';\nexport * as Storage from './storage.ts';\n"],"mappings":"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"}
|