@obsunified/collector 2.0.1
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/LICENSE +21 -0
- package/README.md +60 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +9 -0
- package/dist/auth.js.map +1 -0
- package/dist/chunk-UGWDOGGH.js +319 -0
- package/dist/chunk-UGWDOGGH.js.map +1 -0
- package/dist/chunk-XKQTVIRE.js +671 -0
- package/dist/chunk-XKQTVIRE.js.map +1 -0
- package/dist/index.d.ts +1197 -0
- package/dist/index.js +17707 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
- package/dist/openai-FJYOQ7WO.js +384 -0
- package/dist/openai-FJYOQ7WO.js.map +1 -0
- package/package.json +73 -0
- package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
- package/src/analyses/derive.ts +642 -0
- package/src/analyses/index.ts +50 -0
- package/src/analyses/investigations.ts +277 -0
- package/src/analyses/tier0.ts +568 -0
- package/src/auth/dashboard-auth.ts +269 -0
- package/src/auth/ingest-auth.ts +147 -0
- package/src/durable-objects/tail-hub.test.ts +32 -0
- package/src/durable-objects/tail-hub.ts +158 -0
- package/src/framework/collector.ts +467 -0
- package/src/framework/env.ts +90 -0
- package/src/framework/logger.ts +81 -0
- package/src/index.ts +211 -0
- package/src/lib/action-aggregates.ts +1031 -0
- package/src/lib/action-identity.ts +106 -0
- package/src/lib/actions-store.ts +397 -0
- package/src/lib/ai-pricing.ts +80 -0
- package/src/lib/ai-store/calls.ts +148 -0
- package/src/lib/ai-store/evaluations.test.ts +47 -0
- package/src/lib/ai-store/evaluations.ts +125 -0
- package/src/lib/ai-store/helpers.ts +37 -0
- package/src/lib/ai-store/retention.ts +17 -0
- package/src/lib/ai-store/sessions.ts +277 -0
- package/src/lib/ai-store/spans.ts +109 -0
- package/src/lib/ai-store/types.ts +112 -0
- package/src/lib/ai-store.ts +76 -0
- package/src/lib/alerts-store.ts +458 -0
- package/src/lib/analyses-runner.ts +439 -0
- package/src/lib/analyses-store.test.ts +243 -0
- package/src/lib/analyses-store.ts +454 -0
- package/src/lib/ask.test.ts +302 -0
- package/src/lib/ask.ts +390 -0
- package/src/lib/blob-store-s3.ts +213 -0
- package/src/lib/blob-store.ts +366 -0
- package/src/lib/eval-cases-store.test.ts +171 -0
- package/src/lib/eval-cases-store.ts +1033 -0
- package/src/lib/evidence-references.test.ts +10 -0
- package/src/lib/evidence-references.ts +684 -0
- package/src/lib/hash.ts +26 -0
- package/src/lib/identity-index/action-lookups.ts +805 -0
- package/src/lib/identity-index/constants.ts +1 -0
- package/src/lib/identity-index/key-lookups.ts +199 -0
- package/src/lib/identity-index/mappers.ts +349 -0
- package/src/lib/identity-index/types.ts +196 -0
- package/src/lib/identity-index/user-lookup.ts +137 -0
- package/src/lib/identity-index.test.ts +624 -0
- package/src/lib/identity-index.ts +85 -0
- package/src/lib/json.ts +39 -0
- package/src/lib/llm.ts +263 -0
- package/src/lib/logs-store.ts +188 -0
- package/src/lib/metrics-store.test.ts +189 -0
- package/src/lib/metrics-store.ts +312 -0
- package/src/lib/narrate-gate.test.ts +260 -0
- package/src/lib/narrate-gate.ts +253 -0
- package/src/lib/openai.test.ts +300 -0
- package/src/lib/openai.ts +512 -0
- package/src/lib/otlp.ts +212 -0
- package/src/lib/parse-pprof.test.ts +64 -0
- package/src/lib/parse-pprof.ts +184 -0
- package/src/lib/payload-capture.ts +22 -0
- package/src/lib/projects-store.ts +207 -0
- package/src/lib/propagation-metric.test.ts +196 -0
- package/src/lib/propagation-metric.ts +175 -0
- package/src/lib/sql-db-dialect.test.ts +22 -0
- package/src/lib/sql-db-postgres.ts +229 -0
- package/src/lib/sql-db.ts +166 -0
- package/src/lib/stage6.test.ts +216 -0
- package/src/lib/store/export.ts +65 -0
- package/src/lib/store/helpers.ts +367 -0
- package/src/lib/store/ingest.ts +68 -0
- package/src/lib/store/issues.ts +158 -0
- package/src/lib/store/overview.ts +127 -0
- package/src/lib/store/retention.ts +10 -0
- package/src/lib/store/service-map.ts +384 -0
- package/src/lib/store/trace-detail.bench.ts +76 -0
- package/src/lib/store/trace-detail.test.ts +410 -0
- package/src/lib/store/trace-detail.ts +427 -0
- package/src/lib/store/trace-queries.test.ts +29 -0
- package/src/lib/store/trace-queries.ts +89 -0
- package/src/lib/store-overview.test.ts +77 -0
- package/src/lib/store.ts +96 -0
- package/src/lib/tail-publisher.ts +64 -0
- package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
- package/src/lib/test-utils/mem-sql-db.ts +86 -0
- package/src/lib/ua-parser.ts +61 -0
- package/src/lib/usage-store.ts +567 -0
- package/src/migrations/001_telemetry_spans.sql +28 -0
- package/src/migrations/002_usage_events.sql +24 -0
- package/src/migrations/003_usage_analytics_columns.sql +13 -0
- package/src/migrations/004_logs.sql +19 -0
- package/src/migrations/005_ai_calls.sql +25 -0
- package/src/migrations/006_user_profiles.sql +11 -0
- package/src/migrations/007_session_replay_metadata.sql +10 -0
- package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
- package/src/migrations/009_projects_and_keys.sql +22 -0
- package/src/migrations/010_signal_project_id.sql +15 -0
- package/src/migrations/011_default_project_seed.sql +3 -0
- package/src/migrations/012_alert_rules.sql +17 -0
- package/src/migrations/013_alert_evaluations.sql +13 -0
- package/src/migrations/014_alert_state.sql +9 -0
- package/src/migrations/015_metrics.sql +58 -0
- package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
- package/src/migrations/017_span_dropped_counts.sql +11 -0
- package/src/migrations/018_log_flags.sql +8 -0
- package/src/migrations/019_ai_span_payloads.sql +30 -0
- package/src/migrations/020_ai_span_evaluations.sql +33 -0
- package/src/migrations/021_ai_span_session.sql +13 -0
- package/src/migrations/022_span_log_session_id.sql +13 -0
- package/src/migrations/023_service_map_indexes.sql +32 -0
- package/src/migrations/024_analysis_results.sql +49 -0
- package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
- package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
- package/src/migrations/027_identity_propagation.sql +50 -0
- package/src/migrations/028_profile_blobs.sql +57 -0
- package/src/migrations/029_telemetry_sdk_name.sql +20 -0
- package/src/migrations/030_analysis_claim.sql +18 -0
- package/src/migrations/031_agent_action_graph.sql +154 -0
- package/src/migrations/032_payload_capture_flags.sql +8 -0
- package/src/migrations/033_eval_cases.sql +52 -0
- package/src/migrations/034_eval_case_results.sql +23 -0
- package/src/migrations/035_metric_exemplars.sql +33 -0
- package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations/037_eval_runs.sql +37 -0
- package/src/migrations/038_tool_call_evidence.sql +11 -0
- package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
- package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
- package/src/migrations-postgres/002_usage_events.sql +24 -0
- package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
- package/src/migrations-postgres/004_logs.sql +19 -0
- package/src/migrations-postgres/005_ai_calls.sql +25 -0
- package/src/migrations-postgres/006_user_profiles.sql +11 -0
- package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
- package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
- package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
- package/src/migrations-postgres/010_signal_project_id.sql +18 -0
- package/src/migrations-postgres/011_default_project_seed.sql +5 -0
- package/src/migrations-postgres/012_alert_rules.sql +17 -0
- package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
- package/src/migrations-postgres/014_alert_state.sql +9 -0
- package/src/migrations-postgres/015_metrics.sql +47 -0
- package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
- package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
- package/src/migrations-postgres/018_log_flags.sql +2 -0
- package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
- package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
- package/src/migrations-postgres/021_ai_span_session.sql +7 -0
- package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
- package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
- package/src/migrations-postgres/024_analysis_results.sql +41 -0
- package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
- package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
- package/src/migrations-postgres/027_identity_propagation.sql +27 -0
- package/src/migrations-postgres/028_profile_blobs.sql +48 -0
- package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
- package/src/migrations-postgres/030_analysis_claim.sql +5 -0
- package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
- package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
- package/src/migrations-postgres/033_eval_cases.sql +55 -0
- package/src/migrations-postgres/034_eval_case_results.sql +26 -0
- package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
- package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
- package/src/migrations-postgres/037_eval_runs.sql +40 -0
- package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
- package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
- package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
- package/src/migrations-postgres/README.md +40 -0
- package/src/otlp/acceptance.test.ts +373 -0
- package/src/otlp/decode/body.ts +127 -0
- package/src/otlp/decode/logs.ts +96 -0
- package/src/otlp/decode/metrics.ts +402 -0
- package/src/otlp/decode/traces.ts +92 -0
- package/src/otlp/decode/values.ts +150 -0
- package/src/otlp/decode.test.ts +800 -0
- package/src/otlp/decode.ts +11 -0
- package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
- package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
- package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
- package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
- package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
- package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
- package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
- package/src/otlp/response.test.ts +158 -0
- package/src/otlp/response.ts +129 -0
- package/src/plugins/_context.ts +15 -0
- package/src/plugins/action-graph-processor/enrichers.ts +45 -0
- package/src/plugins/action-graph-processor/redaction.ts +84 -0
- package/src/plugins/action-graph-processor.test.ts +1023 -0
- package/src/plugins/action-graph-processor.ts +1172 -0
- package/src/plugins/action-routes.test.ts +716 -0
- package/src/plugins/action-routes.ts +343 -0
- package/src/plugins/ai-receiver.ts +254 -0
- package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
- package/src/plugins/ai-span-payloads-processor.ts +180 -0
- package/src/plugins/alerts-evaluator.test.ts +91 -0
- package/src/plugins/alerts-evaluator.ts +328 -0
- package/src/plugins/alerts-routes.ts +101 -0
- package/src/plugins/analyses-routes.ts +138 -0
- package/src/plugins/ask-routes.ts +182 -0
- package/src/plugins/bot-filter.ts +19 -0
- package/src/plugins/connected-routes/manifest.ts +531 -0
- package/src/plugins/connected-routes.test.ts +1791 -0
- package/src/plugins/connected-routes.ts +1103 -0
- package/src/plugins/dashboard-routes.ts +41 -0
- package/src/plugins/default-span-enrichment.ts +92 -0
- package/src/plugins/eval-cases-routes.test.ts +391 -0
- package/src/plugins/eval-cases-routes.ts +529 -0
- package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
- package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
- package/src/plugins/evidence-retrieval-routes.ts +2179 -0
- package/src/plugins/gen-ai-normalizer.test.ts +680 -0
- package/src/plugins/gen-ai-normalizer.ts +302 -0
- package/src/plugins/identity-receiver.ts +96 -0
- package/src/plugins/issue-enrichment.ts +132 -0
- package/src/plugins/issue-insights.ts +81 -0
- package/src/plugins/logs-receiver.ts +160 -0
- package/src/plugins/metrics-receiver.ts +91 -0
- package/src/plugins/onboarding-routes.ts +54 -0
- package/src/plugins/otlp-receiver.ts +124 -0
- package/src/plugins/platform-routes.ts +165 -0
- package/src/plugins/profile-routes.ts +468 -0
- package/src/plugins/projects-routes.ts +92 -0
- package/src/plugins/query-routes.ts +220 -0
- package/src/plugins/redaction-processor.ts +132 -0
- package/src/plugins/replay-query-routes.test.ts +38 -0
- package/src/plugins/replay-query-routes.ts +199 -0
- package/src/plugins/replay-receiver.ts +81 -0
- package/src/plugins/tail-routes.test.ts +47 -0
- package/src/plugins/tail-routes.ts +42 -0
- package/src/plugins/timeline-routes.ts +244 -0
- package/src/plugins/ua-enrichment.ts +27 -0
- package/src/plugins/usage-normalization.ts +46 -0
- package/src/plugins/usage-privacy.ts +55 -0
- package/src/plugins/usage-query-routes.ts +102 -0
- package/src/plugins/usage-receiver.ts +129 -0
- package/src/plugins/users-query-routes.ts +78 -0
- package/src/plugins/utm-enrichment.ts +36 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage seam for the collector — RFC 0008.
|
|
3
|
+
*
|
|
4
|
+
* Stores and plugins target this interface instead of `D1Database` directly,
|
|
5
|
+
* so a future Node / better-sqlite3 / ClickHouse runtime is one new adapter
|
|
6
|
+
* away rather than a fork-and-rewrite. The shape is deliberately D1's
|
|
7
|
+
* existing `prepare → bind → first|all|run` chain so the migration of
|
|
8
|
+
* existing stores is mechanical (search-and-replace the constructor type).
|
|
9
|
+
*
|
|
10
|
+
* Today there is one implementation, `D1Adapter`. A `MemSqlDb` test double
|
|
11
|
+
* lives next door under `test-utils/`; `BetterSqliteAdapter` and
|
|
12
|
+
* `ClickHouseAdapter` are deferred until they have a real runtime to serve.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface SqlDb {
|
|
16
|
+
readonly dialect?: SqlDialect;
|
|
17
|
+
prepare(sql: string): SqlStatement;
|
|
18
|
+
/**
|
|
19
|
+
* Run multiple prepared statements as a batch. On D1 this maps to a
|
|
20
|
+
* single network round-trip; on a future better-sqlite3 adapter it
|
|
21
|
+
* wraps the calls in a transaction. Returns one result per statement
|
|
22
|
+
* in input order, mirroring D1's `batch`.
|
|
23
|
+
*
|
|
24
|
+
* Statements MUST have been produced by this same `SqlDb` instance —
|
|
25
|
+
* mixing adapters is an error and runtime behavior is undefined.
|
|
26
|
+
*/
|
|
27
|
+
batch(
|
|
28
|
+
statements: SqlStatement[],
|
|
29
|
+
): Promise<Array<{ meta: { changes: number } }>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SqlDialectName = "sqlite" | "postgres";
|
|
33
|
+
|
|
34
|
+
export interface SqlDialect {
|
|
35
|
+
readonly name: SqlDialectName;
|
|
36
|
+
now(): string;
|
|
37
|
+
sinceHours(param: string): string;
|
|
38
|
+
sinceMinutes(param: string): string;
|
|
39
|
+
jsonText(expr: string, path: string): string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SqlStatement {
|
|
43
|
+
bind(...args: unknown[]): SqlStatement;
|
|
44
|
+
first<T = Record<string, unknown>>(): Promise<T | null>;
|
|
45
|
+
all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;
|
|
46
|
+
run(): Promise<{ meta: { changes: number } }>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* D1 already conforms to this shape almost exactly; the adapter normalizes
|
|
51
|
+
* two small differences:
|
|
52
|
+
* - `all()` on D1 returns `results?: T[]` (optional). We coerce to `[]`.
|
|
53
|
+
* - `run()`'s `meta.changes` is optional on D1. We coerce to `0`.
|
|
54
|
+
* Both differences are runtime-undetectable for the queries this codebase
|
|
55
|
+
* issues (every successful all() returns results; every run() against an
|
|
56
|
+
* INSERT/UPDATE/DELETE reports changes), but coercing keeps the typed
|
|
57
|
+
* interface honest.
|
|
58
|
+
*/
|
|
59
|
+
export class D1Adapter implements SqlDb {
|
|
60
|
+
readonly dialect = sqliteDialect;
|
|
61
|
+
|
|
62
|
+
constructor(private readonly d1: D1Database) {}
|
|
63
|
+
|
|
64
|
+
prepare(sql: string): SqlStatement {
|
|
65
|
+
return new D1StatementAdapter(this.d1.prepare(sql));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async batch(
|
|
69
|
+
statements: SqlStatement[],
|
|
70
|
+
): Promise<Array<{ meta: { changes: number } }>> {
|
|
71
|
+
// Unwrap each statement back to its D1 form — D1's batch takes
|
|
72
|
+
// D1PreparedStatement[], not our typed wrapper.
|
|
73
|
+
const unwrapped = statements.map((s) => {
|
|
74
|
+
if (!(s instanceof D1StatementAdapter)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"D1Adapter.batch: statement was not produced by D1Adapter",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return s.unwrap();
|
|
80
|
+
});
|
|
81
|
+
const results = await this.d1.batch(unwrapped);
|
|
82
|
+
return results.map((r) => ({
|
|
83
|
+
meta: { changes: r.meta?.changes ?? 0 },
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Convenience wrapper for the common case: take a `CollectorEnv`-shaped
|
|
90
|
+
* object and return an `SqlDb`. Most plugins don't have access to the
|
|
91
|
+
* `CollectorRuntime` (they get it at register time, not handler time)
|
|
92
|
+
* so they call this directly. Plugins that *do* hold the runtime should
|
|
93
|
+
* prefer `runtime.getSqlDb(env)` so a host-supplied `sqlDb` factory in
|
|
94
|
+
* `CollectorConfig` takes effect.
|
|
95
|
+
*/
|
|
96
|
+
export const sqlDbFor = (env: { DB: D1Database | SqlDb }): SqlDb =>
|
|
97
|
+
isSqlDb(env.DB) ? env.DB : new D1Adapter(env.DB);
|
|
98
|
+
|
|
99
|
+
export const sqliteDialect: SqlDialect = {
|
|
100
|
+
name: "sqlite",
|
|
101
|
+
now: () => "datetime('now')",
|
|
102
|
+
sinceHours: (param) => `datetime('now', '-' || ${param} || ' hours')`,
|
|
103
|
+
sinceMinutes: (param) => `datetime('now', '-' || ${param} || ' minutes')`,
|
|
104
|
+
jsonText: (expr, path) => `json_extract(${expr}, '${path}')`,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const postgresDialect: SqlDialect = {
|
|
108
|
+
name: "postgres",
|
|
109
|
+
now: () => "CURRENT_TIMESTAMP",
|
|
110
|
+
sinceHours: (param) =>
|
|
111
|
+
`(CURRENT_TIMESTAMP - (${param}::text || ' hours')::interval)`,
|
|
112
|
+
sinceMinutes: (param) =>
|
|
113
|
+
`(CURRENT_TIMESTAMP - (${param}::text || ' minutes')::interval)`,
|
|
114
|
+
jsonText: (expr, path) => {
|
|
115
|
+
const key = path
|
|
116
|
+
.replace(/^\$\./, "")
|
|
117
|
+
.replace(/^"|"$/g, "")
|
|
118
|
+
.replace(/\\u002E/g, ".");
|
|
119
|
+
return `(${expr}::jsonb ->> '${key.replace(/'/g, "''")}')`;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const dialectFor = (db: SqlDb): SqlDialect =>
|
|
124
|
+
db.dialect ?? sqliteDialect;
|
|
125
|
+
|
|
126
|
+
const isSqlDb = (db: unknown): db is SqlDb => {
|
|
127
|
+
if (!db || typeof db !== "object") return false;
|
|
128
|
+
const candidate = db as {
|
|
129
|
+
prepare?: unknown;
|
|
130
|
+
batch?: unknown;
|
|
131
|
+
exec?: unknown;
|
|
132
|
+
};
|
|
133
|
+
return (
|
|
134
|
+
typeof candidate.prepare === "function" &&
|
|
135
|
+
typeof candidate.batch === "function" &&
|
|
136
|
+
typeof candidate.exec !== "function"
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
class D1StatementAdapter implements SqlStatement {
|
|
141
|
+
constructor(private readonly stmt: D1PreparedStatement) {}
|
|
142
|
+
|
|
143
|
+
/** Internal escape hatch for `D1Adapter.batch` to recover the wrapped
|
|
144
|
+
* D1 statement. Not part of the public `SqlStatement` shape. */
|
|
145
|
+
unwrap(): D1PreparedStatement {
|
|
146
|
+
return this.stmt;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
bind(...args: unknown[]): SqlStatement {
|
|
150
|
+
return new D1StatementAdapter(this.stmt.bind(...args));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
first<T = Record<string, unknown>>(): Promise<T | null> {
|
|
154
|
+
return this.stmt.first<T>();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async all<T = Record<string, unknown>>(): Promise<{ results: T[] }> {
|
|
158
|
+
const r = await this.stmt.all<T>();
|
|
159
|
+
return { results: r.results ?? [] };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async run(): Promise<{ meta: { changes: number } }> {
|
|
163
|
+
const r = await this.stmt.run();
|
|
164
|
+
return { meta: { changes: r.meta?.changes ?? 0 } };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0002 Stage 6 — auto-pinning + alert→analysis binding behaviour tests.
|
|
3
|
+
*
|
|
4
|
+
* Uses the shared MemSqlDb test double from RFC 0008. The test asserts on
|
|
5
|
+
* the *decisions* — which SQL we issue, which value the evaluator returns —
|
|
6
|
+
* not the SQL grammar.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { AlertRule } from "@obsunified/types";
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
import { AlertsStore } from "./alerts-store";
|
|
12
|
+
import { AnalysesStore } from "./analyses-store";
|
|
13
|
+
import { MemSqlDb } from "./test-utils/mem-sql-db";
|
|
14
|
+
|
|
15
|
+
// ── Auto-pin ──
|
|
16
|
+
|
|
17
|
+
describe("AnalysesStore.recordAskEvidence", () => {
|
|
18
|
+
it("inserts one row per unique analysis id", async () => {
|
|
19
|
+
const db = new MemSqlDb();
|
|
20
|
+
const store = new AnalysesStore(db);
|
|
21
|
+
await store.recordAskEvidence("default", [
|
|
22
|
+
"a",
|
|
23
|
+
"b",
|
|
24
|
+
"a", // dedupe
|
|
25
|
+
]);
|
|
26
|
+
const inserts = db.callsMatching("INSERT INTO ask_evidence_events");
|
|
27
|
+
expect(inserts).toHaveLength(2);
|
|
28
|
+
expect(inserts.map((c) => c.binds[1])).toEqual(
|
|
29
|
+
expect.arrayContaining(["a", "b"]),
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("is a no-op when no ids supplied", async () => {
|
|
34
|
+
const db = new MemSqlDb();
|
|
35
|
+
const store = new AnalysesStore(db);
|
|
36
|
+
await store.recordAskEvidence("default", []);
|
|
37
|
+
expect(db.calls).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("AnalysesStore.getTopAskedAnalyses", () => {
|
|
42
|
+
it("orders by citation count desc, returns analysisId+citations", async () => {
|
|
43
|
+
const db = new MemSqlDb({
|
|
44
|
+
all: () => [
|
|
45
|
+
{ analysis_id: "service_error_rate::checkout", citations: 7 },
|
|
46
|
+
{ analysis_id: "overall_error_rate", citations: 3 },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
const store = new AnalysesStore(db);
|
|
50
|
+
const top = await store.getTopAskedAnalyses("default");
|
|
51
|
+
expect(top).toEqual([
|
|
52
|
+
{ analysisId: "service_error_rate::checkout", citations: 7 },
|
|
53
|
+
{ analysisId: "overall_error_rate", citations: 3 },
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ── Alert binding ──
|
|
59
|
+
|
|
60
|
+
const baseRule = (overrides: Partial<AlertRule> = {}): AlertRule => ({
|
|
61
|
+
id: "r1",
|
|
62
|
+
projectId: "default",
|
|
63
|
+
name: "checkout errors",
|
|
64
|
+
signal: "spans",
|
|
65
|
+
query: { kind: "spans" } as unknown as AlertRule["query"],
|
|
66
|
+
threshold: 1,
|
|
67
|
+
windowMins: 5,
|
|
68
|
+
comparison: ">",
|
|
69
|
+
channels: [],
|
|
70
|
+
enabled: true,
|
|
71
|
+
createdAt: "2026-04-29T00:00:00Z",
|
|
72
|
+
updatedAt: "2026-04-29T00:00:00Z",
|
|
73
|
+
...overrides,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("AlertsStore.evaluateRule with analysisId binding", () => {
|
|
77
|
+
it("reads analysis_results.primary_value when analysisId is set", async () => {
|
|
78
|
+
const db = new MemSqlDb({
|
|
79
|
+
first: (sql) => {
|
|
80
|
+
if (sql.includes("FROM analysis_results")) return { primary_value: 42 };
|
|
81
|
+
return null;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
const store = new AlertsStore(db);
|
|
85
|
+
const rule = baseRule({ analysisId: "overall_error_rate" });
|
|
86
|
+
const value = await store.evaluateRule(rule);
|
|
87
|
+
expect(value).toBe(42);
|
|
88
|
+
// And we never fell through to the spans evaluator.
|
|
89
|
+
expect(db.callsMatching("FROM telemetry_spans")).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns 0 when the analysis has no result yet (rule treats as 'no signal')", async () => {
|
|
93
|
+
const db = new MemSqlDb({
|
|
94
|
+
first: () => null,
|
|
95
|
+
});
|
|
96
|
+
const store = new AlertsStore(db);
|
|
97
|
+
const rule = baseRule({ analysisId: "brand_new_analysis" });
|
|
98
|
+
const value = await store.evaluateRule(rule);
|
|
99
|
+
expect(value).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("falls through to legacy signal evaluator when analysisId is not set", async () => {
|
|
103
|
+
const db = new MemSqlDb({
|
|
104
|
+
first: (sql) => {
|
|
105
|
+
if (sql.includes("FROM telemetry_spans")) return { c: 5 };
|
|
106
|
+
return null;
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const store = new AlertsStore(db);
|
|
110
|
+
const rule = baseRule({ analysisId: null });
|
|
111
|
+
const value = await store.evaluateRule(rule);
|
|
112
|
+
expect(value).toBe(5);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("AlertsStore.getAnalysisNarrative", () => {
|
|
117
|
+
it("returns the most recent narrative + status row", async () => {
|
|
118
|
+
const db = new MemSqlDb({
|
|
119
|
+
first: () => ({
|
|
120
|
+
narrative: "p95 jumped to 320ms (was 90ms) starting 8m ago",
|
|
121
|
+
status: "warn",
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
const store = new AlertsStore(db);
|
|
125
|
+
const out = await store.getAnalysisNarrative(
|
|
126
|
+
"default",
|
|
127
|
+
"latency_p95_overall",
|
|
128
|
+
);
|
|
129
|
+
expect(out).toEqual({
|
|
130
|
+
narrative: "p95 jumped to 320ms (was 90ms) starting 8m ago",
|
|
131
|
+
status: "warn",
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("returns null when there's no result yet", async () => {
|
|
136
|
+
const db = new MemSqlDb({ first: () => null });
|
|
137
|
+
const store = new AlertsStore(db);
|
|
138
|
+
const out = await store.getAnalysisNarrative("default", "nope");
|
|
139
|
+
expect(out).toBeNull();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("AlertsStore.listEvaluations evidence references", () => {
|
|
144
|
+
it("hoists alert and bound-analysis evidence references to the list response", async () => {
|
|
145
|
+
const db = new MemSqlDb({
|
|
146
|
+
first: (sql) => {
|
|
147
|
+
if (sql.includes("FROM alert_rules")) {
|
|
148
|
+
return {
|
|
149
|
+
id: "rule-1",
|
|
150
|
+
project_id: "default",
|
|
151
|
+
name: "Checkout latency",
|
|
152
|
+
signal: "spans",
|
|
153
|
+
query_json: "{}",
|
|
154
|
+
threshold: 100,
|
|
155
|
+
window_mins: 5,
|
|
156
|
+
comparison: ">",
|
|
157
|
+
channels_json: "[]",
|
|
158
|
+
enabled: 1,
|
|
159
|
+
created_at: "2026-05-01T00:00:00.000Z",
|
|
160
|
+
updated_at: "2026-05-01T00:00:00.000Z",
|
|
161
|
+
analysis_id: "latency.p95",
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
},
|
|
166
|
+
all: (sql) => {
|
|
167
|
+
if (sql.includes("FROM alert_evaluations")) {
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
id: "eval-1",
|
|
171
|
+
rule_id: "rule-1",
|
|
172
|
+
project_id: "default",
|
|
173
|
+
evaluated_at: "2026-05-01T00:05:00.000Z",
|
|
174
|
+
value: 320,
|
|
175
|
+
state: "firing",
|
|
176
|
+
notified: 1,
|
|
177
|
+
},
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
return [];
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
const store = new AlertsStore(db);
|
|
184
|
+
|
|
185
|
+
const response = await store.listEvaluations({
|
|
186
|
+
ruleId: "rule-1",
|
|
187
|
+
projectId: "default",
|
|
188
|
+
hours: 24,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(response.evaluations).toEqual([
|
|
192
|
+
expect.objectContaining({
|
|
193
|
+
id: "eval-1",
|
|
194
|
+
value: 320,
|
|
195
|
+
state: "firing",
|
|
196
|
+
}),
|
|
197
|
+
]);
|
|
198
|
+
expect(response.evaluations[0].evidenceReferences).toBeUndefined();
|
|
199
|
+
expect(response.evidenceReferences).toEqual([
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
evidenceId: "alert:rule-1:rule",
|
|
202
|
+
entityKind: "alert",
|
|
203
|
+
entityId: "rule-1",
|
|
204
|
+
route: "#/alerts?alert=rule-1",
|
|
205
|
+
source: "alerts.rule_evaluation_preview",
|
|
206
|
+
}),
|
|
207
|
+
expect.objectContaining({
|
|
208
|
+
evidenceId: "alert:rule-1:analysis:latency.p95",
|
|
209
|
+
entityKind: "analysis",
|
|
210
|
+
entityId: "latency.p95",
|
|
211
|
+
route: "#/investigate/latency.p95",
|
|
212
|
+
source: "alert_rules.analysis_id",
|
|
213
|
+
}),
|
|
214
|
+
]);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { TelemetryOverviewOptions } from "@obsunified/types";
|
|
2
|
+
import type { SqlDb } from "../sql-db";
|
|
3
|
+
import { cutoffIso } from "./helpers";
|
|
4
|
+
|
|
5
|
+
export async function getTelemetryExportRows(
|
|
6
|
+
db: SqlDb,
|
|
7
|
+
options: TelemetryOverviewOptions,
|
|
8
|
+
): Promise<string> {
|
|
9
|
+
if (!options.projectId)
|
|
10
|
+
throw new Error("TelemetryStore.getExportRows: projectId is required");
|
|
11
|
+
const cutoff = cutoffIso(options.hours);
|
|
12
|
+
const now = new Date().toISOString();
|
|
13
|
+
let whereClause =
|
|
14
|
+
"WHERE project_id = ? AND received_at > ? AND expires_at > ?";
|
|
15
|
+
const params: unknown[] = [options.projectId, cutoff, now];
|
|
16
|
+
|
|
17
|
+
if (options.service) {
|
|
18
|
+
whereClause += " AND service_name = ?";
|
|
19
|
+
params.push(options.service);
|
|
20
|
+
}
|
|
21
|
+
if (options.status === "error") {
|
|
22
|
+
whereClause += " AND status_code = 2";
|
|
23
|
+
} else if (options.status === "ok") {
|
|
24
|
+
whereClause += " AND status_code != 2";
|
|
25
|
+
}
|
|
26
|
+
if (options.search) {
|
|
27
|
+
const term = `%${options.search}%`;
|
|
28
|
+
whereClause +=
|
|
29
|
+
" AND (span_name LIKE ? OR status_message LIKE ? OR attributes_json LIKE ? OR events_json LIKE ?)";
|
|
30
|
+
params.push(term, term, term, term);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rows = await db
|
|
34
|
+
.prepare(`
|
|
35
|
+
SELECT trace_id, span_id, parent_span_id, service_name, span_name,
|
|
36
|
+
status_code, status_message, start_time, end_time, duration_ms,
|
|
37
|
+
attributes_json, events_json, received_at
|
|
38
|
+
FROM telemetry_spans
|
|
39
|
+
${whereClause}
|
|
40
|
+
ORDER BY received_at DESC
|
|
41
|
+
LIMIT 1000
|
|
42
|
+
`)
|
|
43
|
+
.bind(...params)
|
|
44
|
+
.all<Record<string, unknown>>();
|
|
45
|
+
|
|
46
|
+
return (rows.results || [])
|
|
47
|
+
.map((row) =>
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
trace_id: row.trace_id,
|
|
50
|
+
span_id: row.span_id,
|
|
51
|
+
parent_span_id: row.parent_span_id,
|
|
52
|
+
service: row.service_name,
|
|
53
|
+
span_name: row.span_name,
|
|
54
|
+
status_code: row.status_code,
|
|
55
|
+
status_message: row.status_message,
|
|
56
|
+
start_time: row.start_time,
|
|
57
|
+
end_time: row.end_time,
|
|
58
|
+
duration_ms: row.duration_ms,
|
|
59
|
+
attributes: JSON.parse((row.attributes_json as string) || "{}"),
|
|
60
|
+
events: JSON.parse((row.events_json as string) || "[]"),
|
|
61
|
+
received_at: row.received_at,
|
|
62
|
+
}),
|
|
63
|
+
)
|
|
64
|
+
.join("\n");
|
|
65
|
+
}
|