@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,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize OpenTelemetry `gen_ai.*` semantic-convention attributes, OTel MCP attributes,
|
|
3
|
+
* and OpenInference span kinds into the canonical obs-unified Agent Action Graph schema.
|
|
4
|
+
*
|
|
5
|
+
* References:
|
|
6
|
+
* https://opentelemetry.io/docs/specs/semconv/gen-ai/
|
|
7
|
+
* https://github.com/Arize-ai/openinference/blob/main/spec/
|
|
8
|
+
*
|
|
9
|
+
* Must run BEFORE ai-span-payloads-processor — that processor assumes
|
|
10
|
+
* `openinference.span.kind` and `ai.payload.*` are already set.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { JsonValue, StoredSpan } from "@obsunified/types";
|
|
14
|
+
import {
|
|
15
|
+
ACTION_CAUSED_BY_ID_KEY,
|
|
16
|
+
ACTION_CONFIDENCE_KEY,
|
|
17
|
+
ACTION_ID_KEY,
|
|
18
|
+
ACTION_KIND_KEY,
|
|
19
|
+
ACTION_ROOT_ID_KEY,
|
|
20
|
+
ACTOR_TYPE_KEY,
|
|
21
|
+
ActionKind,
|
|
22
|
+
AGENT_RUN_ID_KEY,
|
|
23
|
+
AI_PAYLOAD_INPUT_KEY,
|
|
24
|
+
AI_PAYLOAD_OUTPUT_KEY,
|
|
25
|
+
OPENINFERENCE_SPAN_KIND_KEY,
|
|
26
|
+
OpenInferenceSpanKind,
|
|
27
|
+
TOOL_ARGS_KEY,
|
|
28
|
+
TOOL_CALL_ID_KEY,
|
|
29
|
+
TOOL_NAME_KEY,
|
|
30
|
+
TOOL_SIDE_EFFECT_KEY,
|
|
31
|
+
} from "@obsunified/types/constants";
|
|
32
|
+
import type { CollectorPlugin } from "../framework/collector";
|
|
33
|
+
import { deriveActionId, resolveActionIdentity } from "../lib/action-identity";
|
|
34
|
+
import { parseJsonRecord } from "../lib/json";
|
|
35
|
+
|
|
36
|
+
const GEN_AI_PREFIX = "gen_ai.";
|
|
37
|
+
const MCP_PREFIX = "mcp.";
|
|
38
|
+
|
|
39
|
+
export { deriveActionId };
|
|
40
|
+
|
|
41
|
+
// Map gen_ai operation.name → OpenInference span kind.
|
|
42
|
+
const operationToKind = (op: unknown): string => {
|
|
43
|
+
if (typeof op !== "string") return OpenInferenceSpanKind.LLM;
|
|
44
|
+
const lower = op.toLowerCase();
|
|
45
|
+
if (lower.includes("embed")) return OpenInferenceSpanKind.EMBEDDING;
|
|
46
|
+
if (lower.includes("tool")) return OpenInferenceSpanKind.TOOL;
|
|
47
|
+
if (lower.includes("agent")) return OpenInferenceSpanKind.AGENT;
|
|
48
|
+
return OpenInferenceSpanKind.LLM;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const asNumber = (value: unknown): number | undefined => {
|
|
52
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
53
|
+
if (typeof value === "string") {
|
|
54
|
+
const parsed = Number(value);
|
|
55
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const toJsonString = (value: unknown): string | null => {
|
|
61
|
+
if (value === undefined || value === null) return null;
|
|
62
|
+
if (typeof value === "string") return value;
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify(value);
|
|
65
|
+
} catch {
|
|
66
|
+
return String(value);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const SIDE_EFFECT_VERBS = new Set([
|
|
71
|
+
"create",
|
|
72
|
+
"delete",
|
|
73
|
+
"insert",
|
|
74
|
+
"mutate",
|
|
75
|
+
"patch",
|
|
76
|
+
"remove",
|
|
77
|
+
"set",
|
|
78
|
+
"update",
|
|
79
|
+
"upsert",
|
|
80
|
+
"write",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const inferToolSideEffect = (toolName: unknown): 0 | 1 => {
|
|
84
|
+
if (typeof toolName !== "string") return 0;
|
|
85
|
+
const [firstToken] = toolName
|
|
86
|
+
.trim()
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.split(/[^a-z0-9]+/)
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
return firstToken && SIDE_EFFECT_VERBS.has(firstToken) ? 1 : 0;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const collectIndexed = (
|
|
94
|
+
attrs: Record<string, JsonValue>,
|
|
95
|
+
prefix: string,
|
|
96
|
+
): unknown | undefined => {
|
|
97
|
+
const scalar = attrs[prefix];
|
|
98
|
+
if (scalar !== undefined) return scalar;
|
|
99
|
+
|
|
100
|
+
const messages: Array<Record<string, JsonValue>> = [];
|
|
101
|
+
const indexed = Object.keys(attrs).filter((k) => k.startsWith(`${prefix}.`));
|
|
102
|
+
if (indexed.length === 0) return undefined;
|
|
103
|
+
|
|
104
|
+
for (const key of indexed) {
|
|
105
|
+
const rest = key.slice(prefix.length + 1);
|
|
106
|
+
const dot = rest.indexOf(".");
|
|
107
|
+
const idx = Number.parseInt(rest.slice(0, dot), 10);
|
|
108
|
+
if (!Number.isFinite(idx)) continue;
|
|
109
|
+
const field = rest.slice(dot + 1);
|
|
110
|
+
messages[idx] = messages[idx] ?? {};
|
|
111
|
+
messages[idx][field] = attrs[key];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const compact = messages.filter(
|
|
115
|
+
(m): m is Record<string, JsonValue> => m !== undefined,
|
|
116
|
+
);
|
|
117
|
+
return compact.length > 0 ? compact : undefined;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const genAiNormalizerPlugin: CollectorPlugin = {
|
|
121
|
+
name: "gen-ai-normalizer",
|
|
122
|
+
register(_app, runtime) {
|
|
123
|
+
runtime.addSpanProcessor({
|
|
124
|
+
name: "gen-ai-normalizer",
|
|
125
|
+
async process(spans) {
|
|
126
|
+
return Promise.all(
|
|
127
|
+
spans.map(async (span): Promise<StoredSpan> => {
|
|
128
|
+
const attrs = parseJsonRecord(span.attributesJson);
|
|
129
|
+
|
|
130
|
+
const mcpMethod = attrs["mcp.method.name"] || attrs["mcp.method"];
|
|
131
|
+
const jsonRpcId =
|
|
132
|
+
attrs["jsonrpc.request.id"] || attrs["jsonrpc.id"];
|
|
133
|
+
const hasMcp =
|
|
134
|
+
mcpMethod !== undefined ||
|
|
135
|
+
jsonRpcId !== undefined ||
|
|
136
|
+
Object.keys(attrs).some((k) => k.startsWith(MCP_PREFIX));
|
|
137
|
+
|
|
138
|
+
const hasGenAi = Object.keys(attrs).some((k) =>
|
|
139
|
+
k.startsWith(GEN_AI_PREFIX),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const openInferenceKind = attrs[OPENINFERENCE_SPAN_KIND_KEY];
|
|
143
|
+
const hasOpenInference = openInferenceKind !== undefined;
|
|
144
|
+
|
|
145
|
+
// If it's not a GenAI, MCP, or OpenInference span, keep it as is.
|
|
146
|
+
if (!hasGenAi && !hasMcp && !hasOpenInference) {
|
|
147
|
+
return span;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const normalized: Record<string, JsonValue> = { ...attrs };
|
|
151
|
+
|
|
152
|
+
// 1. Determine OpenInference Span Kind
|
|
153
|
+
let kind = openInferenceKind as string | undefined;
|
|
154
|
+
if (hasMcp && !kind) {
|
|
155
|
+
if (mcpMethod === "tools/call") {
|
|
156
|
+
kind = OpenInferenceSpanKind.TOOL;
|
|
157
|
+
} else if (mcpMethod === "resources/read") {
|
|
158
|
+
kind = OpenInferenceSpanKind.RETRIEVER;
|
|
159
|
+
} else if (mcpMethod === "prompts/get") {
|
|
160
|
+
kind = OpenInferenceSpanKind.PROMPT;
|
|
161
|
+
} else {
|
|
162
|
+
kind = OpenInferenceSpanKind.CHAIN;
|
|
163
|
+
}
|
|
164
|
+
} else if (hasGenAi && !kind) {
|
|
165
|
+
kind = operationToKind(attrs["gen_ai.operation.name"]);
|
|
166
|
+
}
|
|
167
|
+
if (kind) {
|
|
168
|
+
normalized[OPENINFERENCE_SPAN_KIND_KEY] = kind;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. Set Action Graph Schema Attributes
|
|
172
|
+
const identity = await resolveActionIdentity(span, attrs);
|
|
173
|
+
const actionId = identity.actionId;
|
|
174
|
+
|
|
175
|
+
normalized[ACTION_ID_KEY] = actionId;
|
|
176
|
+
normalized[ACTION_CONFIDENCE_KEY] = identity.confidence;
|
|
177
|
+
normalized[ACTION_ROOT_ID_KEY] = identity.rootActionId;
|
|
178
|
+
normalized[ACTION_CAUSED_BY_ID_KEY] = identity.causedByActionId;
|
|
179
|
+
|
|
180
|
+
if (normalized[ACTOR_TYPE_KEY] === undefined) {
|
|
181
|
+
normalized[ACTOR_TYPE_KEY] = "agent";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (normalized[AGENT_RUN_ID_KEY] === undefined) {
|
|
185
|
+
normalized[AGENT_RUN_ID_KEY] =
|
|
186
|
+
identity.agentRunId ?? identity.rootActionId;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Determine Action Kind
|
|
190
|
+
let actionKind = attrs[ACTION_KIND_KEY] as string | undefined;
|
|
191
|
+
if (!actionKind && kind) {
|
|
192
|
+
if (kind === OpenInferenceSpanKind.AGENT) {
|
|
193
|
+
actionKind = ActionKind.AgentStep;
|
|
194
|
+
} else if (kind === OpenInferenceSpanKind.LLM) {
|
|
195
|
+
actionKind = ActionKind.LlmCall;
|
|
196
|
+
} else if (kind === OpenInferenceSpanKind.TOOL) {
|
|
197
|
+
actionKind = ActionKind.ToolCall;
|
|
198
|
+
} else if (kind === OpenInferenceSpanKind.RETRIEVER) {
|
|
199
|
+
actionKind = ActionKind.Retrieval;
|
|
200
|
+
} else if (kind === OpenInferenceSpanKind.EVALUATOR) {
|
|
201
|
+
actionKind = ActionKind.Eval;
|
|
202
|
+
} else {
|
|
203
|
+
actionKind = ActionKind.AgentStep;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (actionKind) {
|
|
207
|
+
normalized[ACTION_KIND_KEY] = actionKind;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 3. MCP Specific Mapping Details
|
|
211
|
+
if (hasMcp) {
|
|
212
|
+
if (kind === OpenInferenceSpanKind.TOOL) {
|
|
213
|
+
if (normalized[TOOL_CALL_ID_KEY] === undefined) {
|
|
214
|
+
normalized[TOOL_CALL_ID_KEY] = actionId;
|
|
215
|
+
}
|
|
216
|
+
if (normalized[TOOL_NAME_KEY] === undefined) {
|
|
217
|
+
const tName =
|
|
218
|
+
attrs["mcp.tool.name"] ??
|
|
219
|
+
attrs["mcp.operation.name"] ??
|
|
220
|
+
attrs["mcp.name"] ??
|
|
221
|
+
span.spanName;
|
|
222
|
+
normalized[TOOL_NAME_KEY] = tName;
|
|
223
|
+
normalized["obs.tool_call.tool_name"] = tName;
|
|
224
|
+
}
|
|
225
|
+
if (normalized[TOOL_ARGS_KEY] === undefined) {
|
|
226
|
+
const rawArgs =
|
|
227
|
+
attrs["mcp.tool.arguments"] ??
|
|
228
|
+
attrs["mcp.operation.arguments"] ??
|
|
229
|
+
attrs["mcp.arguments"];
|
|
230
|
+
const tArgs = toJsonString(rawArgs) ?? "{}";
|
|
231
|
+
normalized[TOOL_ARGS_KEY] = tArgs;
|
|
232
|
+
normalized["obs.tool_call.args"] = tArgs;
|
|
233
|
+
normalized[AI_PAYLOAD_INPUT_KEY] = tArgs;
|
|
234
|
+
}
|
|
235
|
+
if (normalized[TOOL_SIDE_EFFECT_KEY] === undefined) {
|
|
236
|
+
const sideEffect =
|
|
237
|
+
attrs["mcp.tool.side_effect"] ??
|
|
238
|
+
attrs["mcp.side_effect"] ??
|
|
239
|
+
inferToolSideEffect(attrs["mcp.tool.name"]);
|
|
240
|
+
normalized[TOOL_SIDE_EFFECT_KEY] = sideEffect;
|
|
241
|
+
normalized["obs.tool_call.side_effect"] = sideEffect;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 4. GenAI Model + provider normalization
|
|
247
|
+
const model =
|
|
248
|
+
(attrs["gen_ai.response.model"] as string | undefined) ||
|
|
249
|
+
(attrs["gen_ai.request.model"] as string | undefined);
|
|
250
|
+
const provider = attrs["gen_ai.system"];
|
|
251
|
+
if (kind === OpenInferenceSpanKind.LLM) {
|
|
252
|
+
if (typeof model === "string")
|
|
253
|
+
normalized["llm.model_name"] = model;
|
|
254
|
+
if (typeof provider === "string")
|
|
255
|
+
normalized["llm.provider"] = provider;
|
|
256
|
+
} else if (kind === OpenInferenceSpanKind.EMBEDDING) {
|
|
257
|
+
if (typeof model === "string")
|
|
258
|
+
normalized["embedding.model_name"] = model;
|
|
259
|
+
if (typeof provider === "string")
|
|
260
|
+
normalized["embedding.provider"] = provider;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// GenAI Token Usage
|
|
264
|
+
const promptTokens =
|
|
265
|
+
asNumber(attrs["gen_ai.usage.input_tokens"]) ??
|
|
266
|
+
asNumber(attrs["gen_ai.usage.prompt_tokens"]);
|
|
267
|
+
const completionTokens =
|
|
268
|
+
asNumber(attrs["gen_ai.usage.output_tokens"]) ??
|
|
269
|
+
asNumber(attrs["gen_ai.usage.completion_tokens"]);
|
|
270
|
+
if (promptTokens !== undefined)
|
|
271
|
+
normalized["llm.token_count.prompt"] = promptTokens;
|
|
272
|
+
if (completionTokens !== undefined)
|
|
273
|
+
normalized["llm.token_count.completion"] = completionTokens;
|
|
274
|
+
if (promptTokens !== undefined && completionTokens !== undefined) {
|
|
275
|
+
normalized["llm.token_count.total"] =
|
|
276
|
+
promptTokens + completionTokens;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// GenAI Inputs/Outputs
|
|
280
|
+
if (normalized[AI_PAYLOAD_INPUT_KEY] === undefined) {
|
|
281
|
+
const input = collectIndexed(attrs, "gen_ai.prompt");
|
|
282
|
+
if (input !== undefined) {
|
|
283
|
+
normalized[AI_PAYLOAD_INPUT_KEY] = toJsonString(input) ?? "";
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (normalized[AI_PAYLOAD_OUTPUT_KEY] === undefined) {
|
|
287
|
+
const output = collectIndexed(attrs, "gen_ai.completion");
|
|
288
|
+
if (output !== undefined) {
|
|
289
|
+
normalized[AI_PAYLOAD_OUTPUT_KEY] = toJsonString(output) ?? "";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
...span,
|
|
295
|
+
attributesJson: JSON.stringify(normalized),
|
|
296
|
+
};
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { IdentifyInput } from "@obsunified/types";
|
|
2
|
+
import type { CollectorPlugin } from "../framework/collector";
|
|
3
|
+
import { dialectFor, sqlDbFor } from "../lib/sql-db";
|
|
4
|
+
import { getProjectId } from "./_context";
|
|
5
|
+
|
|
6
|
+
export const identityReceiverPlugin: CollectorPlugin = {
|
|
7
|
+
name: "identity-receiver",
|
|
8
|
+
register(app) {
|
|
9
|
+
app.post("/v1/identify", async (c) => {
|
|
10
|
+
const projectId = getProjectId(c);
|
|
11
|
+
let payload: IdentifyInput;
|
|
12
|
+
try {
|
|
13
|
+
payload = await c.req.json<IdentifyInput>();
|
|
14
|
+
} catch {
|
|
15
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
16
|
+
}
|
|
17
|
+
if (!payload.userId || typeof payload.userId !== "string") {
|
|
18
|
+
return c.json({ error: "userId is required" }, 400);
|
|
19
|
+
}
|
|
20
|
+
if (payload.userId.length > 256) {
|
|
21
|
+
return c.json({ error: "userId too long (max 256)" }, 400);
|
|
22
|
+
}
|
|
23
|
+
if (
|
|
24
|
+
payload.email &&
|
|
25
|
+
(typeof payload.email !== "string" || payload.email.length > 320)
|
|
26
|
+
) {
|
|
27
|
+
return c.json({ error: "Invalid email" }, 400);
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
payload.name &&
|
|
31
|
+
(typeof payload.name !== "string" || payload.name.length > 256)
|
|
32
|
+
) {
|
|
33
|
+
return c.json({ error: "Invalid name" }, 400);
|
|
34
|
+
}
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
|
|
37
|
+
// Backfill window: callers (synthetic seeds, historical imports)
|
|
38
|
+
// can pass firstSeenAt to set the user's earliest observed
|
|
39
|
+
// activity. We accept anything in the past; future timestamps
|
|
40
|
+
// fall back to `now` so a clock-skew client can't poison the
|
|
41
|
+
// table.
|
|
42
|
+
let firstSeenAt = now;
|
|
43
|
+
if (payload.firstSeenAt) {
|
|
44
|
+
const parsed = new Date(payload.firstSeenAt);
|
|
45
|
+
if (!Number.isNaN(parsed.getTime()) && parsed.getTime() <= Date.now()) {
|
|
46
|
+
firstSeenAt = parsed.toISOString();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const propertiesJson = payload.properties
|
|
51
|
+
? JSON.stringify(payload.properties)
|
|
52
|
+
: null;
|
|
53
|
+
|
|
54
|
+
// user_id is the PK today (single-tenant legacy). For MVP, we keep
|
|
55
|
+
// that contract — user_id is treated as globally unique and the
|
|
56
|
+
// first project to identify them "owns" the profile. project_id on
|
|
57
|
+
// the row reflects where the user was first seen.
|
|
58
|
+
//
|
|
59
|
+
// On conflict we keep the EARLIEST first_seen_at via MIN() — once
|
|
60
|
+
// observed, a user's first-seen is monotonic; an identify() call
|
|
61
|
+
// that runs at "now" must not overwrite a true historical value.
|
|
62
|
+
const db = sqlDbFor(c.env);
|
|
63
|
+
const firstSeenExpr =
|
|
64
|
+
dialectFor(db).name === "postgres"
|
|
65
|
+
? "LEAST(user_profiles.first_seen_at, excluded.first_seen_at)"
|
|
66
|
+
: "MIN(user_profiles.first_seen_at, excluded.first_seen_at)";
|
|
67
|
+
|
|
68
|
+
await db
|
|
69
|
+
.prepare(
|
|
70
|
+
`INSERT INTO user_profiles (project_id, user_id, visitor_id, email, name, properties_json, first_seen_at, last_seen_at)
|
|
71
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
72
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
73
|
+
visitor_id = excluded.visitor_id,
|
|
74
|
+
email = COALESCE(excluded.email, user_profiles.email),
|
|
75
|
+
name = COALESCE(excluded.name, user_profiles.name),
|
|
76
|
+
properties_json = COALESCE(excluded.properties_json, user_profiles.properties_json),
|
|
77
|
+
first_seen_at = ${firstSeenExpr},
|
|
78
|
+
last_seen_at = excluded.last_seen_at
|
|
79
|
+
`,
|
|
80
|
+
)
|
|
81
|
+
.bind(
|
|
82
|
+
projectId,
|
|
83
|
+
payload.userId,
|
|
84
|
+
payload.visitorId,
|
|
85
|
+
payload.email ?? null,
|
|
86
|
+
payload.name ?? null,
|
|
87
|
+
propertiesJson,
|
|
88
|
+
firstSeenAt,
|
|
89
|
+
now,
|
|
90
|
+
)
|
|
91
|
+
.run();
|
|
92
|
+
|
|
93
|
+
return c.json({ success: true, userId: payload.userId });
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { JsonValue, StoredSpan } from "@obsunified/types";
|
|
2
|
+
import type { CollectorPlugin } from "../framework/collector";
|
|
3
|
+
import { parseJsonRecord } from "../lib/json";
|
|
4
|
+
|
|
5
|
+
const INTERNAL_PATH_PREFIXES = [
|
|
6
|
+
"/api/admin",
|
|
7
|
+
"/api/internal",
|
|
8
|
+
"/internal/telemetry",
|
|
9
|
+
"/v1/traces",
|
|
10
|
+
"/health",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const getStringValue = (
|
|
14
|
+
record: Record<string, JsonValue>,
|
|
15
|
+
keys: string[],
|
|
16
|
+
): string | null => {
|
|
17
|
+
for (const key of keys) {
|
|
18
|
+
const value = record[key];
|
|
19
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const normalizePathSegment = (segment: string): string => {
|
|
25
|
+
if (!segment) return segment;
|
|
26
|
+
if (/^\d+$/.test(segment)) return ":id";
|
|
27
|
+
if (/^[0-9a-f]{16,}$/i.test(segment)) return ":id";
|
|
28
|
+
if (
|
|
29
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
30
|
+
segment,
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
return ":uuid";
|
|
34
|
+
return segment;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const normalizePath = (path: string): string => {
|
|
38
|
+
const [pathname] = path.split("?");
|
|
39
|
+
const segments = pathname
|
|
40
|
+
.split("/")
|
|
41
|
+
.map(normalizePathSegment)
|
|
42
|
+
.filter((segment, index) => !(index === 0 && segment === ""));
|
|
43
|
+
return `/${segments.join("/")}`.replace(/\/{2,}/g, "/");
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const isInternalPath = (path: string): boolean =>
|
|
47
|
+
INTERNAL_PATH_PREFIXES.some(
|
|
48
|
+
(prefix) => path === prefix || path.startsWith(`${prefix}/`),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const deriveDependencyTarget = (
|
|
52
|
+
attributes: Record<string, JsonValue>,
|
|
53
|
+
resourceAttributes: Record<string, JsonValue>,
|
|
54
|
+
): string | null => {
|
|
55
|
+
const dbSystem = getStringValue(attributes, ["db.system"]);
|
|
56
|
+
if (dbSystem) return dbSystem;
|
|
57
|
+
const peerService = getStringValue(attributes, [
|
|
58
|
+
"peer.service",
|
|
59
|
+
"server.address",
|
|
60
|
+
"http.host",
|
|
61
|
+
]);
|
|
62
|
+
if (peerService) return peerService;
|
|
63
|
+
const urlValue = getStringValue(attributes, ["url.full", "http.url"]);
|
|
64
|
+
if (urlValue) {
|
|
65
|
+
try {
|
|
66
|
+
return new URL(urlValue).host;
|
|
67
|
+
} catch {
|
|
68
|
+
return urlValue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return getStringValue(resourceAttributes, ["host.name"]);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const issueEnrichmentPlugin: CollectorPlugin = {
|
|
75
|
+
name: "issue-enrichment",
|
|
76
|
+
register(_app, runtime) {
|
|
77
|
+
runtime.addSpanProcessor({
|
|
78
|
+
name: "issue-enrichment",
|
|
79
|
+
process(spans) {
|
|
80
|
+
return spans.map((span): StoredSpan => {
|
|
81
|
+
const attributes = parseJsonRecord(span.attributesJson);
|
|
82
|
+
const resourceAttributes = parseJsonRecord(
|
|
83
|
+
span.resourceAttributesJson,
|
|
84
|
+
);
|
|
85
|
+
const method = getStringValue(attributes, [
|
|
86
|
+
"http.request.method",
|
|
87
|
+
"http.method",
|
|
88
|
+
]);
|
|
89
|
+
const path = getStringValue(attributes, [
|
|
90
|
+
"url.path",
|
|
91
|
+
"http.target",
|
|
92
|
+
"http.route",
|
|
93
|
+
]);
|
|
94
|
+
const routePath = path ? normalizePath(path) : null;
|
|
95
|
+
const existingRouteLabel = getStringValue(attributes, [
|
|
96
|
+
"collector.route_label",
|
|
97
|
+
]);
|
|
98
|
+
const dependencyTarget = deriveDependencyTarget(
|
|
99
|
+
attributes,
|
|
100
|
+
resourceAttributes,
|
|
101
|
+
);
|
|
102
|
+
const isDependencySpan = Boolean(
|
|
103
|
+
dependencyTarget ||
|
|
104
|
+
span.spanKind === 3 ||
|
|
105
|
+
span.spanKind === 4 ||
|
|
106
|
+
span.spanKind === 5,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const nextAttributes: Record<string, JsonValue> = { ...attributes };
|
|
110
|
+
|
|
111
|
+
if (routePath) {
|
|
112
|
+
nextAttributes["collector.route_key"] = routePath;
|
|
113
|
+
if (!existingRouteLabel) {
|
|
114
|
+
nextAttributes["collector.route_label"] = method
|
|
115
|
+
? `${method} ${routePath}`
|
|
116
|
+
: routePath;
|
|
117
|
+
}
|
|
118
|
+
nextAttributes["collector.is_internal"] = isInternalPath(routePath);
|
|
119
|
+
}
|
|
120
|
+
if (dependencyTarget && isDependencySpan) {
|
|
121
|
+
nextAttributes["collector.dependency_target"] = dependencyTarget;
|
|
122
|
+
}
|
|
123
|
+
if (isDependencySpan) {
|
|
124
|
+
nextAttributes["collector.span_role"] = "dependency";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { ...span, attributesJson: JSON.stringify(nextAttributes) };
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_WINDOW_HOURS,
|
|
3
|
+
MAX_ISSUE_ROWS,
|
|
4
|
+
RETENTION_HOURS,
|
|
5
|
+
} from "@obsunified/types/constants";
|
|
6
|
+
import type { CollectorPlugin } from "../framework/collector";
|
|
7
|
+
import { getProjectId } from "./_context";
|
|
8
|
+
|
|
9
|
+
const parseHours = (rawValue?: string): number =>
|
|
10
|
+
Math.min(
|
|
11
|
+
RETENTION_HOURS,
|
|
12
|
+
Math.max(
|
|
13
|
+
1,
|
|
14
|
+
Number.parseInt(rawValue || String(DEFAULT_WINDOW_HOURS), 10) ||
|
|
15
|
+
DEFAULT_WINDOW_HOURS,
|
|
16
|
+
),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const parseLimit = (rawValue?: string): number =>
|
|
20
|
+
Math.min(
|
|
21
|
+
MAX_ISSUE_ROWS,
|
|
22
|
+
Math.max(
|
|
23
|
+
1,
|
|
24
|
+
Number.parseInt(rawValue || String(MAX_ISSUE_ROWS), 10) || MAX_ISSUE_ROWS,
|
|
25
|
+
),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export const issueInsightsPlugin: CollectorPlugin = {
|
|
29
|
+
name: "issue-insights",
|
|
30
|
+
register(app, runtime) {
|
|
31
|
+
app.get("/internal/telemetry/issues", async (c) => {
|
|
32
|
+
const projectId = getProjectId(c);
|
|
33
|
+
const store = runtime.createStore(c.env);
|
|
34
|
+
const overview = await store.getIssueOverview({
|
|
35
|
+
projectId,
|
|
36
|
+
hours: parseHours(c.req.query("hours")),
|
|
37
|
+
service: c.req.query("service") || undefined,
|
|
38
|
+
category:
|
|
39
|
+
c.req.query("category") === "error" ||
|
|
40
|
+
c.req.query("category") === "latency" ||
|
|
41
|
+
c.req.query("category") === "dependency"
|
|
42
|
+
? (c.req.query("category") as "error" | "latency" | "dependency")
|
|
43
|
+
: "all",
|
|
44
|
+
includeInternal: c.req.query("includeInternal") === "true",
|
|
45
|
+
limit: parseLimit(c.req.query("limit")),
|
|
46
|
+
});
|
|
47
|
+
return c.json({
|
|
48
|
+
...overview,
|
|
49
|
+
plugins: runtime.getRegisteredPluginNames(),
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
app.get("/internal/telemetry/issues/detail", async (c) => {
|
|
54
|
+
const issueId = c.req.query("issueId");
|
|
55
|
+
if (!issueId)
|
|
56
|
+
return c.json(
|
|
57
|
+
{ error: "Bad Request", message: "issueId is required" },
|
|
58
|
+
400,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const projectId = getProjectId(c);
|
|
62
|
+
const store = runtime.createStore(c.env);
|
|
63
|
+
const detail = await store.getIssueDetail(issueId, {
|
|
64
|
+
projectId,
|
|
65
|
+
hours: parseHours(c.req.query("hours")),
|
|
66
|
+
service: c.req.query("service") || undefined,
|
|
67
|
+
category:
|
|
68
|
+
c.req.query("category") === "error" ||
|
|
69
|
+
c.req.query("category") === "latency" ||
|
|
70
|
+
c.req.query("category") === "dependency"
|
|
71
|
+
? (c.req.query("category") as "error" | "latency" | "dependency")
|
|
72
|
+
: "all",
|
|
73
|
+
includeInternal: c.req.query("includeInternal") === "true",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!detail)
|
|
77
|
+
return c.json({ error: "Not Found", message: "Issue not found" }, 404);
|
|
78
|
+
return c.json({ ...detail, plugins: runtime.getRegisteredPluginNames() });
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
};
|