@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.
Files changed (254) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +60 -0
  3. package/dist/auth.d.ts +2 -0
  4. package/dist/auth.js +9 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/chunk-UGWDOGGH.js +319 -0
  7. package/dist/chunk-UGWDOGGH.js.map +1 -0
  8. package/dist/chunk-XKQTVIRE.js +671 -0
  9. package/dist/chunk-XKQTVIRE.js.map +1 -0
  10. package/dist/index.d.ts +1197 -0
  11. package/dist/index.js +17707 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/ingest-auth-HFVO1zIp.d.ts +165 -0
  14. package/dist/openai-FJYOQ7WO.js +384 -0
  15. package/dist/openai-FJYOQ7WO.js.map +1 -0
  16. package/package.json +73 -0
  17. package/src/ai-parity/ai-parity.acceptance.test.ts +449 -0
  18. package/src/analyses/derive.ts +642 -0
  19. package/src/analyses/index.ts +50 -0
  20. package/src/analyses/investigations.ts +277 -0
  21. package/src/analyses/tier0.ts +568 -0
  22. package/src/auth/dashboard-auth.ts +269 -0
  23. package/src/auth/ingest-auth.ts +147 -0
  24. package/src/durable-objects/tail-hub.test.ts +32 -0
  25. package/src/durable-objects/tail-hub.ts +158 -0
  26. package/src/framework/collector.ts +467 -0
  27. package/src/framework/env.ts +90 -0
  28. package/src/framework/logger.ts +81 -0
  29. package/src/index.ts +211 -0
  30. package/src/lib/action-aggregates.ts +1031 -0
  31. package/src/lib/action-identity.ts +106 -0
  32. package/src/lib/actions-store.ts +397 -0
  33. package/src/lib/ai-pricing.ts +80 -0
  34. package/src/lib/ai-store/calls.ts +148 -0
  35. package/src/lib/ai-store/evaluations.test.ts +47 -0
  36. package/src/lib/ai-store/evaluations.ts +125 -0
  37. package/src/lib/ai-store/helpers.ts +37 -0
  38. package/src/lib/ai-store/retention.ts +17 -0
  39. package/src/lib/ai-store/sessions.ts +277 -0
  40. package/src/lib/ai-store/spans.ts +109 -0
  41. package/src/lib/ai-store/types.ts +112 -0
  42. package/src/lib/ai-store.ts +76 -0
  43. package/src/lib/alerts-store.ts +458 -0
  44. package/src/lib/analyses-runner.ts +439 -0
  45. package/src/lib/analyses-store.test.ts +243 -0
  46. package/src/lib/analyses-store.ts +454 -0
  47. package/src/lib/ask.test.ts +302 -0
  48. package/src/lib/ask.ts +390 -0
  49. package/src/lib/blob-store-s3.ts +213 -0
  50. package/src/lib/blob-store.ts +366 -0
  51. package/src/lib/eval-cases-store.test.ts +171 -0
  52. package/src/lib/eval-cases-store.ts +1033 -0
  53. package/src/lib/evidence-references.test.ts +10 -0
  54. package/src/lib/evidence-references.ts +684 -0
  55. package/src/lib/hash.ts +26 -0
  56. package/src/lib/identity-index/action-lookups.ts +805 -0
  57. package/src/lib/identity-index/constants.ts +1 -0
  58. package/src/lib/identity-index/key-lookups.ts +199 -0
  59. package/src/lib/identity-index/mappers.ts +349 -0
  60. package/src/lib/identity-index/types.ts +196 -0
  61. package/src/lib/identity-index/user-lookup.ts +137 -0
  62. package/src/lib/identity-index.test.ts +624 -0
  63. package/src/lib/identity-index.ts +85 -0
  64. package/src/lib/json.ts +39 -0
  65. package/src/lib/llm.ts +263 -0
  66. package/src/lib/logs-store.ts +188 -0
  67. package/src/lib/metrics-store.test.ts +189 -0
  68. package/src/lib/metrics-store.ts +312 -0
  69. package/src/lib/narrate-gate.test.ts +260 -0
  70. package/src/lib/narrate-gate.ts +253 -0
  71. package/src/lib/openai.test.ts +300 -0
  72. package/src/lib/openai.ts +512 -0
  73. package/src/lib/otlp.ts +212 -0
  74. package/src/lib/parse-pprof.test.ts +64 -0
  75. package/src/lib/parse-pprof.ts +184 -0
  76. package/src/lib/payload-capture.ts +22 -0
  77. package/src/lib/projects-store.ts +207 -0
  78. package/src/lib/propagation-metric.test.ts +196 -0
  79. package/src/lib/propagation-metric.ts +175 -0
  80. package/src/lib/sql-db-dialect.test.ts +22 -0
  81. package/src/lib/sql-db-postgres.ts +229 -0
  82. package/src/lib/sql-db.ts +166 -0
  83. package/src/lib/stage6.test.ts +216 -0
  84. package/src/lib/store/export.ts +65 -0
  85. package/src/lib/store/helpers.ts +367 -0
  86. package/src/lib/store/ingest.ts +68 -0
  87. package/src/lib/store/issues.ts +158 -0
  88. package/src/lib/store/overview.ts +127 -0
  89. package/src/lib/store/retention.ts +10 -0
  90. package/src/lib/store/service-map.ts +384 -0
  91. package/src/lib/store/trace-detail.bench.ts +76 -0
  92. package/src/lib/store/trace-detail.test.ts +410 -0
  93. package/src/lib/store/trace-detail.ts +427 -0
  94. package/src/lib/store/trace-queries.test.ts +29 -0
  95. package/src/lib/store/trace-queries.ts +89 -0
  96. package/src/lib/store-overview.test.ts +77 -0
  97. package/src/lib/store.ts +96 -0
  98. package/src/lib/tail-publisher.ts +64 -0
  99. package/src/lib/test-utils/mem-sql-db.test.ts +93 -0
  100. package/src/lib/test-utils/mem-sql-db.ts +86 -0
  101. package/src/lib/ua-parser.ts +61 -0
  102. package/src/lib/usage-store.ts +567 -0
  103. package/src/migrations/001_telemetry_spans.sql +28 -0
  104. package/src/migrations/002_usage_events.sql +24 -0
  105. package/src/migrations/003_usage_analytics_columns.sql +13 -0
  106. package/src/migrations/004_logs.sql +19 -0
  107. package/src/migrations/005_ai_calls.sql +25 -0
  108. package/src/migrations/006_user_profiles.sql +11 -0
  109. package/src/migrations/007_session_replay_metadata.sql +10 -0
  110. package/src/migrations/008_session_replay_storage_bytes.sql +1 -0
  111. package/src/migrations/009_projects_and_keys.sql +22 -0
  112. package/src/migrations/010_signal_project_id.sql +15 -0
  113. package/src/migrations/011_default_project_seed.sql +3 -0
  114. package/src/migrations/012_alert_rules.sql +17 -0
  115. package/src/migrations/013_alert_evaluations.sql +13 -0
  116. package/src/migrations/014_alert_state.sql +9 -0
  117. package/src/migrations/015_metrics.sql +58 -0
  118. package/src/migrations/016_metrics_exp_histogram_summary.sql +5 -0
  119. package/src/migrations/017_span_dropped_counts.sql +11 -0
  120. package/src/migrations/018_log_flags.sql +8 -0
  121. package/src/migrations/019_ai_span_payloads.sql +30 -0
  122. package/src/migrations/020_ai_span_evaluations.sql +33 -0
  123. package/src/migrations/021_ai_span_session.sql +13 -0
  124. package/src/migrations/022_span_log_session_id.sql +13 -0
  125. package/src/migrations/023_service_map_indexes.sql +32 -0
  126. package/src/migrations/024_analysis_results.sql +49 -0
  127. package/src/migrations/025_stage6_autopinning_alerts.sql +38 -0
  128. package/src/migrations/026_obs_dashboard_project_seed.sql +18 -0
  129. package/src/migrations/027_identity_propagation.sql +50 -0
  130. package/src/migrations/028_profile_blobs.sql +57 -0
  131. package/src/migrations/029_telemetry_sdk_name.sql +20 -0
  132. package/src/migrations/030_analysis_claim.sql +18 -0
  133. package/src/migrations/031_agent_action_graph.sql +154 -0
  134. package/src/migrations/032_payload_capture_flags.sql +8 -0
  135. package/src/migrations/033_eval_cases.sql +52 -0
  136. package/src/migrations/034_eval_case_results.sql +23 -0
  137. package/src/migrations/035_metric_exemplars.sql +33 -0
  138. package/src/migrations/036_trace_instrumentation_gaps.sql +19 -0
  139. package/src/migrations/037_eval_runs.sql +37 -0
  140. package/src/migrations/038_tool_call_evidence.sql +11 -0
  141. package/src/migrations/039_drop_trace_instrumentation_gaps.sql +11 -0
  142. package/src/migrations/040_evidence_retrieval_refs.sql +50 -0
  143. package/src/migrations-postgres/001_telemetry_spans.sql +33 -0
  144. package/src/migrations-postgres/002_usage_events.sql +24 -0
  145. package/src/migrations-postgres/003_usage_analytics_columns.sql +13 -0
  146. package/src/migrations-postgres/004_logs.sql +19 -0
  147. package/src/migrations-postgres/005_ai_calls.sql +25 -0
  148. package/src/migrations-postgres/006_user_profiles.sql +11 -0
  149. package/src/migrations-postgres/007_session_replay_metadata.sql +10 -0
  150. package/src/migrations-postgres/008_session_replay_storage_bytes.sql +2 -0
  151. package/src/migrations-postgres/009_projects_and_keys.sql +25 -0
  152. package/src/migrations-postgres/010_signal_project_id.sql +18 -0
  153. package/src/migrations-postgres/011_default_project_seed.sql +5 -0
  154. package/src/migrations-postgres/012_alert_rules.sql +17 -0
  155. package/src/migrations-postgres/013_alert_evaluations.sql +13 -0
  156. package/src/migrations-postgres/014_alert_state.sql +9 -0
  157. package/src/migrations-postgres/015_metrics.sql +47 -0
  158. package/src/migrations-postgres/016_metrics_exp_histogram_summary.sql +1 -0
  159. package/src/migrations-postgres/017_span_dropped_counts.sql +4 -0
  160. package/src/migrations-postgres/018_log_flags.sql +2 -0
  161. package/src/migrations-postgres/019_ai_span_payloads.sql +18 -0
  162. package/src/migrations-postgres/020_ai_span_evaluations.sql +21 -0
  163. package/src/migrations-postgres/021_ai_span_session.sql +7 -0
  164. package/src/migrations-postgres/022_span_log_session_id.sql +7 -0
  165. package/src/migrations-postgres/023_service_map_indexes.sql +9 -0
  166. package/src/migrations-postgres/024_analysis_results.sql +41 -0
  167. package/src/migrations-postgres/025_stage6_autopinning_alerts.sql +17 -0
  168. package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +14 -0
  169. package/src/migrations-postgres/027_identity_propagation.sql +27 -0
  170. package/src/migrations-postgres/028_profile_blobs.sql +48 -0
  171. package/src/migrations-postgres/029_telemetry_sdk_name.sql +5 -0
  172. package/src/migrations-postgres/030_analysis_claim.sql +5 -0
  173. package/src/migrations-postgres/031_agent_action_graph.sql +190 -0
  174. package/src/migrations-postgres/032_payload_capture_flags.sql +8 -0
  175. package/src/migrations-postgres/033_eval_cases.sql +55 -0
  176. package/src/migrations-postgres/034_eval_case_results.sql +26 -0
  177. package/src/migrations-postgres/035_metric_exemplars.sql +31 -0
  178. package/src/migrations-postgres/036_trace_instrumentation_gaps.sql +19 -0
  179. package/src/migrations-postgres/037_eval_runs.sql +40 -0
  180. package/src/migrations-postgres/038_tool_call_evidence.sql +11 -0
  181. package/src/migrations-postgres/039_drop_trace_instrumentation_gaps.sql +11 -0
  182. package/src/migrations-postgres/040_evidence_retrieval_refs.sql +47 -0
  183. package/src/migrations-postgres/README.md +40 -0
  184. package/src/otlp/acceptance.test.ts +373 -0
  185. package/src/otlp/decode/body.ts +127 -0
  186. package/src/otlp/decode/logs.ts +96 -0
  187. package/src/otlp/decode/metrics.ts +402 -0
  188. package/src/otlp/decode/traces.ts +92 -0
  189. package/src/otlp/decode/values.ts +150 -0
  190. package/src/otlp/decode.test.ts +800 -0
  191. package/src/otlp/decode.ts +11 -0
  192. package/src/otlp/gen/opentelemetry/proto/collector/logs/v1/logs_service_pb.ts +132 -0
  193. package/src/otlp/gen/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.ts +158 -0
  194. package/src/otlp/gen/opentelemetry/proto/collector/trace/v1/trace_service_pb.ts +158 -0
  195. package/src/otlp/gen/opentelemetry/proto/common/v1/common_pb.ts +289 -0
  196. package/src/otlp/gen/opentelemetry/proto/logs/v1/logs_pb.ts +397 -0
  197. package/src/otlp/gen/opentelemetry/proto/metrics/v1/metrics_pb.ts +1329 -0
  198. package/src/otlp/gen/opentelemetry/proto/resource/v1/resource_pb.ts +86 -0
  199. package/src/otlp/gen/opentelemetry/proto/trace/v1/trace_pb.ts +710 -0
  200. package/src/otlp/response.test.ts +158 -0
  201. package/src/otlp/response.ts +129 -0
  202. package/src/plugins/_context.ts +15 -0
  203. package/src/plugins/action-graph-processor/enrichers.ts +45 -0
  204. package/src/plugins/action-graph-processor/redaction.ts +84 -0
  205. package/src/plugins/action-graph-processor.test.ts +1023 -0
  206. package/src/plugins/action-graph-processor.ts +1172 -0
  207. package/src/plugins/action-routes.test.ts +716 -0
  208. package/src/plugins/action-routes.ts +343 -0
  209. package/src/plugins/ai-receiver.ts +254 -0
  210. package/src/plugins/ai-span-payloads-processor.test.ts +90 -0
  211. package/src/plugins/ai-span-payloads-processor.ts +180 -0
  212. package/src/plugins/alerts-evaluator.test.ts +91 -0
  213. package/src/plugins/alerts-evaluator.ts +328 -0
  214. package/src/plugins/alerts-routes.ts +101 -0
  215. package/src/plugins/analyses-routes.ts +138 -0
  216. package/src/plugins/ask-routes.ts +182 -0
  217. package/src/plugins/bot-filter.ts +19 -0
  218. package/src/plugins/connected-routes/manifest.ts +531 -0
  219. package/src/plugins/connected-routes.test.ts +1791 -0
  220. package/src/plugins/connected-routes.ts +1103 -0
  221. package/src/plugins/dashboard-routes.ts +41 -0
  222. package/src/plugins/default-span-enrichment.ts +92 -0
  223. package/src/plugins/eval-cases-routes.test.ts +391 -0
  224. package/src/plugins/eval-cases-routes.ts +529 -0
  225. package/src/plugins/evidence-retrieval-routes.benchmark.test.ts +245 -0
  226. package/src/plugins/evidence-retrieval-routes.test.ts +932 -0
  227. package/src/plugins/evidence-retrieval-routes.ts +2179 -0
  228. package/src/plugins/gen-ai-normalizer.test.ts +680 -0
  229. package/src/plugins/gen-ai-normalizer.ts +302 -0
  230. package/src/plugins/identity-receiver.ts +96 -0
  231. package/src/plugins/issue-enrichment.ts +132 -0
  232. package/src/plugins/issue-insights.ts +81 -0
  233. package/src/plugins/logs-receiver.ts +160 -0
  234. package/src/plugins/metrics-receiver.ts +91 -0
  235. package/src/plugins/onboarding-routes.ts +54 -0
  236. package/src/plugins/otlp-receiver.ts +124 -0
  237. package/src/plugins/platform-routes.ts +165 -0
  238. package/src/plugins/profile-routes.ts +468 -0
  239. package/src/plugins/projects-routes.ts +92 -0
  240. package/src/plugins/query-routes.ts +220 -0
  241. package/src/plugins/redaction-processor.ts +132 -0
  242. package/src/plugins/replay-query-routes.test.ts +38 -0
  243. package/src/plugins/replay-query-routes.ts +199 -0
  244. package/src/plugins/replay-receiver.ts +81 -0
  245. package/src/plugins/tail-routes.test.ts +47 -0
  246. package/src/plugins/tail-routes.ts +42 -0
  247. package/src/plugins/timeline-routes.ts +244 -0
  248. package/src/plugins/ua-enrichment.ts +27 -0
  249. package/src/plugins/usage-normalization.ts +46 -0
  250. package/src/plugins/usage-privacy.ts +55 -0
  251. package/src/plugins/usage-query-routes.ts +102 -0
  252. package/src/plugins/usage-receiver.ts +129 -0
  253. package/src/plugins/users-query-routes.ts +78 -0
  254. package/src/plugins/utm-enrichment.ts +36 -0
@@ -0,0 +1,96 @@
1
+ import type {
2
+ StoredSpan,
3
+ TelemetryInstrumentationGapsResponse,
4
+ TelemetryIssueDetailResponse,
5
+ TelemetryIssueOptions,
6
+ TelemetryIssueOverviewResponse,
7
+ TelemetryOverviewOptions,
8
+ TelemetryOverviewResponse,
9
+ TelemetryTraceDetailResponse,
10
+ } from "@obsunified/types";
11
+ import type { SqlDb } from "./sql-db";
12
+ import { getTelemetryExportRows } from "./store/export";
13
+ import { ingestTelemetrySpans } from "./store/ingest";
14
+ import {
15
+ getTelemetryIssueDetail,
16
+ getTelemetryIssueOverview,
17
+ } from "./store/issues";
18
+ import { getTelemetryOverview } from "./store/overview";
19
+ import { purgeExpiredTelemetry } from "./store/retention";
20
+ import {
21
+ getTelemetryServiceMap,
22
+ getTelemetryServiceOperations,
23
+ type ServiceMapOptions,
24
+ type ServiceOperationsOptions,
25
+ } from "./store/service-map";
26
+ import {
27
+ calibrateTelemetryInstrumentationGaps,
28
+ getTelemetryTraceDetail,
29
+ getTelemetryTraceGaps,
30
+ type TelemetryInstrumentationGapCalibrationOptions,
31
+ } from "./store/trace-detail";
32
+
33
+ export class TelemetryStore {
34
+ constructor(private readonly db: SqlDb) {}
35
+
36
+ async ingest(
37
+ spans: StoredSpan[],
38
+ ): Promise<{ inserted: number; traceCount: number }> {
39
+ return ingestTelemetrySpans(this.db, spans);
40
+ }
41
+
42
+ async getOverview(
43
+ options: TelemetryOverviewOptions,
44
+ ): Promise<TelemetryOverviewResponse> {
45
+ return getTelemetryOverview(this.db, options);
46
+ }
47
+
48
+ async getTraceDetail(
49
+ traceId: string,
50
+ projectId: string,
51
+ ): Promise<TelemetryTraceDetailResponse | null> {
52
+ return getTelemetryTraceDetail(this.db, traceId, projectId);
53
+ }
54
+
55
+ async getTraceGaps(
56
+ traceId: string,
57
+ projectId: string,
58
+ ): Promise<TelemetryInstrumentationGapsResponse | null> {
59
+ return getTelemetryTraceGaps(this.db, traceId, projectId);
60
+ }
61
+
62
+ async calibrateTraceGaps(
63
+ options: TelemetryInstrumentationGapCalibrationOptions,
64
+ ) {
65
+ return calibrateTelemetryInstrumentationGaps(this.db, options);
66
+ }
67
+
68
+ async getIssueOverview(
69
+ options: TelemetryIssueOptions,
70
+ ): Promise<TelemetryIssueOverviewResponse> {
71
+ return getTelemetryIssueOverview(this.db, options);
72
+ }
73
+
74
+ async getIssueDetail(
75
+ issueId: string,
76
+ options: TelemetryIssueOptions,
77
+ ): Promise<TelemetryIssueDetailResponse | null> {
78
+ return getTelemetryIssueDetail(this.db, issueId, options);
79
+ }
80
+
81
+ async getExportRows(options: TelemetryOverviewOptions): Promise<string> {
82
+ return getTelemetryExportRows(this.db, options);
83
+ }
84
+
85
+ async getServiceMap(options: ServiceMapOptions) {
86
+ return getTelemetryServiceMap(this.db, options);
87
+ }
88
+
89
+ async getServiceOperations(options: ServiceOperationsOptions) {
90
+ return getTelemetryServiceOperations(this.db, options);
91
+ }
92
+
93
+ async purgeExpired(): Promise<number> {
94
+ return purgeExpiredTelemetry(this.db);
95
+ }
96
+ }
@@ -0,0 +1,64 @@
1
+ import type { LogRecord, StoredSpan } from "@obsunified/types";
2
+ import type { TailEvent } from "../durable-objects/tail-hub";
3
+ import type { CollectorEnv } from "../framework/env";
4
+
5
+ /**
6
+ * Fire-and-forget publish to the TailHub DO. Swallows errors — tail is a
7
+ * best-effort UX feature; a broadcast failure must never break ingest.
8
+ */
9
+ export async function publishTail(
10
+ env: CollectorEnv,
11
+ events: TailEvent[],
12
+ ): Promise<void> {
13
+ if (!env.TAIL_HUB || events.length === 0) return;
14
+ try {
15
+ const id = env.TAIL_HUB.idFromName("singleton");
16
+ const stub = env.TAIL_HUB.get(id);
17
+ await stub.fetch("https://hub/publish", {
18
+ method: "POST",
19
+ headers: { "content-type": "application/json" },
20
+ body: JSON.stringify(events),
21
+ });
22
+ } catch (err) {
23
+ console.error("[tail-publisher] publish failed:", err);
24
+ }
25
+ }
26
+
27
+ export function spanToTailEvent(span: StoredSpan): TailEvent {
28
+ return {
29
+ kind: "span",
30
+ projectId: span.projectId,
31
+ row: {
32
+ traceId: span.traceId,
33
+ spanId: span.spanId,
34
+ parentSpanId: span.parentSpanId,
35
+ serviceName: span.serviceName,
36
+ spanName: span.spanName,
37
+ spanKind: span.spanKind,
38
+ statusCode: span.statusCode,
39
+ statusMessage: span.statusMessage,
40
+ startTime: span.startTime,
41
+ endTime: span.endTime,
42
+ durationMs: span.durationMs,
43
+ },
44
+ t: span.receivedAt,
45
+ };
46
+ }
47
+
48
+ export function logToTailEvent(log: LogRecord): TailEvent {
49
+ return {
50
+ kind: "log",
51
+ projectId: log.projectId,
52
+ row: {
53
+ logId: log.logId,
54
+ traceId: log.traceId,
55
+ spanId: log.spanId,
56
+ serviceName: log.serviceName,
57
+ severity: log.severity,
58
+ loggerName: log.loggerName,
59
+ message: log.message,
60
+ occurredAt: log.occurredAt,
61
+ },
62
+ t: log.receivedAt,
63
+ };
64
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Self-test for the `MemSqlDb` test double — RFC 0008 Phase 0.4.
3
+ *
4
+ * The point is to validate the double behaves as the `SqlDb` contract
5
+ * promises (prepare → bind → first/all/run, with binds carried forward
6
+ * through `bind`), so anything that uses it for store-level testing in
7
+ * later phases can trust the seam.
8
+ */
9
+
10
+ import { describe, expect, it } from "vitest";
11
+ import type { SqlDb } from "../sql-db";
12
+ import { MemSqlDb } from "./mem-sql-db";
13
+
14
+ const useDb = (db: SqlDb) => db; // type-witness — fails to compile if MemSqlDb drifts from SqlDb.
15
+
16
+ describe("MemSqlDb", () => {
17
+ it("conforms to SqlDb structurally", () => {
18
+ const db = new MemSqlDb();
19
+ // Compiles only because MemSqlDb implements SqlDb.
20
+ expect(useDb(db)).toBe(db);
21
+ });
22
+
23
+ it("records prepare / bind / first calls with the bound args", async () => {
24
+ const db = new MemSqlDb({
25
+ first: (sql, binds) =>
26
+ sql.startsWith("SELECT count") && binds[0] === "p1" ? { c: 7 } : null,
27
+ });
28
+
29
+ const row = await db
30
+ .prepare("SELECT count(*) as c FROM x WHERE project_id = ?")
31
+ .bind("p1")
32
+ .first<{ c: number }>();
33
+
34
+ expect(row).toEqual({ c: 7 });
35
+ expect(db.calls).toHaveLength(1);
36
+ expect(db.calls[0]).toEqual({
37
+ sql: "SELECT count(*) as c FROM x WHERE project_id = ?",
38
+ binds: ["p1"],
39
+ op: "first",
40
+ });
41
+ });
42
+
43
+ it("returns wrapped results for all()", async () => {
44
+ const db = new MemSqlDb({
45
+ all: () => [{ id: "a" }, { id: "b" }],
46
+ });
47
+
48
+ const out = await db.prepare("SELECT id FROM x").all<{ id: string }>();
49
+ expect(out).toEqual({ results: [{ id: "a" }, { id: "b" }] });
50
+ });
51
+
52
+ it("returns empty results when no all() handler is configured", async () => {
53
+ const db = new MemSqlDb();
54
+ const out = await db.prepare("SELECT 1").all();
55
+ expect(out).toEqual({ results: [] });
56
+ });
57
+
58
+ it("returns changes=1 by default for run()", async () => {
59
+ const db = new MemSqlDb();
60
+ const out = await db.prepare("INSERT INTO x DEFAULT VALUES").run();
61
+ expect(out).toEqual({ meta: { changes: 1 } });
62
+ });
63
+
64
+ it("honors a configurable run() handler", async () => {
65
+ const db = new MemSqlDb({ run: () => ({ changes: 3 }) });
66
+ const out = await db.prepare("UPDATE x SET y=1").run();
67
+ expect(out).toEqual({ meta: { changes: 3 } });
68
+ });
69
+
70
+ it("each bind() returns a new statement (binds don't leak across statements)", async () => {
71
+ const db = new MemSqlDb({
72
+ first: (_sql, binds) => ({ bind0: binds[0] }),
73
+ });
74
+ const stmt = db.prepare("SELECT ? AS x");
75
+
76
+ const a = await stmt.bind("a").first<{ bind0: string }>();
77
+ const b = await stmt.bind("b").first<{ bind0: string }>();
78
+
79
+ expect(a).toEqual({ bind0: "a" });
80
+ expect(b).toEqual({ bind0: "b" });
81
+ });
82
+
83
+ it("callsMatching filters by SQL substring", async () => {
84
+ const db = new MemSqlDb();
85
+ await db.prepare("INSERT INTO foo VALUES (?)").bind(1).run();
86
+ await db.prepare("INSERT INTO bar VALUES (?)").bind(2).run();
87
+ await db.prepare("SELECT * FROM foo").all();
88
+
89
+ expect(db.callsMatching("foo")).toHaveLength(2);
90
+ expect(db.callsMatching("bar")).toHaveLength(1);
91
+ expect(db.callsMatching("baz")).toHaveLength(0);
92
+ });
93
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * In-memory `SqlDb` test double — RFC 0008.
3
+ *
4
+ * Replaces hand-rolled `FakeDb` patterns scattered across tests (e.g. the
5
+ * one in `stage6.test.ts` that motivated this). Tests configure `first` /
6
+ * `all` / `run` matchers; the double records every prepare/bind so tests
7
+ * can assert *which* SQL was issued, not just that something ran.
8
+ *
9
+ * Not a real SQLite — there is no execution. The point is decision
10
+ * coverage (which query did the store choose, which value did it return),
11
+ * not SQL grammar coverage.
12
+ */
13
+
14
+ import type { SqlDb, SqlStatement } from "../sql-db";
15
+
16
+ export interface MemSqlDbOptions {
17
+ /** Return value for `first()`. Receives the SQL + binds at call time. */
18
+ first?: (sql: string, binds: unknown[]) => Record<string, unknown> | null;
19
+ /** Rows returned for `all()`. The double wraps in `{ results }`. */
20
+ all?: (sql: string, binds: unknown[]) => Record<string, unknown>[];
21
+ /** Changes count for `run()`. Defaults to `1`. */
22
+ run?: (sql: string, binds: unknown[]) => { changes: number };
23
+ }
24
+
25
+ export interface MemSqlDbCall {
26
+ sql: string;
27
+ binds: unknown[];
28
+ op: "first" | "all" | "run";
29
+ }
30
+
31
+ export class MemSqlDb implements SqlDb {
32
+ readonly calls: MemSqlDbCall[] = [];
33
+ constructor(private readonly opts: MemSqlDbOptions = {}) {}
34
+
35
+ prepare(sql: string): SqlStatement {
36
+ return new MemSqlStatement(sql, [], this.calls, this.opts);
37
+ }
38
+
39
+ async batch(
40
+ statements: SqlStatement[],
41
+ ): Promise<Array<{ meta: { changes: number } }>> {
42
+ const results: Array<{ meta: { changes: number } }> = [];
43
+ for (const stmt of statements) {
44
+ results.push(await stmt.run());
45
+ }
46
+ return results;
47
+ }
48
+
49
+ /** Filter recorded calls by a substring of the SQL — convenient in tests. */
50
+ callsMatching(needle: string): MemSqlDbCall[] {
51
+ return this.calls.filter((c) => c.sql.includes(needle));
52
+ }
53
+ }
54
+
55
+ class MemSqlStatement implements SqlStatement {
56
+ constructor(
57
+ private readonly sql: string,
58
+ private readonly binds: unknown[],
59
+ private readonly calls: MemSqlDbCall[],
60
+ private readonly opts: MemSqlDbOptions,
61
+ ) {}
62
+
63
+ bind(...args: unknown[]): SqlStatement {
64
+ return new MemSqlStatement(this.sql, args, this.calls, this.opts);
65
+ }
66
+
67
+ async first<T = Record<string, unknown>>(): Promise<T | null> {
68
+ this.calls.push({ sql: this.sql, binds: this.binds, op: "first" });
69
+ const row = this.opts.first ? this.opts.first(this.sql, this.binds) : null;
70
+ return (row ?? null) as T | null;
71
+ }
72
+
73
+ async all<T = Record<string, unknown>>(): Promise<{ results: T[] }> {
74
+ this.calls.push({ sql: this.sql, binds: this.binds, op: "all" });
75
+ const rows = this.opts.all ? this.opts.all(this.sql, this.binds) : [];
76
+ return { results: rows as T[] };
77
+ }
78
+
79
+ async run(): Promise<{ meta: { changes: number } }> {
80
+ this.calls.push({ sql: this.sql, binds: this.binds, op: "run" });
81
+ const r = this.opts.run
82
+ ? this.opts.run(this.sql, this.binds)
83
+ : { changes: 1 };
84
+ return { meta: { changes: r.changes } };
85
+ }
86
+ }
@@ -0,0 +1,61 @@
1
+ const BOT_PATTERN =
2
+ /(?:^|[^\p{L}\p{N}])(?:bot|crawler|spider|slurp|facebookexternalhit|headless|monitoring|uptime|curl|wget|python-requests|go-http-client)(?:$|[^\p{L}\p{N}])/iu;
3
+
4
+ export interface ParsedUserAgent {
5
+ browser: string | null;
6
+ os: string | null;
7
+ deviceType: string | null;
8
+ }
9
+
10
+ export const isBot = (userAgent: string | null | undefined): boolean =>
11
+ Boolean(userAgent && BOT_PATTERN.test(userAgent));
12
+
13
+ const detectBrowser = (userAgent: string): string | null => {
14
+ if (/edg\//i.test(userAgent)) return "Edge";
15
+ if (/opr\//i.test(userAgent) || /opera/i.test(userAgent)) return "Opera";
16
+ if (/samsungbrowser\//i.test(userAgent)) return "Samsung Internet";
17
+ if (/chrome\//i.test(userAgent) || /crios\//i.test(userAgent))
18
+ return "Chrome";
19
+ if (/firefox\//i.test(userAgent) || /fxios\//i.test(userAgent))
20
+ return "Firefox";
21
+ if (/safari\//i.test(userAgent)) return "Safari";
22
+ return null;
23
+ };
24
+
25
+ const detectOs = (userAgent: string): string | null => {
26
+ if (/windows nt/i.test(userAgent)) return "Windows";
27
+ if (/(iphone|ipad|ipod)/i.test(userAgent)) return "iOS";
28
+ if (/android/i.test(userAgent)) return "Android";
29
+ if (/mac os x/i.test(userAgent) || /macintosh/i.test(userAgent))
30
+ return "macOS";
31
+ if (/linux/i.test(userAgent)) return "Linux";
32
+ return null;
33
+ };
34
+
35
+ const detectDeviceType = (userAgent: string): string | null => {
36
+ if (/ipad|tablet/i.test(userAgent)) return "tablet";
37
+ if (/mobile|iphone|ipod|android/i.test(userAgent)) return "mobile";
38
+ return "desktop";
39
+ };
40
+
41
+ export const parseUserAgent = (
42
+ userAgent: string | null | undefined,
43
+ ): ParsedUserAgent => {
44
+ if (!userAgent) {
45
+ return { browser: null, os: null, deviceType: null };
46
+ }
47
+
48
+ if (isBot(userAgent)) {
49
+ return {
50
+ browser: detectBrowser(userAgent),
51
+ os: detectOs(userAgent),
52
+ deviceType: "bot",
53
+ };
54
+ }
55
+
56
+ return {
57
+ browser: detectBrowser(userAgent),
58
+ os: detectOs(userAgent),
59
+ deviceType: detectDeviceType(userAgent),
60
+ };
61
+ };