@cephalization/phoenix-insight 0.4.0 → 1.0.2

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 (76) hide show
  1. package/README.md +195 -1
  2. package/dist/chunk-KEQDYZIE.js +237 -0
  3. package/dist/chunk-KEQDYZIE.js.map +1 -0
  4. package/dist/cli.d.ts +1 -0
  5. package/dist/cli.js +3950 -642
  6. package/dist/cli.js.map +1 -0
  7. package/dist/index.d.ts +108 -0
  8. package/dist/index.js +13 -1
  9. package/dist/index.js.map +1 -0
  10. package/dist/ui/assets/code-block-F6WJLWQG-BTdTzfvl.js +154 -0
  11. package/dist/ui/assets/code-block-F6WJLWQG-BTdTzfvl.js.map +1 -0
  12. package/dist/ui/assets/index-CX8aDatf.css +1 -0
  13. package/dist/ui/assets/index-DjZuAW6Y.js +63 -0
  14. package/dist/ui/assets/index-DjZuAW6Y.js.map +1 -0
  15. package/dist/ui/assets/vendor-data-r1ZEkUds.js +40 -0
  16. package/dist/ui/assets/vendor-data-r1ZEkUds.js.map +1 -0
  17. package/dist/ui/assets/vendor-react-Cgg2GOmP.js +2 -0
  18. package/dist/ui/assets/vendor-react-Cgg2GOmP.js.map +1 -0
  19. package/dist/ui/assets/vendor-render-DoMl5bum.js +381 -0
  20. package/dist/ui/assets/vendor-render-DoMl5bum.js.map +1 -0
  21. package/dist/ui/assets/vendor-ui-Cg-YC4hK.js +46 -0
  22. package/dist/ui/assets/vendor-ui-Cg-YC4hK.js.map +1 -0
  23. package/dist/ui/index.html +18 -0
  24. package/dist/ui/vite.svg +1 -0
  25. package/package.json +14 -15
  26. package/dist/agent/index.js +0 -230
  27. package/dist/commands/index.js +0 -2
  28. package/dist/commands/px-fetch-more-spans.js +0 -98
  29. package/dist/commands/px-fetch-more-trace.js +0 -110
  30. package/dist/config/index.js +0 -165
  31. package/dist/config/loader.js +0 -141
  32. package/dist/config/schema.js +0 -53
  33. package/dist/modes/index.js +0 -17
  34. package/dist/modes/local.js +0 -134
  35. package/dist/modes/sandbox.js +0 -121
  36. package/dist/modes/types.js +0 -1
  37. package/dist/observability/index.js +0 -65
  38. package/dist/progress.js +0 -209
  39. package/dist/prompts/index.js +0 -1
  40. package/dist/prompts/system.js +0 -30
  41. package/dist/snapshot/client.js +0 -74
  42. package/dist/snapshot/context.js +0 -441
  43. package/dist/snapshot/datasets.js +0 -68
  44. package/dist/snapshot/experiments.js +0 -135
  45. package/dist/snapshot/index.js +0 -262
  46. package/dist/snapshot/projects.js +0 -44
  47. package/dist/snapshot/prompts.js +0 -199
  48. package/dist/snapshot/spans.js +0 -104
  49. package/dist/snapshot/utils.js +0 -112
  50. package/dist/tsconfig.esm.tsbuildinfo +0 -1
  51. package/src/agent/index.ts +0 -323
  52. package/src/cli.ts +0 -854
  53. package/src/commands/index.ts +0 -8
  54. package/src/commands/px-fetch-more-spans.ts +0 -174
  55. package/src/commands/px-fetch-more-trace.ts +0 -183
  56. package/src/config/index.ts +0 -225
  57. package/src/config/loader.ts +0 -173
  58. package/src/config/schema.ts +0 -66
  59. package/src/index.ts +0 -1
  60. package/src/modes/index.ts +0 -21
  61. package/src/modes/local.ts +0 -163
  62. package/src/modes/sandbox.ts +0 -144
  63. package/src/modes/types.ts +0 -31
  64. package/src/observability/index.ts +0 -90
  65. package/src/progress.ts +0 -239
  66. package/src/prompts/index.ts +0 -1
  67. package/src/prompts/system.ts +0 -31
  68. package/src/snapshot/client.ts +0 -129
  69. package/src/snapshot/context.ts +0 -587
  70. package/src/snapshot/datasets.ts +0 -132
  71. package/src/snapshot/experiments.ts +0 -246
  72. package/src/snapshot/index.ts +0 -403
  73. package/src/snapshot/projects.ts +0 -58
  74. package/src/snapshot/prompts.ts +0 -267
  75. package/src/snapshot/spans.ts +0 -163
  76. package/src/snapshot/utils.ts +0 -140
@@ -0,0 +1,18 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="./vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>ui</title>
8
+ <script type="module" crossorigin src="./assets/index-DjZuAW6Y.js"></script>
9
+ <link rel="modulepreload" crossorigin href="./assets/vendor-react-Cgg2GOmP.js">
10
+ <link rel="modulepreload" crossorigin href="./assets/vendor-ui-Cg-YC4hK.js">
11
+ <link rel="modulepreload" crossorigin href="./assets/vendor-data-r1ZEkUds.js">
12
+ <link rel="modulepreload" crossorigin href="./assets/vendor-render-DoMl5bum.js">
13
+ <link rel="stylesheet" crossorigin href="./assets/index-CX8aDatf.css">
14
+ </head>
15
+ <body>
16
+ <div id="root"></div>
17
+ </body>
18
+ </html>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cephalization/phoenix-insight",
3
- "version": "0.4.0",
3
+ "version": "1.0.2",
4
4
  "description": "A CLI for Arize AI Phoenix data analysis with AI agents",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,9 +17,7 @@
17
17
  "access": "public"
18
18
  },
19
19
  "files": [
20
- "dist",
21
- "src",
22
- "package.json"
20
+ "dist"
23
21
  ],
24
22
  "keywords": [
25
23
  "phoenix",
@@ -29,12 +27,13 @@
29
27
  "llmops",
30
28
  "ai"
31
29
  ],
32
- "author": "oss@arize.com",
30
+ "author": "Tony Powell <powell.anthonyd@proton.me>",
33
31
  "license": "Apache-2.0",
34
- "homepage": "https://github.com/cephalization/phoenix-insight#readme",
32
+ "homepage": "https://github.com/cephalization/phoenix-insight/tree/main/packages/cli#readme",
35
33
  "repository": {
36
34
  "type": "git",
37
- "url": "git+https://github.com/cephalization/phoenix-insight.git"
35
+ "url": "git+https://github.com/cephalization/phoenix-insight.git",
36
+ "directory": "packages/cli"
38
37
  },
39
38
  "bugs": {
40
39
  "url": "https://github.com/cephalization/phoenix-insight/issues"
@@ -49,26 +48,26 @@
49
48
  "commander": "^12.0.0",
50
49
  "just-bash": "^2.2.0",
51
50
  "ora": "^9.0.0",
51
+ "ws": "^8.19.0",
52
52
  "zod": "^4.3.5"
53
53
  },
54
54
  "devDependencies": {
55
- "@changesets/cli": "^2.29.8",
56
55
  "@faker-js/faker": "^10.2.0",
57
- "@types/node": "^18.19.0",
56
+ "@types/ws": "^8.18.1",
58
57
  "msw": "^2.12.7",
59
- "rimraf": "^5.0.10",
60
58
  "tsc-alias": "^1.8.11",
61
- "tsx": "^4.21.0",
62
- "typescript": "^5.8.2",
63
- "vitest": "^2.1.9"
59
+ "tsup": "^8.5.1",
60
+ "@cephalization/phoenix-insight-ui": "1.0.0"
64
61
  },
65
62
  "engines": {
66
63
  "node": ">=18"
67
64
  },
68
65
  "scripts": {
69
66
  "clean": "rimraf dist tsconfig.esm.tsbuildinfo tsconfig.tsbuildinfo",
70
- "prebuild": "pnpm run clean",
71
- "build": "tsc --build tsconfig.esm.json",
67
+ "prebuild": "pnpm run clean && pnpm run verify-ui-dist",
68
+ "verify-ui-dist": "node -e \"const fs = require('fs'); const path = require('path'); const uiDist = path.resolve(__dirname, '../ui/dist/index.html'); if (!fs.existsSync(uiDist)) { console.error('ERROR: UI dist not found. Build UI package first with: pnpm --filter @cephalization/phoenix-insight-ui build'); process.exit(1); } console.log('UI dist verified:', uiDist);\"",
69
+ "build": "tsup && pnpm run copy-ui-dist",
70
+ "copy-ui-dist": "node -e \"const fs = require('fs'); const path = require('path'); const src = path.resolve(__dirname, '../ui/dist'); const dest = path.resolve(__dirname, 'dist/ui'); fs.cpSync(src, dest, { recursive: true }); console.log('UI dist copied to:', dest);\"",
72
71
  "start": "node dist/cli.js",
73
72
  "dev": "tsx src/cli.ts",
74
73
  "typecheck": "tsc --noEmit",
@@ -1,230 +0,0 @@
1
- /**
2
- * Phoenix Insight AI agent setup using Vercel AI SDK
3
- */
4
- import { generateText, streamText, tool, stepCountIs, } from "ai";
5
- import { anthropic } from "@ai-sdk/anthropic";
6
- import { z } from "zod";
7
- import { INSIGHT_SYSTEM_PROMPT } from "../prompts/system.js";
8
- import { fetchMoreSpans, fetchMoreTrace, } from "../commands/index.js";
9
- import { PhoenixClientError } from "../snapshot/client.js";
10
- /**
11
- * Phoenix Insight Agent
12
- */
13
- export class PhoenixInsightAgent {
14
- mode;
15
- client;
16
- maxSteps;
17
- tools = null;
18
- model = anthropic("claude-sonnet-4-5");
19
- constructor(config) {
20
- this.mode = config.mode;
21
- this.client = config.client;
22
- this.maxSteps = config.maxSteps || 25;
23
- }
24
- /**
25
- * Initialize the agent tools
26
- */
27
- async initializeTools() {
28
- if (this.tools)
29
- return this.tools;
30
- // Get the bash tool from the execution mode
31
- const bashTool = await this.mode.getBashTool();
32
- // Store references in closure for the custom tools
33
- const client = this.client;
34
- const mode = this.mode;
35
- // Create custom px-fetch-more-spans tool using AI SDK's tool function
36
- const pxFetchMoreSpans = tool({
37
- description: "Fetch additional spans from Phoenix. Use when you need more span data than what's in the snapshot. You must provide both project name and optionally a limit.",
38
- inputSchema: z.object({
39
- project: z.string().describe("The project name"),
40
- limit: z
41
- .number()
42
- .optional()
43
- .describe("Number of spans to fetch (default: 500)"),
44
- startTime: z
45
- .string()
46
- .optional()
47
- .describe("Start time filter in ISO format"),
48
- endTime: z
49
- .string()
50
- .optional()
51
- .describe("End time filter in ISO format"),
52
- }),
53
- execute: async (params) => {
54
- try {
55
- const options = {
56
- project: params.project,
57
- limit: params.limit || 500,
58
- startTime: params.startTime,
59
- endTime: params.endTime,
60
- };
61
- await fetchMoreSpans(client, mode, options);
62
- return {
63
- success: true,
64
- message: `Fetched additional spans for project ${params.project}. Data saved to /phoenix/projects/${params.project}/spans/`,
65
- };
66
- }
67
- catch (error) {
68
- return {
69
- success: false,
70
- error: error instanceof Error ? error.message : String(error),
71
- };
72
- }
73
- },
74
- });
75
- // Create custom px-fetch-more-trace tool using AI SDK's tool function
76
- const pxFetchMoreTrace = tool({
77
- description: "Fetch a specific trace by ID from Phoenix. Use when you need to examine a particular trace in detail. You must provide both the trace ID and the project name.",
78
- inputSchema: z.object({
79
- traceId: z.string().describe("The trace ID to fetch"),
80
- project: z.string().describe("The project name to search in"),
81
- }),
82
- execute: async (params) => {
83
- try {
84
- const options = {
85
- traceId: params.traceId,
86
- project: params.project,
87
- };
88
- await fetchMoreTrace(client, mode, options);
89
- return {
90
- success: true,
91
- message: `Fetched trace ${params.traceId}. Data saved to /phoenix/traces/${params.traceId}/`,
92
- };
93
- }
94
- catch (error) {
95
- return {
96
- success: false,
97
- error: error instanceof Error ? error.message : String(error),
98
- };
99
- }
100
- },
101
- });
102
- this.tools = {
103
- bash: bashTool,
104
- px_fetch_more_spans: pxFetchMoreSpans,
105
- px_fetch_more_trace: pxFetchMoreTrace,
106
- };
107
- return this.tools;
108
- }
109
- /**
110
- * Generate a response for a user query
111
- */
112
- async generate(userQuery, options) {
113
- let tools;
114
- try {
115
- tools = await this.initializeTools();
116
- }
117
- catch (error) {
118
- throw new Error(`Failed to initialize agent tools: ${error instanceof Error ? error.message : String(error)}`);
119
- }
120
- try {
121
- const result = await generateText({
122
- model: this.model,
123
- system: INSIGHT_SYSTEM_PROMPT,
124
- prompt: userQuery,
125
- tools,
126
- stopWhen: stepCountIs(this.maxSteps),
127
- onStepFinish: options?.onStepFinish,
128
- experimental_telemetry: {
129
- isEnabled: true,
130
- },
131
- });
132
- return result;
133
- }
134
- catch (error) {
135
- // Check for specific AI SDK errors
136
- if (error instanceof Error) {
137
- if (error.message.includes("rate limit")) {
138
- throw new Error("AI model rate limit exceeded. Please wait and try again.");
139
- }
140
- if (error.message.includes("timeout")) {
141
- throw new Error("AI model request timed out. Please try again.");
142
- }
143
- if (error.message.includes("authentication") ||
144
- error.message.includes("API key")) {
145
- throw new Error("AI model authentication failed. Check your API key configuration.");
146
- }
147
- }
148
- throw new Error(`AI generation failed: ${error instanceof Error ? error.message : String(error)}`);
149
- }
150
- }
151
- /**
152
- * Stream a response for a user query
153
- */
154
- async stream(userQuery, options) {
155
- let tools;
156
- try {
157
- tools = await this.initializeTools();
158
- }
159
- catch (error) {
160
- throw new Error(`Failed to initialize agent tools: ${error instanceof Error ? error.message : String(error)}`);
161
- }
162
- try {
163
- const result = streamText({
164
- model: this.model,
165
- system: INSIGHT_SYSTEM_PROMPT,
166
- prompt: userQuery,
167
- tools,
168
- stopWhen: stepCountIs(this.maxSteps),
169
- onStepFinish: options?.onStepFinish,
170
- experimental_telemetry: {
171
- isEnabled: true,
172
- },
173
- });
174
- return result;
175
- }
176
- catch (error) {
177
- // Check for specific AI SDK errors
178
- if (error instanceof Error) {
179
- if (error.message.includes("rate limit")) {
180
- throw new Error("AI model rate limit exceeded. Please wait and try again.");
181
- }
182
- if (error.message.includes("timeout")) {
183
- throw new Error("AI model request timed out. Please try again.");
184
- }
185
- if (error.message.includes("authentication") ||
186
- error.message.includes("API key")) {
187
- throw new Error("AI model authentication failed. Check your API key configuration.");
188
- }
189
- }
190
- throw new Error(`AI streaming failed: ${error instanceof Error ? error.message : String(error)}`);
191
- }
192
- }
193
- /**
194
- * Clean up resources
195
- */
196
- async cleanup() {
197
- await this.mode.cleanup();
198
- }
199
- }
200
- /**
201
- * Creates a Phoenix Insight agent
202
- */
203
- export async function createInsightAgent(config) {
204
- return new PhoenixInsightAgent(config);
205
- }
206
- /**
207
- * Run a query with the Phoenix Insight agent
208
- */
209
- export async function runQuery(agent, userQuery, options) {
210
- const { stream = false, ...callbacks } = options || {};
211
- if (stream) {
212
- return await agent.stream(userQuery, callbacks);
213
- }
214
- else {
215
- return await agent.generate(userQuery, callbacks);
216
- }
217
- }
218
- /**
219
- * Create and run a one-shot query
220
- */
221
- export async function runOneShotQuery(config, userQuery, options) {
222
- const agent = await createInsightAgent(config);
223
- try {
224
- const result = await runQuery(agent, userQuery, options);
225
- return result;
226
- }
227
- finally {
228
- await agent.cleanup();
229
- }
230
- }
@@ -1,2 +0,0 @@
1
- export { fetchMoreSpans, } from "./px-fetch-more-spans.js";
2
- export { fetchMoreTrace, } from "./px-fetch-more-trace.js";
@@ -1,98 +0,0 @@
1
- import { withErrorHandling } from "../snapshot/client.js";
2
- /**
3
- * Fetches additional spans for a specific project on-demand
4
- *
5
- * @param client - Phoenix client instance
6
- * @param mode - Execution mode for file operations
7
- * @param options - Options for fetching spans
8
- */
9
- export async function fetchMoreSpans(client, mode, options) {
10
- const { project, limit, startTime, endTime } = options;
11
- await withErrorHandling(async () => {
12
- // Try to read existing metadata to get the last cursor
13
- let existingMetadata = null;
14
- let existingSpans = [];
15
- try {
16
- const metadataResult = await mode.exec(`cat /phoenix/projects/${project}/spans/metadata.json`);
17
- if (metadataResult.exitCode === 0 && metadataResult.stdout) {
18
- existingMetadata = JSON.parse(metadataResult.stdout);
19
- }
20
- }
21
- catch (error) {
22
- // Metadata doesn't exist, that's okay
23
- }
24
- // Try to read existing spans
25
- try {
26
- const spansResult = await mode.exec(`cat /phoenix/projects/${project}/spans/index.jsonl`);
27
- if (spansResult.exitCode === 0 && spansResult.stdout) {
28
- existingSpans = spansResult.stdout
29
- .trim()
30
- .split("\n")
31
- .filter((line) => line.length > 0)
32
- .map((line) => JSON.parse(line));
33
- }
34
- }
35
- catch (error) {
36
- // Spans file doesn't exist, that's okay
37
- }
38
- // Fetch new spans
39
- const newSpans = [];
40
- let cursor = existingMetadata?.lastCursor ?? null;
41
- let totalFetched = 0;
42
- while (totalFetched < limit) {
43
- const query = {
44
- limit: Math.min(100, limit - totalFetched), // Fetch in chunks of 100
45
- };
46
- if (cursor) {
47
- query.cursor = cursor;
48
- }
49
- if (startTime) {
50
- query.start_time =
51
- startTime instanceof Date ? startTime.toISOString() : startTime;
52
- }
53
- if (endTime) {
54
- query.end_time =
55
- endTime instanceof Date ? endTime.toISOString() : endTime;
56
- }
57
- const response = await client.GET("/v1/projects/{project_identifier}/spans", {
58
- params: {
59
- path: {
60
- project_identifier: project,
61
- },
62
- query,
63
- },
64
- });
65
- if (response.error)
66
- throw response.error;
67
- const data = response.data?.data ?? [];
68
- newSpans.push(...data);
69
- totalFetched += data.length;
70
- cursor = response.data?.next_cursor ?? null;
71
- // Stop if there's no more data
72
- if (!cursor || data.length === 0) {
73
- break;
74
- }
75
- }
76
- // Combine existing and new spans
77
- const allSpans = [...existingSpans, ...newSpans];
78
- // Write updated spans to JSONL file
79
- const jsonlContent = allSpans
80
- .map((span) => JSON.stringify(span))
81
- .join("\n");
82
- await mode.writeFile(`/phoenix/projects/${project}/spans/index.jsonl`, jsonlContent);
83
- // Update metadata
84
- const metadata = {
85
- project,
86
- spanCount: allSpans.length,
87
- startTime: startTime instanceof Date
88
- ? startTime.toISOString()
89
- : (startTime ?? null),
90
- endTime: endTime instanceof Date ? endTime.toISOString() : (endTime ?? null),
91
- snapshotTime: new Date().toISOString(),
92
- lastCursor: cursor,
93
- };
94
- await mode.writeFile(`/phoenix/projects/${project}/spans/metadata.json`, JSON.stringify(metadata, null, 2));
95
- console.log(`Fetched ${newSpans.length} additional spans for project "${project}"`);
96
- console.log(`Total spans for project: ${allSpans.length}`);
97
- }, `fetching more spans for project ${project}`);
98
- }
@@ -1,110 +0,0 @@
1
- import { withErrorHandling } from "../snapshot/client.js";
2
- /**
3
- * Fetches all spans for a specific trace ID
4
- *
5
- * @param client - Phoenix client instance
6
- * @param mode - Execution mode for file operations
7
- * @param options - Options for fetching trace
8
- */
9
- export async function fetchMoreTrace(client, mode, options) {
10
- const { traceId, project } = options;
11
- await withErrorHandling(async () => {
12
- // First, check if the project exists in our snapshot
13
- const projectsResult = await mode.exec("cat /phoenix/projects/index.jsonl");
14
- if (projectsResult.exitCode !== 0 || !projectsResult.stdout) {
15
- console.error("No projects found in snapshot. Run a snapshot first.");
16
- return;
17
- }
18
- const projectNames = projectsResult.stdout
19
- .trim()
20
- .split("\n")
21
- .filter((line) => line.length > 0)
22
- .map((line) => JSON.parse(line).name);
23
- if (!projectNames.includes(project)) {
24
- console.error(`Project "${project}" not found. Available projects: ${projectNames.join(", ")}`);
25
- return;
26
- }
27
- // Fetch all spans that belong to this trace
28
- const traceSpans = [];
29
- let cursor = null;
30
- let totalFetched = 0;
31
- console.log(`Fetching trace ${traceId} from project "${project}"...`);
32
- // We need to fetch spans in batches and filter by trace_id
33
- // Since the API doesn't support direct trace_id filtering
34
- while (true) {
35
- const query = {
36
- limit: 100, // Fetch in chunks
37
- };
38
- if (cursor) {
39
- query.cursor = cursor;
40
- }
41
- const response = await client.GET("/v1/projects/{project_identifier}/spans", {
42
- params: {
43
- path: {
44
- project_identifier: project,
45
- },
46
- query,
47
- },
48
- });
49
- if (response.error)
50
- throw response.error;
51
- const data = response.data?.data ?? [];
52
- totalFetched += data.length;
53
- // Filter spans that belong to our trace
54
- const matchingSpans = data.filter((span) => span.context.trace_id === traceId);
55
- traceSpans.push(...matchingSpans);
56
- cursor = response.data?.next_cursor ?? null;
57
- // Stop if we found spans for the trace or no more data
58
- if (traceSpans.length > 0 || !cursor || data.length === 0) {
59
- // If we found some spans, continue until we have all spans from the trace
60
- // This is because trace spans might be spread across multiple pages
61
- if (traceSpans.length > 0 && cursor && data.length > 0) {
62
- console.log(`Found ${traceSpans.length} spans so far, continuing search...`);
63
- continue;
64
- }
65
- break;
66
- }
67
- // Show progress for large datasets
68
- if (totalFetched % 1000 === 0) {
69
- console.log(`Searched ${totalFetched} spans so far...`);
70
- }
71
- }
72
- if (traceSpans.length === 0) {
73
- console.log(`No spans found for trace ${traceId} in project "${project}"`);
74
- console.log(`Searched through ${totalFetched} spans. The trace might not exist or might be in a different project.`);
75
- return;
76
- }
77
- // Sort spans by start_time to show them in order
78
- traceSpans.sort((a, b) => new Date(a.start_time).getTime() - new Date(b.start_time).getTime());
79
- // Write trace spans to a dedicated file
80
- const jsonlContent = traceSpans
81
- .map((span) => JSON.stringify(span))
82
- .join("\n");
83
- const traceDir = `/phoenix/traces/${traceId}`;
84
- await mode.writeFile(`${traceDir}/spans.jsonl`, jsonlContent);
85
- // Create trace metadata
86
- const rootSpan = traceSpans.find((span) => !span.parent_id);
87
- const firstSpan = traceSpans[0];
88
- const lastSpan = traceSpans[traceSpans.length - 1];
89
- const metadata = {
90
- traceId,
91
- project,
92
- spanCount: traceSpans.length,
93
- rootSpan: rootSpan ? { id: rootSpan.id, name: rootSpan.name } : null,
94
- startTime: firstSpan?.start_time || null,
95
- endTime: lastSpan?.end_time || null,
96
- duration: firstSpan && lastSpan
97
- ? new Date(lastSpan.end_time).getTime() -
98
- new Date(firstSpan.start_time).getTime()
99
- : 0,
100
- snapshotTime: new Date().toISOString(),
101
- };
102
- await mode.writeFile(`${traceDir}/metadata.json`, JSON.stringify(metadata, null, 2));
103
- console.log(`\nSuccessfully fetched trace ${traceId}:`);
104
- console.log(`- Project: ${project}`);
105
- console.log(`- Spans: ${traceSpans.length}`);
106
- console.log(`- Root span: ${rootSpan?.name || "Unknown"}`);
107
- console.log(`- Duration: ${(metadata.duration / 1000).toFixed(2)} seconds`);
108
- console.log(`\nTrace data saved to: ${traceDir}/`);
109
- }, `fetching trace ${traceId}`);
110
- }