@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,427 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
JsonValue,
|
|
3
|
+
SpanDetailRow,
|
|
4
|
+
TelemetryCodeReference,
|
|
5
|
+
TelemetryInstrumentationGap,
|
|
6
|
+
TelemetryInstrumentationGapsResponse,
|
|
7
|
+
TelemetrySpanDetail,
|
|
8
|
+
TelemetryTraceDetailResponse,
|
|
9
|
+
} from "@obsunified/types";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_INSTRUMENTATION_GAP_THRESHOLDS,
|
|
12
|
+
isInstrumentationGapCandidate,
|
|
13
|
+
} from "@obsunified/types";
|
|
14
|
+
import { parseJsonArray, parseJsonRecord } from "../json";
|
|
15
|
+
import type { SqlDb } from "../sql-db";
|
|
16
|
+
import { normalizeService } from "./helpers";
|
|
17
|
+
|
|
18
|
+
export interface TelemetryInstrumentationGapCalibrationOptions {
|
|
19
|
+
projectId: string;
|
|
20
|
+
hours: number;
|
|
21
|
+
limit?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function getTelemetryTraceDetail(
|
|
25
|
+
db: SqlDb,
|
|
26
|
+
traceId: string,
|
|
27
|
+
projectId: string,
|
|
28
|
+
): Promise<TelemetryTraceDetailResponse | null> {
|
|
29
|
+
if (!projectId)
|
|
30
|
+
throw new Error("TelemetryStore.getTraceDetail: projectId is required");
|
|
31
|
+
const result = await db
|
|
32
|
+
.prepare(`
|
|
33
|
+
SELECT project_id, trace_id, span_id, parent_span_id, service_name, scope_name,
|
|
34
|
+
scope_version, span_name, span_kind, status_code, status_message,
|
|
35
|
+
start_time, end_time, duration_ms, attributes_json,
|
|
36
|
+
resource_attributes_json, events_json, links_json,
|
|
37
|
+
received_at, expires_at
|
|
38
|
+
FROM telemetry_spans
|
|
39
|
+
WHERE project_id = ? AND trace_id = ?
|
|
40
|
+
ORDER BY start_time ASC, span_id ASC
|
|
41
|
+
`)
|
|
42
|
+
.bind(projectId, traceId)
|
|
43
|
+
.all<SpanDetailRow>();
|
|
44
|
+
|
|
45
|
+
const rows = result.results ?? [];
|
|
46
|
+
if (rows.length === 0) return null;
|
|
47
|
+
|
|
48
|
+
const spans: TelemetrySpanDetail[] = rows.map((row) => ({
|
|
49
|
+
traceId: row.trace_id,
|
|
50
|
+
spanId: row.span_id,
|
|
51
|
+
parentSpanId: row.parent_span_id,
|
|
52
|
+
serviceName: normalizeService(row.service_name),
|
|
53
|
+
scopeName: row.scope_name,
|
|
54
|
+
scopeVersion: row.scope_version,
|
|
55
|
+
spanName: row.span_name,
|
|
56
|
+
spanKind: row.span_kind,
|
|
57
|
+
statusCode: row.status_code,
|
|
58
|
+
statusMessage: row.status_message,
|
|
59
|
+
startTime: row.start_time,
|
|
60
|
+
endTime: row.end_time,
|
|
61
|
+
durationMs: row.duration_ms,
|
|
62
|
+
attributes: parseJsonRecord(row.attributes_json),
|
|
63
|
+
resourceAttributes: parseJsonRecord(row.resource_attributes_json),
|
|
64
|
+
events: parseJsonArray(row.events_json),
|
|
65
|
+
links: parseJsonArray(row.links_json),
|
|
66
|
+
}));
|
|
67
|
+
for (const span of spans) {
|
|
68
|
+
const codeReference = extractCodeReference(span.attributes);
|
|
69
|
+
if (codeReference) span.codeReference = codeReference;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const root = spans.find((span) => !span.parentSpanId) ?? spans[0];
|
|
73
|
+
const timestamp = new Date().toISOString();
|
|
74
|
+
const instrumentationGaps =
|
|
75
|
+
(await getTelemetryTraceGaps(db, traceId, projectId, spans)) ?? undefined;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
trace: {
|
|
79
|
+
traceId,
|
|
80
|
+
serviceName: root.serviceName,
|
|
81
|
+
spanName: root.spanName,
|
|
82
|
+
statusCode: spans.some((span) => span.statusCode === 2)
|
|
83
|
+
? 2
|
|
84
|
+
: root.statusCode,
|
|
85
|
+
statusMessage:
|
|
86
|
+
spans.find((span) => span.statusCode === 2)?.statusMessage ??
|
|
87
|
+
root.statusMessage,
|
|
88
|
+
startTime: root.startTime,
|
|
89
|
+
endTime: root.endTime,
|
|
90
|
+
durationMs: root.durationMs,
|
|
91
|
+
receivedAt: rows[0]?.received_at ?? root.startTime,
|
|
92
|
+
spanCount: spans.length,
|
|
93
|
+
errorSpanCount: spans.filter((span) => span.statusCode === 2).length,
|
|
94
|
+
},
|
|
95
|
+
spans,
|
|
96
|
+
instrumentationGaps,
|
|
97
|
+
timestamp,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function getTelemetryTraceGaps(
|
|
102
|
+
db: SqlDb,
|
|
103
|
+
traceId: string,
|
|
104
|
+
projectId: string,
|
|
105
|
+
spans?: TelemetrySpanDetail[],
|
|
106
|
+
): Promise<TelemetryInstrumentationGapsResponse | null> {
|
|
107
|
+
if (!projectId)
|
|
108
|
+
throw new Error("TelemetryStore.getTraceGaps: projectId is required");
|
|
109
|
+
|
|
110
|
+
// Gaps are derived data: compute them lazily from the trace's spans on
|
|
111
|
+
// read, rather than materializing them on the ingest hot path. The
|
|
112
|
+
// `spans` arg is passed by `getTelemetryTraceDetail` (which has already
|
|
113
|
+
// loaded them); the standalone `/gaps` endpoint omits it, so we load
|
|
114
|
+
// them here. The span query is project-scoped, so this avoids the
|
|
115
|
+
// `trace_instrumentation_gaps` table's lack of project scoping entirely.
|
|
116
|
+
const timestamp = new Date().toISOString();
|
|
117
|
+
|
|
118
|
+
let localSpans = spans;
|
|
119
|
+
if (!localSpans) {
|
|
120
|
+
const result = await db
|
|
121
|
+
.prepare(`
|
|
122
|
+
SELECT project_id, trace_id, span_id, parent_span_id, service_name, scope_name,
|
|
123
|
+
scope_version, span_name, span_kind, status_code, status_message,
|
|
124
|
+
start_time, end_time, duration_ms, attributes_json,
|
|
125
|
+
resource_attributes_json, events_json, links_json,
|
|
126
|
+
received_at, expires_at
|
|
127
|
+
FROM telemetry_spans
|
|
128
|
+
WHERE project_id = ? AND trace_id = ?
|
|
129
|
+
ORDER BY start_time ASC, span_id ASC
|
|
130
|
+
`)
|
|
131
|
+
.bind(projectId, traceId)
|
|
132
|
+
.all<SpanDetailRow>();
|
|
133
|
+
const spanRows = result.results ?? [];
|
|
134
|
+
if (spanRows.length === 0) return null;
|
|
135
|
+
|
|
136
|
+
localSpans = spanRows.map((row) => ({
|
|
137
|
+
traceId: row.trace_id,
|
|
138
|
+
spanId: row.span_id,
|
|
139
|
+
parentSpanId: row.parent_span_id,
|
|
140
|
+
serviceName: normalizeService(row.service_name),
|
|
141
|
+
scopeName: row.scope_name,
|
|
142
|
+
scopeVersion: row.scope_version,
|
|
143
|
+
spanName: row.span_name,
|
|
144
|
+
spanKind: row.span_kind,
|
|
145
|
+
statusCode: row.status_code,
|
|
146
|
+
statusMessage: row.status_message,
|
|
147
|
+
startTime: row.start_time,
|
|
148
|
+
endTime: row.end_time,
|
|
149
|
+
durationMs: row.duration_ms,
|
|
150
|
+
attributes: parseJsonRecord(row.attributes_json),
|
|
151
|
+
resourceAttributes: parseJsonRecord(row.resource_attributes_json),
|
|
152
|
+
events: parseJsonArray(row.events_json),
|
|
153
|
+
links: parseJsonArray(row.links_json),
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (localSpans.length === 0) return null;
|
|
158
|
+
const root = localSpans.find((span) => !span.parentSpanId) ?? localSpans[0];
|
|
159
|
+
return buildTraceInstrumentationGaps(
|
|
160
|
+
traceId,
|
|
161
|
+
localSpans,
|
|
162
|
+
root.durationMs,
|
|
163
|
+
timestamp,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function buildTraceInstrumentationGaps(
|
|
168
|
+
traceId: string,
|
|
169
|
+
spans: TelemetrySpanDetail[],
|
|
170
|
+
totalDurationMs: number,
|
|
171
|
+
timestamp = new Date().toISOString(),
|
|
172
|
+
): TelemetryInstrumentationGapsResponse {
|
|
173
|
+
const children = new Map<string | null, TelemetrySpanDetail[]>();
|
|
174
|
+
for (const span of spans) {
|
|
175
|
+
const parentKey = span.parentSpanId ?? null;
|
|
176
|
+
const bucket = children.get(parentKey) ?? [];
|
|
177
|
+
bucket.push(span);
|
|
178
|
+
children.set(parentKey, bucket);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const blindspots: TelemetryInstrumentationGap[] = [];
|
|
182
|
+
for (const span of spans) {
|
|
183
|
+
const childSpans = children.get(span.spanId) ?? [];
|
|
184
|
+
const childDurationMs = childSpans.reduce(
|
|
185
|
+
(acc, child) => acc + child.durationMs,
|
|
186
|
+
0,
|
|
187
|
+
);
|
|
188
|
+
const rawSelfMs = span.durationMs - childDurationMs;
|
|
189
|
+
const asyncParent = rawSelfMs < 0;
|
|
190
|
+
const selfMs = Math.max(0, rawSelfMs);
|
|
191
|
+
const ratioOfParent =
|
|
192
|
+
span.durationMs > 0 ? Math.min(1, selfMs / span.durationMs) : 0;
|
|
193
|
+
if (
|
|
194
|
+
!isInstrumentationGapCandidate({
|
|
195
|
+
durationMs: span.durationMs,
|
|
196
|
+
selfRatio: ratioOfParent,
|
|
197
|
+
childSpanCount: childSpans.length,
|
|
198
|
+
asyncParent,
|
|
199
|
+
spanKind: span.spanKind,
|
|
200
|
+
})
|
|
201
|
+
) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
blindspots.push({
|
|
205
|
+
traceId,
|
|
206
|
+
parentSpanId: span.spanId,
|
|
207
|
+
parentServiceName: span.serviceName,
|
|
208
|
+
parentSpanName: span.spanName,
|
|
209
|
+
offsetMs: 0,
|
|
210
|
+
durationMs: Math.round(selfMs),
|
|
211
|
+
ratioOfParent,
|
|
212
|
+
childSpanCount: childSpans.length,
|
|
213
|
+
parentSpanKind: span.spanKind,
|
|
214
|
+
asyncParent,
|
|
215
|
+
thresholdVersion: DEFAULT_INSTRUMENTATION_GAP_THRESHOLDS.version,
|
|
216
|
+
recommendation: `Add tracing inside ${span.spanName} or attach a profile for ${span.serviceName}.`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const uninstrumentedTimeMs = blindspots.reduce(
|
|
221
|
+
(acc, gap) => acc + gap.durationMs,
|
|
222
|
+
0,
|
|
223
|
+
);
|
|
224
|
+
return {
|
|
225
|
+
traceId,
|
|
226
|
+
totalDurationMs,
|
|
227
|
+
uninstrumentedTimeMs,
|
|
228
|
+
ratio: totalDurationMs > 0 ? uninstrumentedTimeMs / totalDurationMs : 0,
|
|
229
|
+
blindspots,
|
|
230
|
+
thresholds: DEFAULT_INSTRUMENTATION_GAP_THRESHOLDS,
|
|
231
|
+
timestamp,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function calibrateTelemetryInstrumentationGaps(
|
|
236
|
+
db: SqlDb,
|
|
237
|
+
options: TelemetryInstrumentationGapCalibrationOptions,
|
|
238
|
+
) {
|
|
239
|
+
if (!options.projectId)
|
|
240
|
+
throw new Error("TelemetryStore.calibrateTraceGaps: projectId is required");
|
|
241
|
+
const hours = Math.max(1, Math.min(options.hours, 24 * 30));
|
|
242
|
+
const limit = Math.max(1, Math.min(options.limit ?? 5000, 20_000));
|
|
243
|
+
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
244
|
+
const result = await db
|
|
245
|
+
.prepare(`
|
|
246
|
+
SELECT project_id, trace_id, span_id, parent_span_id, service_name, scope_name,
|
|
247
|
+
scope_version, span_name, span_kind, status_code, status_message,
|
|
248
|
+
start_time, end_time, duration_ms, attributes_json,
|
|
249
|
+
resource_attributes_json, events_json, links_json,
|
|
250
|
+
received_at, expires_at
|
|
251
|
+
FROM telemetry_spans
|
|
252
|
+
WHERE project_id = ? AND received_at >= ?
|
|
253
|
+
ORDER BY received_at DESC, trace_id ASC, start_time ASC, span_id ASC
|
|
254
|
+
LIMIT ?
|
|
255
|
+
`)
|
|
256
|
+
.bind(options.projectId, since, limit)
|
|
257
|
+
.all<SpanDetailRow>();
|
|
258
|
+
|
|
259
|
+
const rows = result.results ?? [];
|
|
260
|
+
const byTrace = new Map<string, TelemetrySpanDetail[]>();
|
|
261
|
+
for (const row of rows) {
|
|
262
|
+
const bucket = byTrace.get(row.trace_id) ?? [];
|
|
263
|
+
bucket.push({
|
|
264
|
+
traceId: row.trace_id,
|
|
265
|
+
spanId: row.span_id,
|
|
266
|
+
parentSpanId: row.parent_span_id,
|
|
267
|
+
serviceName: normalizeService(row.service_name),
|
|
268
|
+
scopeName: row.scope_name,
|
|
269
|
+
scopeVersion: row.scope_version,
|
|
270
|
+
spanName: row.span_name,
|
|
271
|
+
spanKind: row.span_kind,
|
|
272
|
+
statusCode: row.status_code,
|
|
273
|
+
statusMessage: row.status_message,
|
|
274
|
+
startTime: row.start_time,
|
|
275
|
+
endTime: row.end_time,
|
|
276
|
+
durationMs: row.duration_ms,
|
|
277
|
+
attributes: parseJsonRecord(row.attributes_json),
|
|
278
|
+
resourceAttributes: parseJsonRecord(row.resource_attributes_json),
|
|
279
|
+
events: parseJsonArray(row.events_json),
|
|
280
|
+
links: parseJsonArray(row.links_json),
|
|
281
|
+
});
|
|
282
|
+
byTrace.set(row.trace_id, bucket);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const traceSummaries: Array<{
|
|
286
|
+
traceId: string;
|
|
287
|
+
rootSpanName: string;
|
|
288
|
+
serviceName: string;
|
|
289
|
+
totalDurationMs: number;
|
|
290
|
+
blindspotCount: number;
|
|
291
|
+
uninstrumentedTimeMs: number;
|
|
292
|
+
ratio: number;
|
|
293
|
+
topBlindspot: TelemetryInstrumentationGap | null;
|
|
294
|
+
}> = [];
|
|
295
|
+
let flaggedTraceCount = 0;
|
|
296
|
+
let blindspotCount = 0;
|
|
297
|
+
let uninstrumentedTimeMs = 0;
|
|
298
|
+
|
|
299
|
+
for (const [traceId, spans] of byTrace) {
|
|
300
|
+
spans.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
|
301
|
+
const root = spans.find((span) => !span.parentSpanId) ?? spans[0];
|
|
302
|
+
if (!root) continue;
|
|
303
|
+
const gaps = buildTraceInstrumentationGaps(traceId, spans, root.durationMs);
|
|
304
|
+
if (gaps.blindspots.length === 0) continue;
|
|
305
|
+
flaggedTraceCount++;
|
|
306
|
+
blindspotCount += gaps.blindspots.length;
|
|
307
|
+
uninstrumentedTimeMs += gaps.uninstrumentedTimeMs;
|
|
308
|
+
traceSummaries.push({
|
|
309
|
+
traceId,
|
|
310
|
+
rootSpanName: root.spanName,
|
|
311
|
+
serviceName: root.serviceName,
|
|
312
|
+
totalDurationMs: root.durationMs,
|
|
313
|
+
blindspotCount: gaps.blindspots.length,
|
|
314
|
+
uninstrumentedTimeMs: gaps.uninstrumentedTimeMs,
|
|
315
|
+
ratio: gaps.ratio,
|
|
316
|
+
topBlindspot: [...gaps.blindspots].sort(
|
|
317
|
+
(a, b) => b.durationMs - a.durationMs,
|
|
318
|
+
)[0],
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
traceSummaries.sort(
|
|
323
|
+
(a, b) => b.uninstrumentedTimeMs - a.uninstrumentedTimeMs,
|
|
324
|
+
);
|
|
325
|
+
const traceCount = byTrace.size;
|
|
326
|
+
return {
|
|
327
|
+
thresholds: DEFAULT_INSTRUMENTATION_GAP_THRESHOLDS,
|
|
328
|
+
windowHours: hours,
|
|
329
|
+
sampledSpanCount: rows.length,
|
|
330
|
+
traceCount,
|
|
331
|
+
flaggedTraceCount,
|
|
332
|
+
blindspotCount,
|
|
333
|
+
uninstrumentedTimeMs,
|
|
334
|
+
flaggedTraceRate: traceCount > 0 ? flaggedTraceCount / traceCount : 0,
|
|
335
|
+
status:
|
|
336
|
+
traceCount === 0
|
|
337
|
+
? "no_traces"
|
|
338
|
+
: flaggedTraceCount === 0
|
|
339
|
+
? "quiet"
|
|
340
|
+
: flaggedTraceCount / traceCount > 0.25
|
|
341
|
+
? "noisy"
|
|
342
|
+
: "calibrated",
|
|
343
|
+
topTraces: traceSummaries.slice(0, 20),
|
|
344
|
+
timestamp: new Date().toISOString(),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function extractCodeReference(
|
|
349
|
+
attributes: Record<string, JsonValue>,
|
|
350
|
+
): TelemetryCodeReference | undefined {
|
|
351
|
+
const originalPath = firstString(attributes, [
|
|
352
|
+
"code.filepath",
|
|
353
|
+
"code.file.path",
|
|
354
|
+
"code.path",
|
|
355
|
+
"source.file",
|
|
356
|
+
]);
|
|
357
|
+
const symbolName = firstString(attributes, [
|
|
358
|
+
"code.function",
|
|
359
|
+
"code.function.name",
|
|
360
|
+
"code.namespace",
|
|
361
|
+
]);
|
|
362
|
+
const lineNumber = firstPositiveInteger(attributes, [
|
|
363
|
+
"code.lineno",
|
|
364
|
+
"code.line_number",
|
|
365
|
+
"code.line",
|
|
366
|
+
]);
|
|
367
|
+
const columnNumber = firstPositiveInteger(attributes, [
|
|
368
|
+
"code.column",
|
|
369
|
+
"code.column_number",
|
|
370
|
+
]);
|
|
371
|
+
const repoName = firstString(attributes, ["code.repository", "repo.name"]);
|
|
372
|
+
|
|
373
|
+
if (!originalPath && !symbolName && lineNumber === undefined)
|
|
374
|
+
return undefined;
|
|
375
|
+
|
|
376
|
+
const reference: TelemetryCodeReference = {};
|
|
377
|
+
if (repoName) reference.repoName = repoName;
|
|
378
|
+
if (originalPath) {
|
|
379
|
+
reference.originalPath = originalPath;
|
|
380
|
+
if (isAbsolutePath(originalPath)) {
|
|
381
|
+
reference.absolutePath = originalPath;
|
|
382
|
+
} else {
|
|
383
|
+
reference.relativePath = normalizeRelativePath(originalPath);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (symbolName) reference.symbolName = symbolName;
|
|
387
|
+
if (lineNumber !== undefined) reference.lineNumber = lineNumber;
|
|
388
|
+
if (columnNumber !== undefined) reference.columnNumber = columnNumber;
|
|
389
|
+
|
|
390
|
+
return reference;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function firstString(
|
|
394
|
+
attributes: Record<string, JsonValue>,
|
|
395
|
+
keys: string[],
|
|
396
|
+
): string | undefined {
|
|
397
|
+
for (const key of keys) {
|
|
398
|
+
const value = attributes[key];
|
|
399
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
400
|
+
}
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function firstPositiveInteger(
|
|
405
|
+
attributes: Record<string, JsonValue>,
|
|
406
|
+
keys: string[],
|
|
407
|
+
): number | undefined {
|
|
408
|
+
for (const key of keys) {
|
|
409
|
+
const value = attributes[key];
|
|
410
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
411
|
+
return value;
|
|
412
|
+
}
|
|
413
|
+
if (typeof value === "string") {
|
|
414
|
+
const parsed = Number.parseInt(value, 10);
|
|
415
|
+
if (Number.isInteger(parsed) && parsed > 0) return parsed;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return undefined;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function isAbsolutePath(value: string): boolean {
|
|
422
|
+
return value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function normalizeRelativePath(value: string): string {
|
|
426
|
+
return value.replace(/^\.\/+/, "");
|
|
427
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { MemSqlDb } from "../test-utils/mem-sql-db";
|
|
3
|
+
import { fetchSpansForTraceIds } from "./trace-queries";
|
|
4
|
+
|
|
5
|
+
describe("fetchSpansForTraceIds", () => {
|
|
6
|
+
it("does not query when there are no trace ids", async () => {
|
|
7
|
+
const db = new MemSqlDb();
|
|
8
|
+
|
|
9
|
+
const spans = await fetchSpansForTraceIds(db, "default", []);
|
|
10
|
+
|
|
11
|
+
expect(spans).toEqual([]);
|
|
12
|
+
expect(db.calls).toHaveLength(0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("chunks large trace id lists to stay under D1 SQL variable limits", async () => {
|
|
16
|
+
const db = new MemSqlDb({ all: () => [] });
|
|
17
|
+
const traceIds = Array.from({ length: 1801 }, (_, i) => `trace-${i}`);
|
|
18
|
+
|
|
19
|
+
await fetchSpansForTraceIds(db, "default", traceIds);
|
|
20
|
+
|
|
21
|
+
const calls = db.callsMatching("FROM telemetry_spans");
|
|
22
|
+
expect(calls).toHaveLength(19);
|
|
23
|
+
expect(calls.slice(0, -1).map((call) => call.binds.length)).toEqual(
|
|
24
|
+
Array.from({ length: 18 }, () => 100),
|
|
25
|
+
);
|
|
26
|
+
expect(calls.at(-1)?.binds.length).toBe(20);
|
|
27
|
+
expect(calls.every((call) => call.binds[0] === "default")).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { SqlDb } from "../sql-db";
|
|
2
|
+
import {
|
|
3
|
+
type ParsedSpan,
|
|
4
|
+
placeholders,
|
|
5
|
+
rowToSpan,
|
|
6
|
+
spanSelectColumns,
|
|
7
|
+
type TraceCandidateRow,
|
|
8
|
+
toParsedSpan,
|
|
9
|
+
} from "./helpers";
|
|
10
|
+
|
|
11
|
+
const MAX_TRACE_ID_BINDINGS = 99;
|
|
12
|
+
|
|
13
|
+
export interface TraceCandidateOptions {
|
|
14
|
+
projectId: string;
|
|
15
|
+
cutoff: string;
|
|
16
|
+
service?: string;
|
|
17
|
+
search?: string;
|
|
18
|
+
status?: "all" | "error" | "ok";
|
|
19
|
+
limit: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function selectTraceCandidates(
|
|
23
|
+
db: SqlDb,
|
|
24
|
+
options: TraceCandidateOptions,
|
|
25
|
+
): Promise<TraceCandidateRow[]> {
|
|
26
|
+
let whereClause = "WHERE project_id = ? AND received_at >= ?";
|
|
27
|
+
const binds: unknown[] = [options.projectId, options.cutoff];
|
|
28
|
+
|
|
29
|
+
if (options.service) {
|
|
30
|
+
whereClause += " AND service_name = ?";
|
|
31
|
+
binds.push(options.service);
|
|
32
|
+
}
|
|
33
|
+
if (options.search) {
|
|
34
|
+
const term = `%${options.search}%`;
|
|
35
|
+
whereClause +=
|
|
36
|
+
" AND (span_name LIKE ? OR status_message LIKE ? OR attributes_json LIKE ? OR events_json LIKE ?)";
|
|
37
|
+
binds.push(term, term, term, term);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const having =
|
|
41
|
+
options.status === "error"
|
|
42
|
+
? "HAVING SUM(CASE WHEN status_code = 2 THEN 1 ELSE 0 END) > 0"
|
|
43
|
+
: options.status === "ok"
|
|
44
|
+
? "HAVING SUM(CASE WHEN status_code = 2 THEN 1 ELSE 0 END) = 0"
|
|
45
|
+
: "";
|
|
46
|
+
|
|
47
|
+
const result = await db
|
|
48
|
+
.prepare(`
|
|
49
|
+
SELECT
|
|
50
|
+
trace_id,
|
|
51
|
+
MAX(received_at) AS latest_received_at,
|
|
52
|
+
SUM(CASE WHEN status_code = 2 THEN 1 ELSE 0 END) AS error_span_count
|
|
53
|
+
FROM telemetry_spans
|
|
54
|
+
${whereClause}
|
|
55
|
+
GROUP BY trace_id
|
|
56
|
+
${having}
|
|
57
|
+
ORDER BY latest_received_at DESC
|
|
58
|
+
LIMIT ?
|
|
59
|
+
`)
|
|
60
|
+
.bind(...binds, options.limit)
|
|
61
|
+
.all<TraceCandidateRow>();
|
|
62
|
+
|
|
63
|
+
return result.results ?? [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function fetchSpansForTraceIds(
|
|
67
|
+
db: SqlDb,
|
|
68
|
+
projectId: string,
|
|
69
|
+
traceIds: string[],
|
|
70
|
+
): Promise<ParsedSpan[]> {
|
|
71
|
+
if (traceIds.length === 0) return [];
|
|
72
|
+
|
|
73
|
+
const rows: Record<string, unknown>[] = [];
|
|
74
|
+
for (let i = 0; i < traceIds.length; i += MAX_TRACE_ID_BINDINGS) {
|
|
75
|
+
const chunk = traceIds.slice(i, i + MAX_TRACE_ID_BINDINGS);
|
|
76
|
+
const result = await db
|
|
77
|
+
.prepare(`
|
|
78
|
+
SELECT ${spanSelectColumns}
|
|
79
|
+
FROM telemetry_spans
|
|
80
|
+
WHERE project_id = ? AND trace_id IN (${placeholders(chunk.length)})
|
|
81
|
+
ORDER BY received_at DESC, start_time ASC, span_id ASC
|
|
82
|
+
`)
|
|
83
|
+
.bind(projectId, ...chunk)
|
|
84
|
+
.all<Record<string, unknown>>();
|
|
85
|
+
rows.push(...(result.results ?? []));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return rows.map(rowToSpan).map(toParsedSpan);
|
|
89
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { TelemetryStore } from "./store";
|
|
3
|
+
import { MemSqlDb } from "./test-utils/mem-sql-db";
|
|
4
|
+
|
|
5
|
+
const spanRow = (overrides: Record<string, unknown>) => ({
|
|
6
|
+
project_id: "default",
|
|
7
|
+
trace_id: "trace-1",
|
|
8
|
+
span_id: "span-1",
|
|
9
|
+
parent_span_id: null,
|
|
10
|
+
service_name: "api",
|
|
11
|
+
scope_name: null,
|
|
12
|
+
scope_version: null,
|
|
13
|
+
span_name: "GET /checkout",
|
|
14
|
+
span_kind: 2,
|
|
15
|
+
status_code: 1,
|
|
16
|
+
status_message: null,
|
|
17
|
+
start_time: "2026-05-31T00:00:00.000Z",
|
|
18
|
+
end_time: "2026-05-31T00:00:00.100Z",
|
|
19
|
+
duration_ms: 100,
|
|
20
|
+
attributes_json: "{}",
|
|
21
|
+
resource_attributes_json: "{}",
|
|
22
|
+
events_json: "[]",
|
|
23
|
+
links_json: "[]",
|
|
24
|
+
received_at: "2026-05-31T00:00:01.000Z",
|
|
25
|
+
expires_at: "2026-06-01T00:00:00.000Z",
|
|
26
|
+
...overrides,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("TelemetryStore overview trace aggregation", () => {
|
|
30
|
+
it("selects trace ids first and fetches complete spans for selected traces", async () => {
|
|
31
|
+
const db = new MemSqlDb({
|
|
32
|
+
all: (sql) => {
|
|
33
|
+
if (sql.includes("GROUP BY trace_id")) {
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
trace_id: "trace-1",
|
|
37
|
+
latest_received_at: "2026-05-31T00:00:01.000Z",
|
|
38
|
+
error_span_count: 1,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
if (sql.includes("trace_id IN")) {
|
|
43
|
+
return [
|
|
44
|
+
spanRow({ span_id: "root", status_code: 1 }),
|
|
45
|
+
spanRow({
|
|
46
|
+
span_id: "child-error",
|
|
47
|
+
parent_span_id: "root",
|
|
48
|
+
span_name: "db.query",
|
|
49
|
+
status_code: 2,
|
|
50
|
+
status_message: "boom",
|
|
51
|
+
duration_ms: 25,
|
|
52
|
+
}),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const store = new TelemetryStore(db);
|
|
59
|
+
|
|
60
|
+
const overview = await store.getOverview({
|
|
61
|
+
projectId: "default",
|
|
62
|
+
hours: 1,
|
|
63
|
+
limit: 1,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(overview.traces).toHaveLength(1);
|
|
67
|
+
expect(overview.traces[0]).toMatchObject({
|
|
68
|
+
traceId: "trace-1",
|
|
69
|
+
spanCount: 2,
|
|
70
|
+
errorSpanCount: 1,
|
|
71
|
+
statusCode: 2,
|
|
72
|
+
statusMessage: "boom",
|
|
73
|
+
});
|
|
74
|
+
expect(db.calls[0].sql).toContain("GROUP BY trace_id");
|
|
75
|
+
expect(db.calls[1].sql).toContain("trace_id IN");
|
|
76
|
+
});
|
|
77
|
+
});
|