@kb-labs/adapters 0.5.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/.cursorrules +32 -0
- package/.github/workflows/ci.yml +13 -0
- package/.github/workflows/deploy.yml +28 -0
- package/.github/workflows/docker-build.yml +25 -0
- package/.github/workflows/drift-check.yml +10 -0
- package/.github/workflows/profiles-validate.yml +16 -0
- package/.github/workflows/release.yml +8 -0
- package/.kb/devkit/agents/devkit-maintainer/context.globs +15 -0
- package/.kb/devkit/agents/devkit-maintainer/permissions.yml +17 -0
- package/.kb/devkit/agents/devkit-maintainer/prompt.md +28 -0
- package/.kb/devkit/agents/devkit-maintainer/runbook.md +31 -0
- package/.kb/devkit/agents/docs-crafter/prompt.md +24 -0
- package/.kb/devkit/agents/docs-crafter/runbook.md +18 -0
- package/.kb/devkit/agents/release-manager/context.globs +7 -0
- package/.kb/devkit/agents/release-manager/prompt.md +27 -0
- package/.kb/devkit/agents/release-manager/runbook.md +17 -0
- package/.kb/devkit/agents/test-generator/context.globs +7 -0
- package/.kb/devkit/agents/test-generator/prompt.md +27 -0
- package/.kb/devkit/agents/test-generator/runbook.md +18 -0
- package/CONTRIBUTING.md +90 -0
- package/IMPLEMENTATION_COMPLETE.md +416 -0
- package/LICENSE +186 -0
- package/README-TEMPLATE.md +179 -0
- package/README.md +306 -0
- package/docs/DOCUMENTATION.md +74 -0
- package/docs/adr/0000-template.md +49 -0
- package/docs/adr/0001-architecture-and-repository-layout.md +33 -0
- package/docs/adr/0002-plugins-and-extensibility.md +46 -0
- package/docs/adr/0003-package-and-module-boundaries.md +37 -0
- package/docs/adr/0004-versioning-and-release-policy.md +38 -0
- package/docs/adr/0005-use-devkit-for-shared-tooling.md +48 -0
- package/docs/adr/0006-adopt-devkit-sync.md +47 -0
- package/docs/adr/0007-drift-kit-check.md +72 -0
- package/docs/adr/0008-devkit-sync-wrapper-strategy.md +67 -0
- package/docs/naming-convention.md +272 -0
- package/eslint.config.js +27 -0
- package/kb-labs.config.json +5 -0
- package/package.json +84 -0
- package/package.json.bin +25 -0
- package/package.json.lib +30 -0
- package/packages/adapters-analytics-duckdb/package.json +54 -0
- package/packages/adapters-analytics-duckdb/scripts/migrate-from-jsonl.mjs +253 -0
- package/packages/adapters-analytics-duckdb/src/index.ts +380 -0
- package/packages/adapters-analytics-duckdb/src/manifest.ts +36 -0
- package/packages/adapters-analytics-duckdb/src/schema.ts +161 -0
- package/packages/adapters-analytics-duckdb/tsconfig.build.json +15 -0
- package/packages/adapters-analytics-duckdb/tsconfig.json +9 -0
- package/packages/adapters-analytics-duckdb/tsup.config.ts +9 -0
- package/packages/adapters-analytics-file/README.md +32 -0
- package/packages/adapters-analytics-file/eslint.config.js +27 -0
- package/packages/adapters-analytics-file/package.json +50 -0
- package/packages/adapters-analytics-file/src/__tests__/daily-stats.spec.ts +287 -0
- package/packages/adapters-analytics-file/src/__tests__/scoped-analytics.test.ts +233 -0
- package/packages/adapters-analytics-file/src/index.test.ts +214 -0
- package/packages/adapters-analytics-file/src/index.ts +830 -0
- package/packages/adapters-analytics-file/src/manifest.ts +45 -0
- package/packages/adapters-analytics-file/tsconfig.build.json +15 -0
- package/packages/adapters-analytics-file/tsconfig.json +9 -0
- package/packages/adapters-analytics-file/tsup.config.ts +9 -0
- package/packages/adapters-analytics-sqlite/package.json +55 -0
- package/packages/adapters-analytics-sqlite/scripts/migrate-from-jsonl.mjs +194 -0
- package/packages/adapters-analytics-sqlite/src/index.ts +460 -0
- package/packages/adapters-analytics-sqlite/src/manifest.ts +41 -0
- package/packages/adapters-analytics-sqlite/tsconfig.build.json +15 -0
- package/packages/adapters-analytics-sqlite/tsconfig.json +9 -0
- package/packages/adapters-analytics-sqlite/tsup.config.ts +9 -0
- package/packages/adapters-environment-docker/README.md +28 -0
- package/packages/adapters-environment-docker/eslint.config.js +5 -0
- package/packages/adapters-environment-docker/package.json +49 -0
- package/packages/adapters-environment-docker/src/index.test.ts +138 -0
- package/packages/adapters-environment-docker/src/index.ts +439 -0
- package/packages/adapters-environment-docker/src/manifest.ts +65 -0
- package/packages/adapters-environment-docker/tsconfig.build.json +15 -0
- package/packages/adapters-environment-docker/tsconfig.json +16 -0
- package/packages/adapters-environment-docker/tsup.config.ts +9 -0
- package/packages/adapters-eventbus-cache/README.md +242 -0
- package/packages/adapters-eventbus-cache/eslint.config.js +27 -0
- package/packages/adapters-eventbus-cache/package.json +46 -0
- package/packages/adapters-eventbus-cache/src/index.test.ts +235 -0
- package/packages/adapters-eventbus-cache/src/index.ts +215 -0
- package/packages/adapters-eventbus-cache/src/manifest.ts +50 -0
- package/packages/adapters-eventbus-cache/src/types.ts +58 -0
- package/packages/adapters-eventbus-cache/tsconfig.build.json +15 -0
- package/packages/adapters-eventbus-cache/tsconfig.json +9 -0
- package/packages/adapters-eventbus-cache/tsup.config.ts +9 -0
- package/packages/adapters-fs/README.md +171 -0
- package/packages/adapters-fs/allowed.txt +1 -0
- package/packages/adapters-fs/conflict.txt +1 -0
- package/packages/adapters-fs/dest.txt +1 -0
- package/packages/adapters-fs/eslint.config.js +27 -0
- package/packages/adapters-fs/exists.txt +1 -0
- package/packages/adapters-fs/not-allowed.txt +1 -0
- package/packages/adapters-fs/other.txt +1 -0
- package/packages/adapters-fs/package.json +55 -0
- package/packages/adapters-fs/public/file1.txt +1 -0
- package/packages/adapters-fs/public/file2.txt +1 -0
- package/packages/adapters-fs/secret.txt +1 -0
- package/packages/adapters-fs/secrets/key.txt +1 -0
- package/packages/adapters-fs/src/index.test.ts +243 -0
- package/packages/adapters-fs/src/index.ts +258 -0
- package/packages/adapters-fs/src/manifest.ts +35 -0
- package/packages/adapters-fs/src/secure-storage.test.ts +380 -0
- package/packages/adapters-fs/src/secure-storage.ts +268 -0
- package/packages/adapters-fs/test.json +1 -0
- package/packages/adapters-fs/test.txt +1 -0
- package/packages/adapters-fs/test.xyz +1 -0
- package/packages/adapters-fs/test1.txt +1 -0
- package/packages/adapters-fs/test2.txt +1 -0
- package/packages/adapters-fs/tsconfig.build.json +15 -0
- package/packages/adapters-fs/tsconfig.json +9 -0
- package/packages/adapters-fs/tsup.config.ts +8 -0
- package/packages/adapters-fs/vitest.config.ts +19 -0
- package/packages/adapters-log-ringbuffer/README.md +228 -0
- package/packages/adapters-log-ringbuffer/eslint.config.js +27 -0
- package/packages/adapters-log-ringbuffer/package.json +47 -0
- package/packages/adapters-log-ringbuffer/src/__tests__/ring-buffer.test.ts +450 -0
- package/packages/adapters-log-ringbuffer/src/index.ts +212 -0
- package/packages/adapters-log-ringbuffer/src/manifest.ts +30 -0
- package/packages/adapters-log-ringbuffer/tsconfig.build.json +15 -0
- package/packages/adapters-log-ringbuffer/tsconfig.json +9 -0
- package/packages/adapters-log-ringbuffer/tsup.config.ts +9 -0
- package/packages/adapters-log-ringbuffer/vitest.config.ts +14 -0
- package/packages/adapters-log-sqlite/README.md +396 -0
- package/packages/adapters-log-sqlite/eslint.config.js +27 -0
- package/packages/adapters-log-sqlite/package.json +49 -0
- package/packages/adapters-log-sqlite/src/__tests__/log-persistence.test.ts +718 -0
- package/packages/adapters-log-sqlite/src/index.ts +1068 -0
- package/packages/adapters-log-sqlite/src/manifest.ts +36 -0
- package/packages/adapters-log-sqlite/src/schema.sql +46 -0
- package/packages/adapters-log-sqlite/tsconfig.build.json +15 -0
- package/packages/adapters-log-sqlite/tsconfig.json +9 -0
- package/packages/adapters-log-sqlite/tsup.config.ts +9 -0
- package/packages/adapters-log-sqlite/vitest.config.ts +15 -0
- package/packages/adapters-mongodb/README.md +147 -0
- package/packages/adapters-mongodb/eslint.config.js +27 -0
- package/packages/adapters-mongodb/package.json +53 -0
- package/packages/adapters-mongodb/src/index.ts +428 -0
- package/packages/adapters-mongodb/src/manifest.ts +45 -0
- package/packages/adapters-mongodb/src/secure-document.ts +231 -0
- package/packages/adapters-mongodb/tsconfig.build.json +15 -0
- package/packages/adapters-mongodb/tsconfig.json +9 -0
- package/packages/adapters-mongodb/tsup.config.ts +8 -0
- package/packages/adapters-openai/README.md +151 -0
- package/packages/adapters-openai/embeddings.ts +37 -0
- package/packages/adapters-openai/eslint.config.js +26 -0
- package/packages/adapters-openai/index.ts +22 -0
- package/packages/adapters-openai/package.json +57 -0
- package/packages/adapters-openai/src/embeddings-manifest.ts +45 -0
- package/packages/adapters-openai/src/embeddings.ts +104 -0
- package/packages/adapters-openai/src/index.ts +13 -0
- package/packages/adapters-openai/src/llm.ts +304 -0
- package/packages/adapters-openai/src/manifest.ts +47 -0
- package/packages/adapters-openai/tsconfig.build.json +15 -0
- package/packages/adapters-openai/tsconfig.json +9 -0
- package/packages/adapters-openai/tsup.config.ts +8 -0
- package/packages/adapters-pino/README.md +152 -0
- package/packages/adapters-pino/eslint.config.js +27 -0
- package/packages/adapters-pino/package.json +49 -0
- package/packages/adapters-pino/src/index.test.ts +44 -0
- package/packages/adapters-pino/src/index.ts +322 -0
- package/packages/adapters-pino/src/log-ring-buffer.ts +142 -0
- package/packages/adapters-pino/src/manifest.ts +49 -0
- package/packages/adapters-pino/tsconfig.build.json +15 -0
- package/packages/adapters-pino/tsconfig.json +9 -0
- package/packages/adapters-pino/tsup.config.ts +9 -0
- package/packages/adapters-pino-http/README.md +141 -0
- package/packages/adapters-pino-http/eslint.config.js +27 -0
- package/packages/adapters-pino-http/package.json +46 -0
- package/packages/adapters-pino-http/src/index.ts +229 -0
- package/packages/adapters-pino-http/tsconfig.build.json +15 -0
- package/packages/adapters-pino-http/tsconfig.json +9 -0
- package/packages/adapters-pino-http/tsup.config.ts +9 -0
- package/packages/adapters-qdrant/README.md +166 -0
- package/packages/adapters-qdrant/eslint.config.js +27 -0
- package/packages/adapters-qdrant/package.json +49 -0
- package/packages/adapters-qdrant/src/index.ts +490 -0
- package/packages/adapters-qdrant/src/manifest.ts +54 -0
- package/packages/adapters-qdrant/src/retry.ts +204 -0
- package/packages/adapters-qdrant/tsconfig.build.json +15 -0
- package/packages/adapters-qdrant/tsconfig.json +9 -0
- package/packages/adapters-qdrant/tsup.config.ts +9 -0
- package/packages/adapters-redis/README.md +159 -0
- package/packages/adapters-redis/eslint.config.js +27 -0
- package/packages/adapters-redis/package.json +49 -0
- package/packages/adapters-redis/src/index.ts +164 -0
- package/packages/adapters-redis/src/manifest.ts +49 -0
- package/packages/adapters-redis/tsconfig.build.json +15 -0
- package/packages/adapters-redis/tsconfig.json +9 -0
- package/packages/adapters-redis/tsup.config.ts +9 -0
- package/packages/adapters-snapshot-localfs/README.md +10 -0
- package/packages/adapters-snapshot-localfs/eslint.config.js +2 -0
- package/packages/adapters-snapshot-localfs/package.json +46 -0
- package/packages/adapters-snapshot-localfs/src/index.test.ts +40 -0
- package/packages/adapters-snapshot-localfs/src/index.ts +292 -0
- package/packages/adapters-snapshot-localfs/src/manifest.ts +32 -0
- package/packages/adapters-snapshot-localfs/tsconfig.build.json +15 -0
- package/packages/adapters-snapshot-localfs/tsconfig.json +16 -0
- package/packages/adapters-snapshot-localfs/tsup.config.ts +11 -0
- package/packages/adapters-sqlite/README.md +163 -0
- package/packages/adapters-sqlite/eslint.config.js +27 -0
- package/packages/adapters-sqlite/package.json +54 -0
- package/packages/adapters-sqlite/src/index.test.ts +245 -0
- package/packages/adapters-sqlite/src/index.ts +382 -0
- package/packages/adapters-sqlite/src/manifest.ts +47 -0
- package/packages/adapters-sqlite/src/secure-sql.test.ts +290 -0
- package/packages/adapters-sqlite/src/secure-sql.ts +281 -0
- package/packages/adapters-sqlite/tsconfig.build.json +15 -0
- package/packages/adapters-sqlite/tsconfig.json +9 -0
- package/packages/adapters-sqlite/tsup.config.ts +8 -0
- package/packages/adapters-sqlite/vitest.config.ts +19 -0
- package/packages/adapters-transport/README.md +170 -0
- package/packages/adapters-transport/eslint.config.js +27 -0
- package/packages/adapters-transport/package.json +49 -0
- package/packages/adapters-transport/src/__tests__/unix-socket-server.test.ts +550 -0
- package/packages/adapters-transport/src/index.ts +101 -0
- package/packages/adapters-transport/src/ipc-transport.ts +228 -0
- package/packages/adapters-transport/src/transport.ts +224 -0
- package/packages/adapters-transport/src/types.ts +92 -0
- package/packages/adapters-transport/src/unix-socket-server.ts +193 -0
- package/packages/adapters-transport/src/unix-socket-transport.ts +280 -0
- package/packages/adapters-transport/tsconfig.build.json +15 -0
- package/packages/adapters-transport/tsconfig.json +9 -0
- package/packages/adapters-transport/tsup.config.ts +9 -0
- package/packages/adapters-vibeproxy/README.md +159 -0
- package/packages/adapters-vibeproxy/eslint.config.js +27 -0
- package/packages/adapters-vibeproxy/package.json +51 -0
- package/packages/adapters-vibeproxy/src/index.ts +13 -0
- package/packages/adapters-vibeproxy/src/llm.ts +437 -0
- package/packages/adapters-vibeproxy/src/manifest.ts +51 -0
- package/packages/adapters-vibeproxy/tsconfig.build.json +15 -0
- package/packages/adapters-vibeproxy/tsconfig.json +9 -0
- package/packages/adapters-vibeproxy/tsup.config.ts +8 -0
- package/packages/adapters-workspace-agent/package.json +46 -0
- package/packages/adapters-workspace-agent/src/__tests__/adapter.test.ts +212 -0
- package/packages/adapters-workspace-agent/src/index.ts +220 -0
- package/packages/adapters-workspace-agent/src/manifest.ts +36 -0
- package/packages/adapters-workspace-agent/tsconfig.build.json +15 -0
- package/packages/adapters-workspace-agent/tsconfig.json +16 -0
- package/packages/adapters-workspace-agent/tsup.config.ts +11 -0
- package/packages/adapters-workspace-localfs/README.md +9 -0
- package/packages/adapters-workspace-localfs/eslint.config.js +2 -0
- package/packages/adapters-workspace-localfs/package.json +46 -0
- package/packages/adapters-workspace-localfs/src/index.test.ts +27 -0
- package/packages/adapters-workspace-localfs/src/index.ts +172 -0
- package/packages/adapters-workspace-localfs/src/manifest.ts +32 -0
- package/packages/adapters-workspace-localfs/tsconfig.build.json +15 -0
- package/packages/adapters-workspace-localfs/tsconfig.json +16 -0
- package/packages/adapters-workspace-localfs/tsup.config.ts +11 -0
- package/packages/adapters-workspace-worktree/README.md +9 -0
- package/packages/adapters-workspace-worktree/eslint.config.js +2 -0
- package/packages/adapters-workspace-worktree/package.json +46 -0
- package/packages/adapters-workspace-worktree/src/index.test.ts +38 -0
- package/packages/adapters-workspace-worktree/src/index.ts +245 -0
- package/packages/adapters-workspace-worktree/src/manifest.ts +38 -0
- package/packages/adapters-workspace-worktree/tsconfig.build.json +15 -0
- package/packages/adapters-workspace-worktree/tsconfig.json +16 -0
- package/packages/adapters-workspace-worktree/tsup.config.ts +11 -0
- package/pnpm-workspace.yaml +2800 -0
- package/prettierrc.json +1 -0
- package/scripts/devkit-sync.mjs +37 -0
- package/scripts/hooks/post-push +9 -0
- package/scripts/hooks/pre-commit +9 -0
- package/scripts/hooks/pre-push +9 -0
- package/test-integration.ts +242 -0
- package/test.txt +1 -0
- package/tsconfig.base.json +6 -0
- package/tsconfig.build.json +15 -0
- package/tsconfig.json +9 -0
- package/tsconfig.paths.json +26 -0
- package/tsconfig.tools.json +17 -0
- package/tsup.config.bin.ts +34 -0
- package/tsup.config.cli.ts +41 -0
- package/tsup.config.dual.ts +46 -0
- package/tsup.config.ts +36 -0
- package/tsup.external.json +103 -0
- package/vitest.config.ts +2 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { format, parseISO } from "date-fns";
|
|
4
|
+
import type {
|
|
5
|
+
IAnalytics,
|
|
6
|
+
ICache,
|
|
7
|
+
AnalyticsContext,
|
|
8
|
+
EventsQuery,
|
|
9
|
+
StatsQuery,
|
|
10
|
+
EventsResponse,
|
|
11
|
+
EventsStats,
|
|
12
|
+
DailyStats,
|
|
13
|
+
BufferStatus,
|
|
14
|
+
DlqStatus,
|
|
15
|
+
AnalyticsEvent as PlatformAnalyticsEvent,
|
|
16
|
+
} from "@kb-labs/core-platform/adapters";
|
|
17
|
+
|
|
18
|
+
// Re-export manifest
|
|
19
|
+
export { manifest } from "./manifest.js";
|
|
20
|
+
import { randomUUID } from "node:crypto";
|
|
21
|
+
|
|
22
|
+
export interface FileAnalyticsOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Base directory for analytics logs.
|
|
25
|
+
* Defaults to ".kb/analytics/buffer" relative to process.cwd().
|
|
26
|
+
*/
|
|
27
|
+
baseDir?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Filename pattern (without extension), default: "events-YYYYMMDD"
|
|
30
|
+
*/
|
|
31
|
+
filenamePattern?: string;
|
|
32
|
+
|
|
33
|
+
// Runtime contexts injected by loader based on manifest
|
|
34
|
+
/**
|
|
35
|
+
* Workspace context (injected when manifest requests 'workspace')
|
|
36
|
+
*/
|
|
37
|
+
workspace?: { cwd: string };
|
|
38
|
+
/**
|
|
39
|
+
* Analytics context (injected when manifest requests 'analytics')
|
|
40
|
+
*/
|
|
41
|
+
analytics?: AnalyticsContext;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Legacy stored event format (for backward compatibility)
|
|
46
|
+
*/
|
|
47
|
+
interface StoredEventLegacy {
|
|
48
|
+
type: "event" | "metric";
|
|
49
|
+
timestamp: string;
|
|
50
|
+
name: string;
|
|
51
|
+
properties?: Record<string, unknown>;
|
|
52
|
+
value?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class FileAnalytics implements IAnalytics {
|
|
56
|
+
private readonly baseDir: string;
|
|
57
|
+
private readonly filenamePattern: string;
|
|
58
|
+
private context: AnalyticsContext; // Removed readonly to allow setSource()
|
|
59
|
+
private cache?: ICache; // Optional cache for stats caching
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
options: FileAnalyticsOptions = {},
|
|
63
|
+
context?: AnalyticsContext,
|
|
64
|
+
cache?: ICache,
|
|
65
|
+
) {
|
|
66
|
+
// Get cwd from workspace context (injected by loader) or fallback
|
|
67
|
+
const cwd = options.workspace?.cwd ?? process.cwd();
|
|
68
|
+
|
|
69
|
+
const defaultBaseDir = join(cwd, ".kb/analytics/buffer");
|
|
70
|
+
const configuredBaseDir = options.baseDir ?? defaultBaseDir;
|
|
71
|
+
|
|
72
|
+
// If baseDir is relative, resolve from cwd; otherwise use as-is
|
|
73
|
+
this.baseDir = configuredBaseDir.startsWith("/")
|
|
74
|
+
? configuredBaseDir
|
|
75
|
+
: join(cwd, configuredBaseDir);
|
|
76
|
+
|
|
77
|
+
this.filenamePattern = options.filenamePattern ?? "events-YYYYMMDD";
|
|
78
|
+
|
|
79
|
+
// Priority: options.analytics (injected) → legacy context param → fallback
|
|
80
|
+
this.context = options.analytics ??
|
|
81
|
+
context ?? {
|
|
82
|
+
source: { product: "unknown", version: "0.0.0" },
|
|
83
|
+
runId: randomUUID(),
|
|
84
|
+
ctx: { workspace: cwd },
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Store cache if provided
|
|
88
|
+
this.cache = cache;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async track(event: string, payload?: unknown): Promise<void> {
|
|
92
|
+
// Create V1 event with automatic context enrichment
|
|
93
|
+
const v1Event: PlatformAnalyticsEvent = {
|
|
94
|
+
id: randomUUID(),
|
|
95
|
+
schema: "kb.v1",
|
|
96
|
+
type: event,
|
|
97
|
+
ts: new Date().toISOString(),
|
|
98
|
+
ingestTs: new Date().toISOString(),
|
|
99
|
+
source: this.context.source,
|
|
100
|
+
runId: this.context.runId,
|
|
101
|
+
actor: this.context.actor,
|
|
102
|
+
ctx: this.context.ctx,
|
|
103
|
+
payload,
|
|
104
|
+
};
|
|
105
|
+
await this.writeV1(v1Event);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async metric(
|
|
109
|
+
name: string,
|
|
110
|
+
value: number,
|
|
111
|
+
tags?: Record<string, string>,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
// Metrics are tracked as events with value in payload
|
|
114
|
+
const v1Event: PlatformAnalyticsEvent = {
|
|
115
|
+
id: randomUUID(),
|
|
116
|
+
schema: "kb.v1",
|
|
117
|
+
type: name,
|
|
118
|
+
ts: new Date().toISOString(),
|
|
119
|
+
ingestTs: new Date().toISOString(),
|
|
120
|
+
source: this.context.source,
|
|
121
|
+
runId: this.context.runId,
|
|
122
|
+
actor: this.context.actor,
|
|
123
|
+
ctx: { ...this.context.ctx, ...tags },
|
|
124
|
+
payload: { value },
|
|
125
|
+
};
|
|
126
|
+
await this.writeV1(v1Event);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async identify(
|
|
130
|
+
userId: string,
|
|
131
|
+
traits?: Record<string, unknown>,
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
const v1Event: PlatformAnalyticsEvent = {
|
|
134
|
+
id: randomUUID(),
|
|
135
|
+
schema: "kb.v1",
|
|
136
|
+
type: "user.identify",
|
|
137
|
+
ts: new Date().toISOString(),
|
|
138
|
+
ingestTs: new Date().toISOString(),
|
|
139
|
+
source: this.context.source,
|
|
140
|
+
runId: this.context.runId,
|
|
141
|
+
actor: {
|
|
142
|
+
type: "user",
|
|
143
|
+
id: userId,
|
|
144
|
+
name: (traits?.name as string) || undefined,
|
|
145
|
+
},
|
|
146
|
+
ctx: this.context.ctx,
|
|
147
|
+
payload: traits,
|
|
148
|
+
};
|
|
149
|
+
await this.writeV1(v1Event);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async flush(): Promise<void> {
|
|
153
|
+
// No buffering, so nothing to flush
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get current source attribution
|
|
158
|
+
*
|
|
159
|
+
* Returns the current source used for tracking events.
|
|
160
|
+
* Useful for saving and restoring source in nested plugin execution.
|
|
161
|
+
*
|
|
162
|
+
* @returns Current source (product + version)
|
|
163
|
+
*/
|
|
164
|
+
getSource(): { product: string; version: string } {
|
|
165
|
+
return this.context.source;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Override source attribution for scoped execution
|
|
170
|
+
*
|
|
171
|
+
* This allows plugins to track events with their own source (product + version)
|
|
172
|
+
* instead of using the root package.json source.
|
|
173
|
+
*
|
|
174
|
+
* @param source - New source to use for future events
|
|
175
|
+
*/
|
|
176
|
+
setSource(source: { product: string; version: string }): void {
|
|
177
|
+
this.context = {
|
|
178
|
+
...this.context,
|
|
179
|
+
source,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ========================================
|
|
184
|
+
// Read Methods (NEW)
|
|
185
|
+
// ========================================
|
|
186
|
+
|
|
187
|
+
async getEvents(query?: EventsQuery): Promise<EventsResponse> {
|
|
188
|
+
const allEvents = await this.readAllEvents();
|
|
189
|
+
|
|
190
|
+
// Apply filters
|
|
191
|
+
let filtered = allEvents;
|
|
192
|
+
|
|
193
|
+
if (query?.type) {
|
|
194
|
+
const types = Array.isArray(query.type) ? query.type : [query.type];
|
|
195
|
+
filtered = filtered.filter((e) => types.includes(e.type));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (query?.source) {
|
|
199
|
+
filtered = filtered.filter((e) => e.source.product === query.source);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (query?.actor) {
|
|
203
|
+
filtered = filtered.filter((e) => e.actor?.type === query.actor);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (query?.from) {
|
|
207
|
+
const fromTs = parseISO(query.from).getTime();
|
|
208
|
+
filtered = filtered.filter((e) => parseISO(e.ts).getTime() >= fromTs);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (query?.to) {
|
|
212
|
+
const toTs = parseISO(query.to).getTime();
|
|
213
|
+
filtered = filtered.filter((e) => parseISO(e.ts).getTime() <= toTs);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Sort by timestamp descending (newest first)
|
|
217
|
+
filtered.sort(
|
|
218
|
+
(a, b) => parseISO(b.ts).getTime() - parseISO(a.ts).getTime(),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Apply pagination
|
|
222
|
+
const limit = query?.limit ?? 100;
|
|
223
|
+
const offset = query?.offset ?? 0;
|
|
224
|
+
const paginated = filtered.slice(offset, offset + limit);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
events: paginated,
|
|
228
|
+
total: filtered.length,
|
|
229
|
+
hasMore: offset + limit < filtered.length,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Analytics aggregation with grouping, counting, and date ranges
|
|
234
|
+
async getStats(): Promise<EventsStats> {
|
|
235
|
+
// Cache key for stats
|
|
236
|
+
const cacheKey = "analytics:file:stats";
|
|
237
|
+
|
|
238
|
+
// Try to get from cache first (if cache adapter was provided)
|
|
239
|
+
if (this.cache) {
|
|
240
|
+
try {
|
|
241
|
+
const cached = await this.cache.get<EventsStats>(cacheKey);
|
|
242
|
+
if (cached) {
|
|
243
|
+
return cached;
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
// Cache miss or error, continue to compute
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Compute stats
|
|
251
|
+
const allEvents = await this.readAllEvents();
|
|
252
|
+
|
|
253
|
+
const byType: Record<string, number> = {};
|
|
254
|
+
const bySource: Record<string, number> = {};
|
|
255
|
+
const byActor: Record<string, number> = {};
|
|
256
|
+
|
|
257
|
+
for (const event of allEvents) {
|
|
258
|
+
byType[event.type] = (byType[event.type] || 0) + 1;
|
|
259
|
+
if (event.source?.product) {
|
|
260
|
+
bySource[event.source.product] =
|
|
261
|
+
(bySource[event.source.product] || 0) + 1;
|
|
262
|
+
}
|
|
263
|
+
if (event.actor) {
|
|
264
|
+
byActor[event.actor.type] = (byActor[event.actor.type] || 0) + 1;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let oldestTs = Date.now();
|
|
269
|
+
let newestTs = Date.now();
|
|
270
|
+
|
|
271
|
+
if (allEvents.length > 0) {
|
|
272
|
+
const firstTs = parseISO(allEvents[0]!.ts).getTime();
|
|
273
|
+
oldestTs = firstTs;
|
|
274
|
+
newestTs = firstTs;
|
|
275
|
+
|
|
276
|
+
for (const event of allEvents) {
|
|
277
|
+
const ts = parseISO(event.ts).getTime();
|
|
278
|
+
if (ts < oldestTs) {
|
|
279
|
+
oldestTs = ts;
|
|
280
|
+
}
|
|
281
|
+
if (ts > newestTs) {
|
|
282
|
+
newestTs = ts;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const stats: EventsStats = {
|
|
288
|
+
totalEvents: allEvents.length,
|
|
289
|
+
byType,
|
|
290
|
+
bySource,
|
|
291
|
+
byActor,
|
|
292
|
+
timeRange: {
|
|
293
|
+
from: new Date(oldestTs).toISOString(),
|
|
294
|
+
to: new Date(newestTs).toISOString(),
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Cache result for 60 seconds (if cache adapter was provided)
|
|
299
|
+
if (this.cache) {
|
|
300
|
+
try {
|
|
301
|
+
await this.cache.set(cacheKey, stats, 60 * 1000);
|
|
302
|
+
} catch {
|
|
303
|
+
// Ignore cache write errors
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return stats;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
async getDailyStats(query?: StatsQuery): Promise<DailyStats[]> {
|
|
312
|
+
const { events } = await this.getEvents(query);
|
|
313
|
+
|
|
314
|
+
const groupBy = query?.groupBy ?? "day";
|
|
315
|
+
const breakdownBy = query?.breakdownBy;
|
|
316
|
+
const metricsFilter = query?.metrics;
|
|
317
|
+
|
|
318
|
+
// Build date bucket key from event timestamp
|
|
319
|
+
const getBucketKey = (ts: string): string => {
|
|
320
|
+
const d = parseISO(ts);
|
|
321
|
+
switch (groupBy) {
|
|
322
|
+
case "hour":
|
|
323
|
+
return format(d, "yyyy-MM-dd'T'HH");
|
|
324
|
+
case "week":
|
|
325
|
+
return format(d, "yyyy-'W'II");
|
|
326
|
+
case "month":
|
|
327
|
+
return format(d, "yyyy-MM");
|
|
328
|
+
default:
|
|
329
|
+
return format(d, "yyyy-MM-dd");
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Read a nested field by dot-notation path from an event object
|
|
334
|
+
const getFieldValue = (event: PlatformAnalyticsEvent, path: string): string => {
|
|
335
|
+
const parts = path.split(".");
|
|
336
|
+
let cur: unknown = event;
|
|
337
|
+
for (const part of parts) {
|
|
338
|
+
if (cur === null || cur === undefined || typeof cur !== "object") {return "";}
|
|
339
|
+
cur = (cur as Record<string, unknown>)[part];
|
|
340
|
+
}
|
|
341
|
+
return cur === null || cur === undefined ? "" : String(cur);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Group events by bucket key + optional breakdown value
|
|
345
|
+
// Map key: `${bucketKey}::${breakdownValue}` (or just bucketKey when no breakdown)
|
|
346
|
+
const groups = new Map<string, { date: string; breakdown?: string; events: PlatformAnalyticsEvent[] }>();
|
|
347
|
+
|
|
348
|
+
for (const event of events) {
|
|
349
|
+
const bucketKey = getBucketKey(event.ts);
|
|
350
|
+
const breakdownValue = breakdownBy ? getFieldValue(event, breakdownBy) : undefined;
|
|
351
|
+
const groupKey = breakdownValue !== undefined ? `${bucketKey}::${breakdownValue}` : bucketKey;
|
|
352
|
+
|
|
353
|
+
if (!groups.has(groupKey)) {
|
|
354
|
+
groups.set(groupKey, { date: bucketKey, breakdown: breakdownValue, events: [] });
|
|
355
|
+
}
|
|
356
|
+
groups.get(groupKey)!.events.push(event);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const dailyStats: DailyStats[] = [];
|
|
360
|
+
|
|
361
|
+
for (const { date, breakdown, events: groupEvents } of groups.values()) {
|
|
362
|
+
const metrics = this.aggregateMetricsForGroup(groupEvents, metricsFilter);
|
|
363
|
+
const entry: DailyStats = {
|
|
364
|
+
date,
|
|
365
|
+
count: groupEvents.length,
|
|
366
|
+
metrics: Object.keys(metrics).length > 0 ? metrics : undefined,
|
|
367
|
+
};
|
|
368
|
+
if (breakdown !== undefined) {entry.breakdown = breakdown;}
|
|
369
|
+
dailyStats.push(entry);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
dailyStats.sort((a, b) => {
|
|
373
|
+
const dateCmp = a.date.localeCompare(b.date);
|
|
374
|
+
if (dateCmp !== 0) {return dateCmp;}
|
|
375
|
+
return (a.breakdown ?? "").localeCompare(b.breakdown ?? "");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
return dailyStats;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async getBufferStatus(): Promise<BufferStatus | null> {
|
|
382
|
+
// File-based analytics doesn't have a WAL buffer
|
|
383
|
+
// But we can return info about stored files
|
|
384
|
+
try {
|
|
385
|
+
await fs.ensureDir(this.baseDir);
|
|
386
|
+
const files = await fs.readdir(this.baseDir);
|
|
387
|
+
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
388
|
+
|
|
389
|
+
if (jsonlFiles.length === 0) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let totalSize = 0;
|
|
394
|
+
const timestamps: number[] = [];
|
|
395
|
+
|
|
396
|
+
for (const file of jsonlFiles) {
|
|
397
|
+
const filePath = join(this.baseDir, file);
|
|
398
|
+
|
|
399
|
+
const stats = await fs.stat(filePath);
|
|
400
|
+
totalSize += stats.size;
|
|
401
|
+
|
|
402
|
+
// Read first and last line to get timestamps
|
|
403
|
+
|
|
404
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
405
|
+
const lines = content
|
|
406
|
+
.trim()
|
|
407
|
+
.split("\n")
|
|
408
|
+
.filter((l) => l.length > 0);
|
|
409
|
+
|
|
410
|
+
for (const line of lines) {
|
|
411
|
+
try {
|
|
412
|
+
const event = JSON.parse(line);
|
|
413
|
+
// Handle both V1 and legacy formats
|
|
414
|
+
const ts = event.ts || event.timestamp;
|
|
415
|
+
if (ts) {
|
|
416
|
+
timestamps.push(parseISO(ts).getTime());
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
// Skip invalid lines
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
segments: jsonlFiles.length,
|
|
426
|
+
totalSizeBytes: totalSize,
|
|
427
|
+
oldestEventTs:
|
|
428
|
+
timestamps.length > 0
|
|
429
|
+
? new Date(Math.min(...timestamps)).toISOString()
|
|
430
|
+
: null,
|
|
431
|
+
newestEventTs:
|
|
432
|
+
timestamps.length > 0
|
|
433
|
+
? new Date(Math.max(...timestamps)).toISOString()
|
|
434
|
+
: null,
|
|
435
|
+
};
|
|
436
|
+
} catch (_error) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async getDlqStatus(): Promise<DlqStatus | null> {
|
|
442
|
+
// File-based analytics doesn't have a DLQ
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ========================================
|
|
447
|
+
// Private Methods
|
|
448
|
+
// ========================================
|
|
449
|
+
|
|
450
|
+
private shouldIncludeMetric(metricsFilter: string[] | undefined, key: string): boolean {
|
|
451
|
+
return !metricsFilter || metricsFilter.includes(key);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private readNumeric(payload: unknown, key: string): number {
|
|
455
|
+
const value = (payload as Record<string, unknown> | undefined)?.[key];
|
|
456
|
+
return typeof value === "number" ? value : 0;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private aggregateLlmMetrics(
|
|
460
|
+
groupEvents: PlatformAnalyticsEvent[],
|
|
461
|
+
metricsFilter?: string[],
|
|
462
|
+
): Record<string, number> {
|
|
463
|
+
let totalTokens = 0;
|
|
464
|
+
let totalCost = 0;
|
|
465
|
+
let totalDuration = 0;
|
|
466
|
+
|
|
467
|
+
for (const event of groupEvents) {
|
|
468
|
+
totalTokens += this.readNumeric(event.payload, "totalTokens");
|
|
469
|
+
const estimatedCost = this.readNumeric(event.payload, "estimatedCost");
|
|
470
|
+
totalCost += estimatedCost || this.readNumeric(event.payload, "cost");
|
|
471
|
+
totalDuration += this.readNumeric(event.payload, "durationMs");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const metrics: Record<string, number> = {};
|
|
475
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalTokens")) {
|
|
476
|
+
metrics.totalTokens = totalTokens;
|
|
477
|
+
}
|
|
478
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalCost")) {
|
|
479
|
+
metrics.totalCost = totalCost;
|
|
480
|
+
}
|
|
481
|
+
if (this.shouldIncludeMetric(metricsFilter, "avgDurationMs")) {
|
|
482
|
+
metrics.avgDurationMs =
|
|
483
|
+
groupEvents.length > 0 ? totalDuration / groupEvents.length : 0;
|
|
484
|
+
}
|
|
485
|
+
return metrics;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private aggregateEmbeddingsMetrics(
|
|
489
|
+
groupEvents: PlatformAnalyticsEvent[],
|
|
490
|
+
metricsFilter?: string[],
|
|
491
|
+
): Record<string, number> {
|
|
492
|
+
let totalTokens = 0;
|
|
493
|
+
let totalCost = 0;
|
|
494
|
+
let totalDuration = 0;
|
|
495
|
+
|
|
496
|
+
for (const event of groupEvents) {
|
|
497
|
+
totalTokens += this.readNumeric(event.payload, "tokens");
|
|
498
|
+
const estimatedCost = this.readNumeric(event.payload, "estimatedCost");
|
|
499
|
+
totalCost += estimatedCost || this.readNumeric(event.payload, "cost");
|
|
500
|
+
totalDuration += this.readNumeric(event.payload, "durationMs");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const metrics: Record<string, number> = {};
|
|
504
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalTokens")) {
|
|
505
|
+
metrics.totalTokens = totalTokens;
|
|
506
|
+
}
|
|
507
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalCost")) {
|
|
508
|
+
metrics.totalCost = totalCost;
|
|
509
|
+
}
|
|
510
|
+
if (this.shouldIncludeMetric(metricsFilter, "avgDurationMs")) {
|
|
511
|
+
metrics.avgDurationMs =
|
|
512
|
+
groupEvents.length > 0 ? totalDuration / groupEvents.length : 0;
|
|
513
|
+
}
|
|
514
|
+
return metrics;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private aggregateVectorstoreMetrics(
|
|
518
|
+
groupEvents: PlatformAnalyticsEvent[],
|
|
519
|
+
metricsFilter?: string[],
|
|
520
|
+
): Record<string, number> {
|
|
521
|
+
let totalSearches = 0;
|
|
522
|
+
let totalUpserts = 0;
|
|
523
|
+
let totalDeletes = 0;
|
|
524
|
+
let totalDuration = 0;
|
|
525
|
+
|
|
526
|
+
for (const event of groupEvents) {
|
|
527
|
+
if (event.type.includes("search")) {
|
|
528
|
+
totalSearches++;
|
|
529
|
+
}
|
|
530
|
+
if (event.type.includes("upsert")) {
|
|
531
|
+
totalUpserts++;
|
|
532
|
+
}
|
|
533
|
+
if (event.type.includes("delete")) {
|
|
534
|
+
totalDeletes++;
|
|
535
|
+
}
|
|
536
|
+
totalDuration += this.readNumeric(event.payload, "durationMs");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const metrics: Record<string, number> = {};
|
|
540
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalSearches")) {
|
|
541
|
+
metrics.totalSearches = totalSearches;
|
|
542
|
+
}
|
|
543
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalUpserts")) {
|
|
544
|
+
metrics.totalUpserts = totalUpserts;
|
|
545
|
+
}
|
|
546
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalDeletes")) {
|
|
547
|
+
metrics.totalDeletes = totalDeletes;
|
|
548
|
+
}
|
|
549
|
+
if (this.shouldIncludeMetric(metricsFilter, "avgDurationMs")) {
|
|
550
|
+
metrics.avgDurationMs =
|
|
551
|
+
groupEvents.length > 0 ? totalDuration / groupEvents.length : 0;
|
|
552
|
+
}
|
|
553
|
+
return metrics;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private aggregateCacheMetrics(
|
|
557
|
+
groupEvents: PlatformAnalyticsEvent[],
|
|
558
|
+
metricsFilter?: string[],
|
|
559
|
+
): Record<string, number> {
|
|
560
|
+
let totalHits = 0;
|
|
561
|
+
let totalMisses = 0;
|
|
562
|
+
let totalSets = 0;
|
|
563
|
+
|
|
564
|
+
for (const event of groupEvents) {
|
|
565
|
+
if (event.type === "cache.hit") {
|
|
566
|
+
totalHits++;
|
|
567
|
+
}
|
|
568
|
+
if (event.type === "cache.miss") {
|
|
569
|
+
totalMisses++;
|
|
570
|
+
}
|
|
571
|
+
if (event.type === "cache.set") {
|
|
572
|
+
totalSets++;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const totalGets = totalHits + totalMisses;
|
|
577
|
+
const metrics: Record<string, number> = {};
|
|
578
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalHits")) {
|
|
579
|
+
metrics.totalHits = totalHits;
|
|
580
|
+
}
|
|
581
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalMisses")) {
|
|
582
|
+
metrics.totalMisses = totalMisses;
|
|
583
|
+
}
|
|
584
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalSets")) {
|
|
585
|
+
metrics.totalSets = totalSets;
|
|
586
|
+
}
|
|
587
|
+
if (this.shouldIncludeMetric(metricsFilter, "hitRate")) {
|
|
588
|
+
metrics.hitRate = totalGets > 0 ? (totalHits / totalGets) * 100 : 0;
|
|
589
|
+
}
|
|
590
|
+
return metrics;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private aggregateStorageMetrics(
|
|
594
|
+
groupEvents: PlatformAnalyticsEvent[],
|
|
595
|
+
metricsFilter?: string[],
|
|
596
|
+
): Record<string, number> {
|
|
597
|
+
let totalBytesRead = 0;
|
|
598
|
+
let totalBytesWritten = 0;
|
|
599
|
+
let totalDuration = 0;
|
|
600
|
+
|
|
601
|
+
for (const event of groupEvents) {
|
|
602
|
+
totalBytesRead += this.readNumeric(event.payload, "bytesRead");
|
|
603
|
+
totalBytesWritten += this.readNumeric(event.payload, "bytesWritten");
|
|
604
|
+
totalDuration += this.readNumeric(event.payload, "durationMs");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const metrics: Record<string, number> = {};
|
|
608
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalBytesRead")) {
|
|
609
|
+
metrics.totalBytesRead = totalBytesRead;
|
|
610
|
+
}
|
|
611
|
+
if (this.shouldIncludeMetric(metricsFilter, "totalBytesWritten")) {
|
|
612
|
+
metrics.totalBytesWritten = totalBytesWritten;
|
|
613
|
+
}
|
|
614
|
+
if (this.shouldIncludeMetric(metricsFilter, "avgDurationMs")) {
|
|
615
|
+
metrics.avgDurationMs =
|
|
616
|
+
groupEvents.length > 0 ? totalDuration / groupEvents.length : 0;
|
|
617
|
+
}
|
|
618
|
+
return metrics;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
private aggregateMetricsForGroup(
|
|
622
|
+
groupEvents: PlatformAnalyticsEvent[],
|
|
623
|
+
metricsFilter?: string[],
|
|
624
|
+
): Record<string, number> {
|
|
625
|
+
const firstType = groupEvents[0]?.type ?? "";
|
|
626
|
+
|
|
627
|
+
if (firstType.startsWith("llm.")) {
|
|
628
|
+
return this.aggregateLlmMetrics(groupEvents, metricsFilter);
|
|
629
|
+
}
|
|
630
|
+
if (firstType.startsWith("embeddings.")) {
|
|
631
|
+
return this.aggregateEmbeddingsMetrics(groupEvents, metricsFilter);
|
|
632
|
+
}
|
|
633
|
+
if (firstType.startsWith("vectorstore.")) {
|
|
634
|
+
return this.aggregateVectorstoreMetrics(groupEvents, metricsFilter);
|
|
635
|
+
}
|
|
636
|
+
if (firstType.startsWith("cache.")) {
|
|
637
|
+
return this.aggregateCacheMetrics(groupEvents, metricsFilter);
|
|
638
|
+
}
|
|
639
|
+
if (firstType.startsWith("storage.")) {
|
|
640
|
+
return this.aggregateStorageMetrics(groupEvents, metricsFilter);
|
|
641
|
+
}
|
|
642
|
+
return {};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Write V1 event directly to file (new format)
|
|
647
|
+
*/
|
|
648
|
+
private async writeV1(event: PlatformAnalyticsEvent): Promise<void> {
|
|
649
|
+
const dateStr = format(new Date(), "yyyyMMdd");
|
|
650
|
+
const filename =
|
|
651
|
+
this.filenamePattern.replace("YYYYMMDD", dateStr) + ".jsonl";
|
|
652
|
+
const fullPath = join(this.baseDir, filename);
|
|
653
|
+
await fs.ensureDir(this.baseDir);
|
|
654
|
+
await fs.appendFile(fullPath, JSON.stringify(event) + "\n", {
|
|
655
|
+
encoding: "utf8",
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Read all events from all .jsonl files.
|
|
661
|
+
* Supports both V1 format (schema: "kb.v1") and legacy format (for backward compatibility).
|
|
662
|
+
*/
|
|
663
|
+
private async readAllEvents(): Promise<PlatformAnalyticsEvent[]> {
|
|
664
|
+
try {
|
|
665
|
+
await fs.ensureDir(this.baseDir);
|
|
666
|
+
const files = await fs.readdir(this.baseDir);
|
|
667
|
+
const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
|
|
668
|
+
|
|
669
|
+
const events: PlatformAnalyticsEvent[] = [];
|
|
670
|
+
|
|
671
|
+
for (const file of jsonlFiles) {
|
|
672
|
+
const filePath = join(this.baseDir, file);
|
|
673
|
+
|
|
674
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
675
|
+
const lines = content
|
|
676
|
+
.trim()
|
|
677
|
+
.split("\n")
|
|
678
|
+
.filter((l) => l.length > 0);
|
|
679
|
+
|
|
680
|
+
for (const line of lines) {
|
|
681
|
+
try {
|
|
682
|
+
const parsed = JSON.parse(line);
|
|
683
|
+
|
|
684
|
+
// Check if it's already V1 format
|
|
685
|
+
if (parsed.schema === "kb.v1") {
|
|
686
|
+
events.push(parsed as PlatformAnalyticsEvent);
|
|
687
|
+
} else {
|
|
688
|
+
// Legacy format - convert to V1
|
|
689
|
+
const legacy = parsed as StoredEventLegacy;
|
|
690
|
+
const mapped = this.mapLegacyToPlatformEvent(legacy);
|
|
691
|
+
events.push(mapped);
|
|
692
|
+
}
|
|
693
|
+
} catch (error) {
|
|
694
|
+
// Skip invalid lines
|
|
695
|
+
console.warn(`Failed to parse line in ${file}:`, error);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return events;
|
|
701
|
+
} catch (error) {
|
|
702
|
+
console.warn("Failed to read events:", error);
|
|
703
|
+
return [];
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Map legacy stored event format to platform AnalyticsEvent format (kb.v1 schema)
|
|
709
|
+
* Used for backward compatibility with old events.
|
|
710
|
+
*/
|
|
711
|
+
private mapLegacyToPlatformEvent(
|
|
712
|
+
stored: StoredEventLegacy,
|
|
713
|
+
): PlatformAnalyticsEvent {
|
|
714
|
+
// Extract actor info from properties if available
|
|
715
|
+
const userId = stored.properties?.userId as string | undefined;
|
|
716
|
+
const actorType = stored.properties?.actorType as
|
|
717
|
+
| "user"
|
|
718
|
+
| "agent"
|
|
719
|
+
| "ci"
|
|
720
|
+
| undefined;
|
|
721
|
+
const actorName = stored.properties?.actorName as string | undefined;
|
|
722
|
+
|
|
723
|
+
// Extract source info from properties if available
|
|
724
|
+
const sourceProduct =
|
|
725
|
+
(stored.properties?.source as string) || "file-analytics";
|
|
726
|
+
const sourceVersion = (stored.properties?.version as string) || "0.1.0";
|
|
727
|
+
|
|
728
|
+
// Extract runId from properties if available
|
|
729
|
+
const runId = (stored.properties?.runId as string) || randomUUID();
|
|
730
|
+
|
|
731
|
+
// Build actor object
|
|
732
|
+
const actor =
|
|
733
|
+
userId || actorType
|
|
734
|
+
? {
|
|
735
|
+
type: actorType || "user",
|
|
736
|
+
id: userId,
|
|
737
|
+
name: actorName,
|
|
738
|
+
}
|
|
739
|
+
: undefined;
|
|
740
|
+
|
|
741
|
+
// Build context from properties
|
|
742
|
+
const ctx: Record<string, string | number | boolean | null> = {};
|
|
743
|
+
if (stored.properties) {
|
|
744
|
+
for (const [key, value] of Object.entries(stored.properties)) {
|
|
745
|
+
// Skip internal fields
|
|
746
|
+
if (
|
|
747
|
+
[
|
|
748
|
+
"userId",
|
|
749
|
+
"actorType",
|
|
750
|
+
"actorName",
|
|
751
|
+
"source",
|
|
752
|
+
"version",
|
|
753
|
+
"runId",
|
|
754
|
+
].includes(key)
|
|
755
|
+
) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Only include primitive values in ctx
|
|
760
|
+
if (
|
|
761
|
+
typeof value === "string" ||
|
|
762
|
+
typeof value === "number" ||
|
|
763
|
+
typeof value === "boolean" ||
|
|
764
|
+
value === null
|
|
765
|
+
) {
|
|
766
|
+
ctx[key] = value;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
id: randomUUID(),
|
|
773
|
+
schema: "kb.v1" as const,
|
|
774
|
+
type: stored.name,
|
|
775
|
+
ts: stored.timestamp,
|
|
776
|
+
ingestTs: stored.timestamp,
|
|
777
|
+
source: {
|
|
778
|
+
product: sourceProduct,
|
|
779
|
+
version: sourceVersion,
|
|
780
|
+
},
|
|
781
|
+
runId,
|
|
782
|
+
actor,
|
|
783
|
+
ctx: Object.keys(ctx).length > 0 ? ctx : undefined,
|
|
784
|
+
payload:
|
|
785
|
+
stored.type === "metric" && stored.value !== undefined
|
|
786
|
+
? { value: stored.value }
|
|
787
|
+
: stored.properties,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function isAnalyticsContext(value: unknown): value is AnalyticsContext {
|
|
793
|
+
if (!value || typeof value !== "object") {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
const source = (value as { source?: unknown }).source;
|
|
797
|
+
if (!source || typeof source !== "object") {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
const product = (source as { product?: unknown }).product;
|
|
801
|
+
const version = (source as { version?: unknown }).version;
|
|
802
|
+
return typeof product === "string" && typeof version === "string";
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function createAdapter(
|
|
806
|
+
options?: FileAnalyticsOptions,
|
|
807
|
+
depsOrContext?: Record<string, unknown> | AnalyticsContext,
|
|
808
|
+
): IAnalytics {
|
|
809
|
+
const deps =
|
|
810
|
+
depsOrContext && !isAnalyticsContext(depsOrContext)
|
|
811
|
+
? depsOrContext
|
|
812
|
+
: undefined;
|
|
813
|
+
const cache = deps?.cache as ICache | undefined;
|
|
814
|
+
|
|
815
|
+
const legacyContext = isAnalyticsContext(depsOrContext)
|
|
816
|
+
? depsOrContext
|
|
817
|
+
: undefined;
|
|
818
|
+
const injectedContext = isAnalyticsContext(deps?.analytics)
|
|
819
|
+
? deps.analytics
|
|
820
|
+
: isAnalyticsContext(deps?.context)
|
|
821
|
+
? deps.context
|
|
822
|
+
: undefined;
|
|
823
|
+
|
|
824
|
+
const context = options?.analytics ?? injectedContext ?? legacyContext;
|
|
825
|
+
const adapterOptions = context ? { ...options, analytics: context } : options;
|
|
826
|
+
|
|
827
|
+
return new FileAnalytics(adapterOptions, context, cache);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export default createAdapter;
|