@genn-inc/cluebase-cli 0.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 (38) hide show
  1. package/README.md +101 -0
  2. package/bin/cluebase-cli.mjs +11 -0
  3. package/package.json +17 -0
  4. package/src/cli-command.mjs +515 -0
  5. package/src/cli-invocation.mjs +17 -0
  6. package/src/code-evidence-analyzer.mjs +2041 -0
  7. package/src/contracts.mjs +36 -0
  8. package/src/generated-code-evidence-contract.mjs +22 -0
  9. package/src/generated-sdk-version-contract.mjs +5 -0
  10. package/src/generated-source-path-policy.mjs +20 -0
  11. package/src/lifecycle-guard.mjs +202 -0
  12. package/src/path-policy.mjs +81 -0
  13. package/src/setup-ai-contract.mjs +221 -0
  14. package/src/setup-check-constants.mjs +110 -0
  15. package/src/setup-check-scan-a.mjs +849 -0
  16. package/src/setup-check-scan-b.mjs +994 -0
  17. package/src/setup-check.mjs +575 -0
  18. package/src/setup-discover-check.mjs +755 -0
  19. package/src/setup-doctor-deadline.mjs +221 -0
  20. package/src/setup-doctor-env.mjs +331 -0
  21. package/src/setup-doctor-file-boundary.mjs +426 -0
  22. package/src/setup-doctor-probe.mjs +719 -0
  23. package/src/setup-doctor-quality-checks-a.mjs +593 -0
  24. package/src/setup-doctor-quality-checks-b.mjs +638 -0
  25. package/src/setup-doctor-quality-shared.mjs +382 -0
  26. package/src/setup-doctor-quality.mjs +209 -0
  27. package/src/setup-doctor-route-scan.mjs +160 -0
  28. package/src/setup-doctor-sdk-probe.mjs +340 -0
  29. package/src/setup-doctor.mjs +545 -0
  30. package/src/setup-documents.mjs +112 -0
  31. package/src/setup-help.mjs +130 -0
  32. package/src/setup-prepare.mjs +360 -0
  33. package/src/setup-repository-discovery.mjs +764 -0
  34. package/src/setup-step-builders-discover.mjs +701 -0
  35. package/src/setup-step-builders-events.mjs +229 -0
  36. package/src/setup-step-builders-implement.mjs +710 -0
  37. package/src/setup-step-commands.mjs +427 -0
  38. package/src/setup-tool.mjs +27 -0
@@ -0,0 +1,719 @@
1
+ // setup-doctor ingest probe + payload builders.
2
+ // Extracted from setup-doctor for file-size limits; content is unchanged.
3
+
4
+ import {
5
+ CLUEBASE_TEST_SETUP_SDK_VERSION,
6
+ FRONTEND_SOURCE_IDENTIFIER,
7
+ BATCH_VISIBILITY_POLL_INTERVAL_MS,
8
+ optionalString,
9
+ trimTrailingSlash,
10
+ joinUrl,
11
+ batchStatusPathFor,
12
+ manifestDetectedServices,
13
+ } from "./setup-doctor-env.mjs";
14
+
15
+ export const firstTargetBackendTargetId = (manifest) => {
16
+ for (const target of manifestDetectedServices(manifest, "backend")) {
17
+ const targetId = optionalString(target.target_id);
18
+ if (targetId) return targetId;
19
+ }
20
+ return null;
21
+ };
22
+
23
+ export const readTextResponse = async (response) => {
24
+ try {
25
+ return await response.text();
26
+ } catch {
27
+ return "";
28
+ }
29
+ };
30
+
31
+ export const parseJsonText = (text) => {
32
+ try {
33
+ return text.trim() ? JSON.parse(text) : null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ };
38
+
39
+ export const postJson = async ({ body, fetchImpl, headers = {}, url }) => {
40
+ try {
41
+ const response = await fetchImpl(url, {
42
+ method: "POST",
43
+ headers: {
44
+ "content-type": "application/json",
45
+ ...headers,
46
+ },
47
+ body: JSON.stringify(body),
48
+ });
49
+ const text = await readTextResponse(response);
50
+ return {
51
+ transportOk: true,
52
+ response,
53
+ text,
54
+ json: parseJsonText(text),
55
+ };
56
+ } catch (error) {
57
+ return {
58
+ transportOk: false,
59
+ error: error instanceof Error ? error.message : String(error),
60
+ };
61
+ }
62
+ };
63
+
64
+ export const compactFailure = (result) => {
65
+ if (!result.transportOk) return result.error ?? "request failed";
66
+ const jsonMessage =
67
+ typeof result.json?.message === "string"
68
+ ? result.json.message
69
+ : typeof result.json?.error === "string"
70
+ ? result.json.error
71
+ : null;
72
+ return jsonMessage ?? result.text?.slice(0, 240) ?? "request failed";
73
+ };
74
+
75
+ export const tokenFromResult = (result) =>
76
+ result.transportOk &&
77
+ result.response.ok &&
78
+ typeof result.json?.token === "string" &&
79
+ result.json.token.trim() &&
80
+ typeof result.json?.expiresAt === "string" &&
81
+ result.json.expiresAt.trim()
82
+ ? result.json.token.trim()
83
+ : null;
84
+
85
+ export const observationBatchIdFromResult = (result) => {
86
+ if (!result.transportOk || !result.response.ok) return null;
87
+ const json = result.json;
88
+ return typeof json?.observation_ingest_batch_id === "string" &&
89
+ json.observation_ingest_batch_id.trim()
90
+ ? json.observation_ingest_batch_id.trim()
91
+ : null;
92
+ };
93
+
94
+ // A 401 whose body says the project key is invalid means the manifest's
95
+ // project_key can no longer be resolved by the Cluebase backend (e.g. the project
96
+ // was re-provisioned and the stored key rotated). This is a self-healing
97
+ // customer state, NOT an endpoint bug — surface the recovery path so the
98
+ // failure is not misdiagnosed as a Cluebase API defect (Round 1 was misdiagnosed
99
+ // this way). The fail verdict is unchanged; this only adds guidance.
100
+ // A batch that is accepted (201) but not yet published within the polling
101
+ // window is often just slow local materialization (worker / pipeline latency),
102
+ // not a genuine failure. Point the user at the timeout override before they
103
+ // assume the pipeline is broken.
104
+ export const BATCH_TIMEOUT_HINT =
105
+ " ローカルで worker / pipeline の処理に時間がかかる場合は、環境変数 CLUEBASE_SETUP_VERIFICATION_BATCH_TIMEOUT_MS を延長して /cluebase-doctor を再実行してください。";
106
+
107
+ export const STALE_PROJECT_KEY_REMEDIATION =
108
+ "`.cluebase/setup-manifest.json` の project_key が Cluebase backend で解決できません (鍵が古い / 再発行された可能性)。setup 画面から現行の project key を再取得して setup をやり直すか、クリーンな状態で /cluebase-discover を実行して manifest を再生成してください。これは endpoint 側のコードではなく、manifest の鍵と Cluebase backend の不一致が原因です。";
109
+
110
+ export const staleProjectKeyRemediation = (result) => {
111
+ if (!result?.transportOk || result.response?.status !== 401) return null;
112
+ const message = (
113
+ (typeof result.json?.message === "string" && result.json.message) ||
114
+ (typeof result.json?.error === "string" && result.json.error) ||
115
+ result.text ||
116
+ ""
117
+ ).toLowerCase();
118
+ return message.includes("invalid project key")
119
+ ? STALE_PROJECT_KEY_REMEDIATION
120
+ : null;
121
+ };
122
+
123
+ export const buildCheck = ({
124
+ error = null,
125
+ id,
126
+ method = "POST",
127
+ passed,
128
+ result = null,
129
+ url,
130
+ }) => {
131
+ const check = {
132
+ id,
133
+ // API 疎通 check は production reach への前提条件 (= 認証 / network) なので
134
+ // failure 時は ERROR + halt。 quality checks (= setup-doctor-quality.mjs) は
135
+ // severity を独立に付与する。
136
+ severity: "error",
137
+ method,
138
+ url,
139
+ passed: Boolean(passed),
140
+ status: result?.transportOk ? result.response.status : null,
141
+ error: passed ? null : (error ?? compactFailure(result)),
142
+ };
143
+ if (!check.passed) {
144
+ const remediation = staleProjectKeyRemediation(result);
145
+ if (remediation) check.remediation_prompt = remediation;
146
+ }
147
+ return check;
148
+ };
149
+
150
+ export const typedEvidenceCountsFrom = (json) => {
151
+ const value = json?.typed_evidence_counts;
152
+ if (!value || typeof value !== "object") return null;
153
+ const count = (key) =>
154
+ typeof value[key] === "number" && Number.isFinite(value[key])
155
+ ? value[key]
156
+ : null;
157
+ return {
158
+ typed_observation_count: count("typed_observation_count"),
159
+ identity_link_count: count("identity_link_count"),
160
+ backend_operation_count: count("backend_operation_count"),
161
+ backend_data_operation_count: count("backend_data_operation_count"),
162
+ backend_decision_count: count("backend_decision_count"),
163
+ backend_dependency_count: count("backend_dependency_count"),
164
+ };
165
+ };
166
+
167
+ export const dataUsabilityFrom = ({ checkId, typedEvidenceCounts }) => {
168
+ if (!typedEvidenceCounts) {
169
+ return {
170
+ available: false,
171
+ passed: false,
172
+ reason: "batch-status response does not include typed_evidence_counts",
173
+ };
174
+ }
175
+ const typedObservationCount =
176
+ typedEvidenceCounts.typed_observation_count ?? 0;
177
+ if (checkId === "browser_ingest_reached_ledger") {
178
+ const identityLinkCount = typedEvidenceCounts.identity_link_count ?? 0;
179
+ return {
180
+ available: true,
181
+ passed: typedObservationCount > 0 && identityLinkCount > 0,
182
+ required_counts: {
183
+ typed_observation_count: typedObservationCount,
184
+ identity_link_count: identityLinkCount,
185
+ },
186
+ };
187
+ }
188
+ if (checkId === "backend_ingest_reached_ledger") {
189
+ const backendOperationCount =
190
+ typedEvidenceCounts.backend_operation_count ?? 0;
191
+ const backendDataOperationCount =
192
+ typedEvidenceCounts.backend_data_operation_count ?? 0;
193
+ const backendDecisionCount =
194
+ typedEvidenceCounts.backend_decision_count ?? 0;
195
+ const backendDependencyCount =
196
+ typedEvidenceCounts.backend_dependency_count ?? 0;
197
+ return {
198
+ available: true,
199
+ passed:
200
+ typedObservationCount > 0 &&
201
+ backendOperationCount > 0 &&
202
+ backendDataOperationCount > 0 &&
203
+ backendDecisionCount > 0 &&
204
+ backendDependencyCount > 0,
205
+ required_counts: {
206
+ typed_observation_count: typedObservationCount,
207
+ backend_operation_count: backendOperationCount,
208
+ backend_data_operation_count: backendDataOperationCount,
209
+ backend_decision_count: backendDecisionCount,
210
+ backend_dependency_count: backendDependencyCount,
211
+ },
212
+ };
213
+ }
214
+ return {
215
+ available: true,
216
+ passed: typedObservationCount > 0,
217
+ required_counts: {
218
+ typed_observation_count: typedObservationCount,
219
+ },
220
+ };
221
+ };
222
+
223
+ export const pollBatchVisibility = async ({
224
+ apiKey,
225
+ batchId,
226
+ checkId,
227
+ cluebaseApiBaseUrl,
228
+ fetchImpl,
229
+ projectKey,
230
+ timeoutMs,
231
+ }) => {
232
+ const statusUrl = joinUrl(cluebaseApiBaseUrl, batchStatusPathFor(batchId));
233
+ const intervalMs = BATCH_VISIBILITY_POLL_INTERVAL_MS;
234
+ const maxAttempts = Math.max(1, Math.ceil(timeoutMs / intervalMs));
235
+ let pollResult = null;
236
+ let observedStatus = "accepted";
237
+ let observedError = null;
238
+ let observedStage = "accepted";
239
+ let published = false;
240
+ let publishedObservationCount = null;
241
+ let typedEvidenceCounts = null;
242
+ let dataUsability = dataUsabilityFrom({
243
+ checkId,
244
+ typedEvidenceCounts: null,
245
+ });
246
+
247
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
248
+ const response = await fetchImpl(statusUrl, {
249
+ method: "GET",
250
+ headers: {
251
+ "x-cluebase-project-key": projectKey,
252
+ "x-cluebase-api-key": apiKey,
253
+ },
254
+ })
255
+ .then(async (r) => {
256
+ const text = await r.text();
257
+ let json = null;
258
+ try {
259
+ json = text ? JSON.parse(text) : null;
260
+ } catch {
261
+ json = null;
262
+ }
263
+ return { response: r, text, json, transportOk: true };
264
+ })
265
+ .catch((error) => ({
266
+ transportOk: false,
267
+ error: error instanceof Error ? error.message : String(error),
268
+ }));
269
+ pollResult = response;
270
+
271
+ if (!response.transportOk) break;
272
+ if (response.response.status === 404) {
273
+ observedStatus = "accepted";
274
+ observedStage = "accepted";
275
+ } else if (response.response.ok) {
276
+ observedStatus =
277
+ typeof response.json?.status === "string"
278
+ ? response.json.status
279
+ : "unknown";
280
+ observedError =
281
+ typeof response.json?.error_message === "string"
282
+ ? response.json.error_message
283
+ : null;
284
+ publishedObservationCount =
285
+ typeof response.json?.published_observation_count === "number"
286
+ ? response.json.published_observation_count
287
+ : null;
288
+ published =
289
+ response.json?.published === true &&
290
+ typeof publishedObservationCount === "number" &&
291
+ publishedObservationCount > 0;
292
+ typedEvidenceCounts = typedEvidenceCountsFrom(response.json);
293
+ dataUsability = dataUsabilityFrom({ checkId, typedEvidenceCounts });
294
+
295
+ if (observedStatus === "completed" && published) {
296
+ observedStage = "published";
297
+ break;
298
+ }
299
+ if (
300
+ observedStatus === "received" ||
301
+ observedStatus === "completed" ||
302
+ observedStatus === "failed"
303
+ ) {
304
+ observedStage = observedStatus;
305
+ } else {
306
+ observedStage = "accepted";
307
+ }
308
+ if (observedStatus === "failed") break;
309
+ } else {
310
+ break;
311
+ }
312
+
313
+ if (attempt < maxAttempts - 1) {
314
+ await (fetchImpl.waitForPollInterval?.(intervalMs) ?? new Promise((resolve) => setTimeout(resolve, intervalMs)));
315
+ }
316
+ }
317
+
318
+ const publishedPassed = observedStage === "published";
319
+ const dataUsabilityPassed = dataUsability.passed === true;
320
+ const passed = publishedPassed && dataUsabilityPassed;
321
+ const error = passed
322
+ ? null
323
+ : publishedPassed
324
+ ? `batch reached published state but typed data usability failed: ${dataUsability.reason ?? "required typed evidence count is missing"}`
325
+ : observedStatus === "failed"
326
+ ? `worker normalize failed: ${observedError ?? "no error message"}`
327
+ : observedStage === "accepted"
328
+ ? `batch was accepted but did not reach published state within ${timeoutMs / 1000}s.${BATCH_TIMEOUT_HINT}`
329
+ : observedStage === "completed"
330
+ ? "batch completed but was not reported as published"
331
+ : `batch did not reach published completion within ${timeoutMs / 1000}s (last stage: ${observedStage}).${BATCH_TIMEOUT_HINT}`;
332
+
333
+ const remediationPrompt = passed
334
+ ? null
335
+ : staleProjectKeyRemediation(pollResult);
336
+ return {
337
+ id: checkId,
338
+ severity: "error",
339
+ method: "GET",
340
+ url: statusUrl,
341
+ passed,
342
+ status: pollResult?.transportOk ? pollResult.response.status : null,
343
+ error,
344
+ ...(remediationPrompt ? { remediation_prompt: remediationPrompt } : {}),
345
+ observed_batch_id: batchId,
346
+ observed_status: observedStatus,
347
+ observed_stage: observedStage,
348
+ published,
349
+ published_observation_count: publishedObservationCount,
350
+ typed_evidence_counts: typedEvidenceCounts,
351
+ data_usability: dataUsability,
352
+ timeout_ms: timeoutMs,
353
+ };
354
+ };
355
+
356
+ export const hexId = (value, length) => {
357
+ let hex = "";
358
+ for (const char of String(value)) {
359
+ hex += char.charCodeAt(0).toString(16).padStart(2, "0");
360
+ }
361
+ return hex.padEnd(length, "0").slice(0, length).toLowerCase();
362
+ };
363
+
364
+ export const buildSetupCorrelation = () => {
365
+ const id = `cluebase_test_${Date.now()}`;
366
+ const traceId = hexId(`${id}_trace`, 32);
367
+ return {
368
+ id,
369
+ anonymous_id: `anon_${id}`,
370
+ user_id: `user_${id}`,
371
+ organization_id: `org_${id}`,
372
+ session_id: `session_${id}`,
373
+ tab_id: `tab_${id}`,
374
+ interaction_id: `interaction_${id}`,
375
+ request_id: `request_${id}`,
376
+ request_span_id: `request_span_${id}`,
377
+ trace_id: traceId,
378
+ };
379
+ };
380
+
381
+ export const nanosFromNow = (offsetMs = 0) =>
382
+ (BigInt(Date.now() + offsetMs) * 1_000_000n).toString();
383
+
384
+ export const sourceSignalEvent = ({
385
+ attributes,
386
+ context = {},
387
+ endMs = 25,
388
+ events = [],
389
+ name,
390
+ parentSpanId = null,
391
+ resource = {},
392
+ sourceEventId,
393
+ spanId,
394
+ spanKind,
395
+ startMs = 0,
396
+ statusCode = 1,
397
+ surface,
398
+ }) => ({
399
+ source_event_id: sourceEventId,
400
+ source_event_type: "sdk_source_signal",
401
+ source_event_kind: "sdk_source_signal",
402
+ event_name: "sdk_source_signal_observed",
403
+ event_category: "context",
404
+ occurred_at: new Date().toISOString(),
405
+ sdk_signal: {
406
+ surface,
407
+ kind: "span",
408
+ name,
409
+ span_kind: spanKind,
410
+ trace_id: context.trace_id ?? attributes["cluebase.trace_id"] ?? null,
411
+ span_id: spanId,
412
+ parent_span_id: parentSpanId,
413
+ start_time_unix_nano: nanosFromNow(startMs),
414
+ end_time_unix_nano: nanosFromNow(endMs),
415
+ status_code: statusCode,
416
+ status_message: statusCode === 2 ? "ERROR" : null,
417
+ instrumentation_scope_name:
418
+ surface === "backend"
419
+ ? "@cluebase/setup-doctor/backend"
420
+ : "@cluebase/setup-doctor/frontend",
421
+ attributes,
422
+ events,
423
+ resource,
424
+ context,
425
+ },
426
+ });
427
+
428
+ export const browserSignalContext = (correlation) => ({
429
+ producer_id: FRONTEND_SOURCE_IDENTIFIER,
430
+ anonymous_id: correlation.anonymous_id,
431
+ user_id: correlation.user_id,
432
+ organization_id: correlation.organization_id,
433
+ session_id: correlation.session_id,
434
+ tab_id: correlation.tab_id,
435
+ interaction_id: correlation.interaction_id,
436
+ trace_id: correlation.trace_id,
437
+ sdk_collection_mode: "standard",
438
+ frontend_release: CLUEBASE_TEST_SETUP_SDK_VERSION,
439
+ });
440
+
441
+ export const buildBrowserEventPayload = ({ correlation, origin = null }) => {
442
+ const timestamp = new Date().toISOString();
443
+ const id = `${correlation.id}_browser`;
444
+ const pageUrl = origin
445
+ ? `${trimTrailingSlash(origin)}/cluebase/setup-doctor`
446
+ : null;
447
+ const context = browserSignalContext(correlation);
448
+ return {
449
+ batchId: `batch_${id}`,
450
+ idempotencyKey: `idem_${id}`,
451
+ sentAt: timestamp,
452
+ sourceType: "frontend_sdk",
453
+ sourceSchemaVersion: "1",
454
+ producerMetadata: {
455
+ producer_id: FRONTEND_SOURCE_IDENTIFIER,
456
+ sdk_type: "browser",
457
+ sdk_version: CLUEBASE_TEST_SETUP_SDK_VERSION,
458
+ },
459
+ events: [
460
+ sourceSignalEvent({
461
+ sourceEventId: `event_${id}_click`,
462
+ surface: "frontend",
463
+ name: "element_clicked",
464
+ spanKind: "INTERNAL",
465
+ spanId: hexId(`${id}_click`, 16),
466
+ attributes: {
467
+ "cluebase.event_name": "cluebase_test_browser_connectivity",
468
+ "cluebase.event_category": "custom",
469
+ "cluebase.status": "observed",
470
+ "cluebase.event_source": "frontend_sdk",
471
+ "cluebase.trace_id": correlation.trace_id,
472
+ "cluebase.interaction.id": correlation.interaction_id,
473
+ "cluebase.actor.anonymous_id": correlation.anonymous_id,
474
+ "cluebase.actor.user_id": correlation.user_id,
475
+ "cluebase.subject.organization_id": correlation.organization_id,
476
+ "cluebase.session.id": correlation.session_id,
477
+ "cluebase.tab.id": correlation.tab_id,
478
+ ...(pageUrl ? { "cluebase.page.url": pageUrl } : {}),
479
+ "cluebase.raw_event.payload": {
480
+ path: "/cluebase/setup-doctor",
481
+ currentUrl: pageUrl ?? "/cluebase/setup-doctor",
482
+ stableKey: "setup-doctor.browser-connectivity",
483
+ stableKeyQuality: "official",
484
+ event_name: "cluebase_test_browser_connectivity",
485
+ source_event_type: "cluebase_test_browser_connectivity",
486
+ elementTag: "button",
487
+ elementRole: "button",
488
+ clickTargetText: "Cluebase setup doctor",
489
+ primaryActionKey: "cluebase_test_browser_connectivity.clicked",
490
+ },
491
+ },
492
+ context,
493
+ }),
494
+ sourceSignalEvent({
495
+ sourceEventId: `event_${id}_identity`,
496
+ surface: "frontend",
497
+ name: "custom_emitted",
498
+ spanKind: "INTERNAL",
499
+ spanId: hexId(`${id}_identity`, 16),
500
+ startMs: 30,
501
+ endMs: 35,
502
+ attributes: {
503
+ "cluebase.event_source": "frontend_sdk",
504
+ "cluebase.trace_id": correlation.trace_id,
505
+ "cluebase.custom_event_name": "identity_identified",
506
+ "cluebase.custom_event_origin": "official_sdk_lifecycle",
507
+ "cluebase.actor.anonymous_id": correlation.anonymous_id,
508
+ "cluebase.actor.user_id": correlation.user_id,
509
+ "cluebase.subject.organization_id": correlation.organization_id,
510
+ "cluebase.session.id": correlation.session_id,
511
+ "cluebase.tab.id": correlation.tab_id,
512
+ "cluebase.interaction.id": correlation.interaction_id,
513
+ "cluebase.raw_event.payload": {
514
+ userId: correlation.user_id,
515
+ anonymousId: correlation.anonymous_id,
516
+ organizationId: correlation.organization_id,
517
+ },
518
+ },
519
+ context,
520
+ }),
521
+ sourceSignalEvent({
522
+ sourceEventId: `event_${id}_request`,
523
+ surface: "frontend",
524
+ name: "request_finished",
525
+ spanKind: "INTERNAL",
526
+ spanId: hexId(`${id}_request`, 16),
527
+ startMs: 40,
528
+ endMs: 65,
529
+ attributes: {
530
+ component: "fetch",
531
+ "http.method": "GET",
532
+ "http.request.method": "GET",
533
+ "http.route": "/cluebase/setup-doctor/backend-connectivity",
534
+ ...(pageUrl
535
+ ? {
536
+ "http.url": `${trimTrailingSlash(origin)}/cluebase/setup-doctor/backend-connectivity`,
537
+ "url.full": `${trimTrailingSlash(origin)}/cluebase/setup-doctor/backend-connectivity`,
538
+ }
539
+ : {}),
540
+ "http.status_code": 200,
541
+ "http.response.status_code": 200,
542
+ "cluebase.event_category": "request",
543
+ "cluebase.event_name": "request_finished",
544
+ "cluebase.status": "finished",
545
+ "cluebase.event_source": "frontend_sdk",
546
+ "cluebase.trace_id": correlation.trace_id,
547
+ "cluebase.request.id": correlation.request_id,
548
+ "cluebase.request_span_id": correlation.request_span_id,
549
+ "cluebase.interaction.id": correlation.interaction_id,
550
+ "cluebase.interaction.initiator": "human_user",
551
+ "cluebase.interaction.start_event_kind": "element_clicked",
552
+ "cluebase.actor.anonymous_id": correlation.anonymous_id,
553
+ "cluebase.actor.user_id": correlation.user_id,
554
+ "cluebase.subject.organization_id": correlation.organization_id,
555
+ "cluebase.session.id": correlation.session_id,
556
+ "cluebase.tab.id": correlation.tab_id,
557
+ "cluebase.raw_event.payload": {
558
+ path: "/cluebase/setup-doctor",
559
+ currentUrl: pageUrl ?? "/cluebase/setup-doctor",
560
+ method: "GET",
561
+ pathTemplate: "/cluebase/setup-doctor/backend-connectivity",
562
+ statusCode: 200,
563
+ durationMs: 25,
564
+ requestKind: "user_action",
565
+ },
566
+ },
567
+ context: {
568
+ ...context,
569
+ request_id: correlation.request_id,
570
+ request_span_id: correlation.request_span_id,
571
+ },
572
+ }),
573
+ ],
574
+ };
575
+ };
576
+
577
+ export const buildBackendEventPayload = ({
578
+ backendServiceKey,
579
+ correlation,
580
+ projectKey,
581
+ }) => {
582
+ const timestamp = new Date().toISOString();
583
+ const id = `${correlation.id}_backend`;
584
+ const requestSpanId = hexId(`${id}_request_span`, 16);
585
+ const backendContext = {
586
+ producer_id: backendServiceKey,
587
+ anonymous_id: correlation.anonymous_id,
588
+ user_id: correlation.user_id,
589
+ organization_id: correlation.organization_id,
590
+ session_id: correlation.session_id,
591
+ tab_id: correlation.tab_id,
592
+ interaction_id: correlation.interaction_id,
593
+ request_id: correlation.request_id,
594
+ request_span_id: correlation.request_span_id,
595
+ trace_id: correlation.trace_id,
596
+ };
597
+ const resource = {
598
+ "service.name": backendServiceKey,
599
+ "telemetry.sdk.language": "nodejs",
600
+ "service.version": CLUEBASE_TEST_SETUP_SDK_VERSION,
601
+ };
602
+ return {
603
+ projectKey,
604
+ batchId: `batch_${id}`,
605
+ idempotencyKey: `idem_${id}`,
606
+ sentAt: timestamp,
607
+ sourceType: "backend_sdk",
608
+ sourceSchemaVersion: "1",
609
+ producerMetadata: {
610
+ producer_id: backendServiceKey,
611
+ service_name: backendServiceKey,
612
+ service_key: backendServiceKey,
613
+ sdk_type: "nodejs",
614
+ sdk_version: CLUEBASE_TEST_SETUP_SDK_VERSION,
615
+ },
616
+ events: [
617
+ sourceSignalEvent({
618
+ sourceEventId: `event_${id}_request`,
619
+ surface: "backend",
620
+ name: "GET /cluebase/setup-doctor/backend-connectivity",
621
+ spanKind: "SERVER",
622
+ spanId: requestSpanId,
623
+ attributes: {
624
+ "http.method": "GET",
625
+ "http.request.method": "GET",
626
+ "http.route": "/cluebase/setup-doctor/backend-connectivity",
627
+ "url.path": "/cluebase/setup-doctor/backend-connectivity",
628
+ "http.status_code": 200,
629
+ "http.response.status_code": 200,
630
+ "cluebase.event_name": "cluebase_test_backend_connectivity",
631
+ "cluebase.event_source": "backend_sdk",
632
+ "cluebase.trace_id": correlation.trace_id,
633
+ "cluebase.request.id": correlation.request_id,
634
+ "cluebase.request_span_id": correlation.request_span_id,
635
+ "cluebase.interaction.id": correlation.interaction_id,
636
+ "cluebase.actor.anonymous_id": correlation.anonymous_id,
637
+ "cluebase.actor.user_id": correlation.user_id,
638
+ "cluebase.subject.organization_id": correlation.organization_id,
639
+ "cluebase.session.id": correlation.session_id,
640
+ "cluebase.tab.id": correlation.tab_id,
641
+ "cluebase.decision.type": "permission",
642
+ "cluebase.decision.result": "allowed",
643
+ "cluebase.decision.reason_code": "cluebase_test_backend_connectivity_probe",
644
+ "cluebase.raw_event.payload": {
645
+ event_name: "cluebase_test_backend_connectivity",
646
+ pathTemplate: "/cluebase/setup-doctor/backend-connectivity",
647
+ },
648
+ },
649
+ context: backendContext,
650
+ resource,
651
+ }),
652
+ sourceSignalEvent({
653
+ sourceEventId: `event_${id}_data`,
654
+ surface: "backend",
655
+ name: "UPDATE cluebase_test_connectivity",
656
+ spanKind: "INTERNAL",
657
+ spanId: hexId(`${id}_data_span`, 16),
658
+ parentSpanId: requestSpanId,
659
+ startMs: 30,
660
+ endMs: 40,
661
+ attributes: {
662
+ "db.system": "postgresql",
663
+ "db.operation.name": "UPDATE",
664
+ "db.collection.name": "cluebase_test_connectivity",
665
+ "cluebase.data.resource.type": "table",
666
+ "cluebase.data.resource.key": "cluebase_test_connectivity",
667
+ "cluebase.changed_fields_schema": [
668
+ { field_key: "last_checked_at", field_type: "timestamp" },
669
+ ],
670
+ "cluebase.event_source": "backend_sdk",
671
+ "cluebase.trace_id": correlation.trace_id,
672
+ "cluebase.request.id": correlation.request_id,
673
+ "cluebase.request_span_id": correlation.request_span_id,
674
+ "cluebase.interaction.id": correlation.interaction_id,
675
+ "cluebase.actor.anonymous_id": correlation.anonymous_id,
676
+ "cluebase.actor.user_id": correlation.user_id,
677
+ "cluebase.subject.organization_id": correlation.organization_id,
678
+ "cluebase.session.id": correlation.session_id,
679
+ "cluebase.tab.id": correlation.tab_id,
680
+ },
681
+ context: backendContext,
682
+ resource,
683
+ }),
684
+ sourceSignalEvent({
685
+ sourceEventId: `event_${id}_dependency`,
686
+ surface: "backend",
687
+ name: "GET /api/v1/ingest/batch-status/:batchId",
688
+ spanKind: "CLIENT",
689
+ spanId: hexId(`${id}_dependency_span`, 16),
690
+ parentSpanId: requestSpanId,
691
+ startMs: 45,
692
+ endMs: 55,
693
+ attributes: {
694
+ "http.method": "GET",
695
+ "http.request.method": "GET",
696
+ "http.route": "/api/v1/ingest/batch-status/:batchId",
697
+ "url.path": "/api/v1/ingest/batch-status/:batchId",
698
+ "url.full":
699
+ "https://api.cluebase.example/api/v1/ingest/batch-status/test",
700
+ "server.address": "api.cluebase.example",
701
+ "http.status_code": 200,
702
+ "http.response.status_code": 200,
703
+ "cluebase.event_source": "backend_sdk",
704
+ "cluebase.trace_id": correlation.trace_id,
705
+ "cluebase.request.id": correlation.request_id,
706
+ "cluebase.request_span_id": correlation.request_span_id,
707
+ "cluebase.interaction.id": correlation.interaction_id,
708
+ "cluebase.actor.anonymous_id": correlation.anonymous_id,
709
+ "cluebase.actor.user_id": correlation.user_id,
710
+ "cluebase.subject.organization_id": correlation.organization_id,
711
+ "cluebase.session.id": correlation.session_id,
712
+ "cluebase.tab.id": correlation.tab_id,
713
+ },
714
+ context: backendContext,
715
+ resource,
716
+ }),
717
+ ],
718
+ };
719
+ };