newpr 1.0.2 → 1.0.4
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/package.json +1 -1
- package/src/stack/publish.ts +69 -14
- package/src/telemetry/index.ts +115 -0
- package/src/web/client/App.tsx +1 -0
- package/src/web/client/components/AnalyticsConsent.tsx +10 -0
- package/src/web/client/components/ResultsScreen.tsx +3 -1
- package/src/web/client/components/SettingsPanel.tsx +5 -0
- package/src/web/client/hooks/useStack.ts +25 -3
- package/src/web/client/panels/StackPanel.tsx +7 -2
- package/src/web/server/routes.ts +9 -0
- package/src/web/server/session-manager.ts +7 -0
- package/src/web/server.ts +3 -0
package/package.json
CHANGED
package/src/stack/publish.ts
CHANGED
|
@@ -46,9 +46,9 @@ export async function publishStack(input: PublishInput): Promise<StackPublishRes
|
|
|
46
46
|
? `[${order}/${total}] ${gc.pr_title}`
|
|
47
47
|
: `[Stack ${order}/${total}] ${gc.group_id}`;
|
|
48
48
|
|
|
49
|
-
const
|
|
49
|
+
const placeholder = buildPlaceholderBody(gc.group_id, order, total, pr_meta);
|
|
50
50
|
|
|
51
|
-
const prResult = await Bun.$`gh pr create --repo ${ghRepo} --base ${prBase} --head ${gc.branch_name} --title ${title} --body ${
|
|
51
|
+
const prResult = await Bun.$`gh pr create --repo ${ghRepo} --base ${prBase} --head ${gc.branch_name} --title ${title} --body ${placeholder} --draft`.quiet().nothrow();
|
|
52
52
|
|
|
53
53
|
if (prResult.exitCode === 0) {
|
|
54
54
|
const prUrl = prResult.stdout.toString().trim();
|
|
@@ -68,29 +68,84 @@ export async function publishStack(input: PublishInput): Promise<StackPublishRes
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
await updatePrBodies(ghRepo, prs, pr_meta);
|
|
72
|
+
|
|
71
73
|
return { branches, prs };
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
function
|
|
76
|
+
async function updatePrBodies(ghRepo: string, prs: PrInfo[], prMeta: PrMeta): Promise<void> {
|
|
77
|
+
if (prs.length === 0) return;
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < prs.length; i++) {
|
|
80
|
+
const pr = prs[i]!;
|
|
81
|
+
const body = buildFullBody(pr, i, prs, prMeta);
|
|
82
|
+
|
|
83
|
+
await Bun.$`gh pr edit ${pr.number} --repo ${ghRepo} --body ${body}`.quiet().nothrow();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildPlaceholderBody(
|
|
75
88
|
groupId: string,
|
|
76
89
|
order: number,
|
|
77
90
|
total: number,
|
|
78
|
-
_execResult: StackExecResult,
|
|
79
91
|
prMeta: PrMeta,
|
|
80
92
|
): string {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const lines = [
|
|
85
|
-
`> This is part of a stacked PR chain created by [newpr](${prMeta.pr_url})`,
|
|
86
|
-
`>`,
|
|
87
|
-
`> **Stack order**: ${order}/${total}`,
|
|
88
|
-
`> **${prevPr}** | **${nextPr}**`,
|
|
93
|
+
return [
|
|
94
|
+
`> This is part of a stacked PR chain created by [newpr](https://github.com/jiwonMe/newpr).`,
|
|
95
|
+
`> Stack order: ${order}/${total} — body will be updated with links shortly.`,
|
|
89
96
|
``,
|
|
90
97
|
`## ${groupId}`,
|
|
91
98
|
``,
|
|
92
99
|
`*From PR #${prMeta.pr_number}: ${prMeta.pr_title}*`,
|
|
93
|
-
];
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildFullBody(
|
|
104
|
+
current: PrInfo,
|
|
105
|
+
index: number,
|
|
106
|
+
allPrs: PrInfo[],
|
|
107
|
+
prMeta: PrMeta,
|
|
108
|
+
): string {
|
|
109
|
+
const total = allPrs.length;
|
|
110
|
+
const order = index + 1;
|
|
111
|
+
|
|
112
|
+
const stackTable = allPrs.map((pr, i) => {
|
|
113
|
+
const num = i + 1;
|
|
114
|
+
const isCurrent = i === index;
|
|
115
|
+
const marker = isCurrent ? "👉" : statusEmoji(i, index);
|
|
116
|
+
const link = `[#${pr.number}](${pr.url})`;
|
|
117
|
+
const titleText = pr.title.replace(/^\[\d+\/\d+\]\s*/, "");
|
|
118
|
+
return `| ${marker} | ${num}/${total} | ${link} | ${titleText} |`;
|
|
119
|
+
}).join("\n");
|
|
120
|
+
|
|
121
|
+
const prev = index > 0
|
|
122
|
+
? `⬅️ Previous: [#${allPrs[index - 1]!.number}](${allPrs[index - 1]!.url})`
|
|
123
|
+
: "⬅️ Previous: base branch";
|
|
124
|
+
const next = index < total - 1
|
|
125
|
+
? `➡️ Next: [#${allPrs[index + 1]!.number}](${allPrs[index + 1]!.url})`
|
|
126
|
+
: "➡️ Next: top of stack";
|
|
127
|
+
|
|
128
|
+
return [
|
|
129
|
+
`> **Stack ${order}/${total}** — This PR is part of a stacked PR chain created by [newpr](https://github.com/jiwonMe/newpr).`,
|
|
130
|
+
`> Source: #${prMeta.pr_number} ${prMeta.pr_title}`,
|
|
131
|
+
``,
|
|
132
|
+
`### 📚 Stack Navigation`,
|
|
133
|
+
``,
|
|
134
|
+
`| | Order | PR | Title |`,
|
|
135
|
+
`|---|---|---|---|`,
|
|
136
|
+
stackTable,
|
|
137
|
+
``,
|
|
138
|
+
`${prev} | ${next}`,
|
|
139
|
+
``,
|
|
140
|
+
`---`,
|
|
141
|
+
``,
|
|
142
|
+
`## ${current.group_id}`,
|
|
143
|
+
``,
|
|
144
|
+
`*From PR [#${prMeta.pr_number}](${prMeta.pr_url}): ${prMeta.pr_title}*`,
|
|
145
|
+
].join("\n");
|
|
146
|
+
}
|
|
94
147
|
|
|
95
|
-
|
|
148
|
+
function statusEmoji(prIndex: number, currentIndex: number): string {
|
|
149
|
+
if (prIndex < currentIndex) return "✅";
|
|
150
|
+
return "⬜";
|
|
96
151
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { mkdirSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
const GA_ID = "G-L3SL6T6JQ1";
|
|
7
|
+
const GA_SECRET = "Sier1nbXS2-eX2TR3j1kZQ";
|
|
8
|
+
const MP_ENDPOINT = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_ID}&api_secret=${GA_SECRET}`;
|
|
9
|
+
|
|
10
|
+
const CONFIG_DIR = join(homedir(), ".newpr");
|
|
11
|
+
const TELEMETRY_FILE = join(CONFIG_DIR, "telemetry.json");
|
|
12
|
+
|
|
13
|
+
interface TelemetryConfig {
|
|
14
|
+
client_id: string;
|
|
15
|
+
consent: "granted" | "denied" | "pending";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let config: TelemetryConfig | null = null;
|
|
19
|
+
|
|
20
|
+
function ensureDir(): void {
|
|
21
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function loadConfig(): Promise<TelemetryConfig> {
|
|
25
|
+
if (config) return config;
|
|
26
|
+
try {
|
|
27
|
+
const file = Bun.file(TELEMETRY_FILE);
|
|
28
|
+
if (await file.exists()) {
|
|
29
|
+
const data = JSON.parse(await file.text()) as Partial<TelemetryConfig>;
|
|
30
|
+
config = {
|
|
31
|
+
client_id: data.client_id || randomUUID(),
|
|
32
|
+
consent: data.consent === "granted" || data.consent === "denied" ? data.consent : "pending",
|
|
33
|
+
};
|
|
34
|
+
} else {
|
|
35
|
+
config = { client_id: randomUUID(), consent: "pending" };
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
config = { client_id: randomUUID(), consent: "pending" };
|
|
39
|
+
}
|
|
40
|
+
await saveConfig();
|
|
41
|
+
return config;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function saveConfig(): Promise<void> {
|
|
45
|
+
if (!config) return;
|
|
46
|
+
ensureDir();
|
|
47
|
+
await Bun.write(TELEMETRY_FILE, `${JSON.stringify(config, null, 2)}\n`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getTelemetryConsent(): Promise<"granted" | "denied" | "pending"> {
|
|
51
|
+
const cfg = await loadConfig();
|
|
52
|
+
return cfg.consent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function setTelemetryConsent(consent: "granted" | "denied"): Promise<void> {
|
|
56
|
+
const cfg = await loadConfig();
|
|
57
|
+
cfg.consent = consent;
|
|
58
|
+
await saveConfig();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function sendEvents(events: Array<{ name: string; params?: Record<string, string | number | boolean> }>): Promise<void> {
|
|
62
|
+
const cfg = await loadConfig();
|
|
63
|
+
if (cfg.consent !== "granted") return;
|
|
64
|
+
|
|
65
|
+
const body = {
|
|
66
|
+
client_id: cfg.client_id,
|
|
67
|
+
events,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await fetch(MP_ENDPOINT, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify(body),
|
|
75
|
+
});
|
|
76
|
+
} catch {
|
|
77
|
+
// Fire-and-forget — network failures are silent
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function track(name: string, params?: Record<string, string | number | boolean>): void {
|
|
82
|
+
sendEvents([{ name, params }]);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const telemetry = {
|
|
86
|
+
serverStarted: (version: string) =>
|
|
87
|
+
track("server_started", { version, platform: process.platform, arch: process.arch }),
|
|
88
|
+
|
|
89
|
+
analysisStarted: (fileCount: number) =>
|
|
90
|
+
track("analysis_started", { file_count: fileCount }),
|
|
91
|
+
|
|
92
|
+
analysisCompleted: (fileCount: number, durationSec: number) =>
|
|
93
|
+
track("analysis_completed", { file_count: fileCount, duration_sec: durationSec }),
|
|
94
|
+
|
|
95
|
+
analysisError: (errorType: string) =>
|
|
96
|
+
track("analysis_error", { error_type: errorType.slice(0, 100) }),
|
|
97
|
+
|
|
98
|
+
chatSent: () =>
|
|
99
|
+
track("chat_sent"),
|
|
100
|
+
|
|
101
|
+
chatCompleted: (durationSec: number, hasTools: boolean) =>
|
|
102
|
+
track("chat_completed", { duration_sec: durationSec, has_tools: hasTools }),
|
|
103
|
+
|
|
104
|
+
reviewSubmitted: (event: string) =>
|
|
105
|
+
track("review_submitted", { review_event: event }),
|
|
106
|
+
|
|
107
|
+
stackStarted: () =>
|
|
108
|
+
track("stack_started"),
|
|
109
|
+
|
|
110
|
+
stackCompleted: (groupCount: number) =>
|
|
111
|
+
track("stack_completed", { group_count: groupCount }),
|
|
112
|
+
|
|
113
|
+
stackPublished: (prCount: number) =>
|
|
114
|
+
track("stack_published", { pr_count: prCount }),
|
|
115
|
+
};
|
package/src/web/client/App.tsx
CHANGED
|
@@ -202,6 +202,7 @@ export function App() {
|
|
|
202
202
|
onTabChange={handleTabChange}
|
|
203
203
|
onReanalyze={(prUrl: string) => { analysis.start(prUrl); }}
|
|
204
204
|
enabledPlugins={features.enabledPlugins}
|
|
205
|
+
onTrackAnalysis={bgAnalyses.track}
|
|
205
206
|
/>
|
|
206
207
|
)}
|
|
207
208
|
{analysis.phase === "error" && (
|
|
@@ -7,13 +7,23 @@ export function AnalyticsConsent({ onDone }: { onDone: () => void }) {
|
|
|
7
7
|
|
|
8
8
|
if (state !== "pending") return null;
|
|
9
9
|
|
|
10
|
+
const syncServer = (consent: "granted" | "denied") => {
|
|
11
|
+
fetch("/api/config", {
|
|
12
|
+
method: "PUT",
|
|
13
|
+
headers: { "Content-Type": "application/json" },
|
|
14
|
+
body: JSON.stringify({ telemetry_consent: consent }),
|
|
15
|
+
}).catch(() => {});
|
|
16
|
+
};
|
|
17
|
+
|
|
10
18
|
const handleAccept = () => {
|
|
11
19
|
setConsent("granted");
|
|
20
|
+
syncServer("granted");
|
|
12
21
|
onDone();
|
|
13
22
|
};
|
|
14
23
|
|
|
15
24
|
const handleDecline = () => {
|
|
16
25
|
setConsent("denied");
|
|
26
|
+
syncServer("denied");
|
|
17
27
|
onDone();
|
|
18
28
|
};
|
|
19
29
|
|
|
@@ -51,6 +51,7 @@ export function ResultsScreen({
|
|
|
51
51
|
onTabChange,
|
|
52
52
|
onReanalyze,
|
|
53
53
|
enabledPlugins,
|
|
54
|
+
onTrackAnalysis,
|
|
54
55
|
}: {
|
|
55
56
|
data: NewprOutput;
|
|
56
57
|
onBack: () => void;
|
|
@@ -61,6 +62,7 @@ export function ResultsScreen({
|
|
|
61
62
|
onTabChange?: (tab: string) => void;
|
|
62
63
|
onReanalyze?: (prUrl: string) => void;
|
|
63
64
|
enabledPlugins?: string[];
|
|
65
|
+
onTrackAnalysis?: (analysisSessionId: string, prUrl: string) => void;
|
|
64
66
|
}) {
|
|
65
67
|
const { meta, summary } = data;
|
|
66
68
|
const [tab, setTab] = useState<TabValue>(getInitialTab);
|
|
@@ -274,7 +276,7 @@ export function ResultsScreen({
|
|
|
274
276
|
/>
|
|
275
277
|
</TabsContent>
|
|
276
278
|
<TabsContent value="stack">
|
|
277
|
-
<StackPanel sessionId={sessionId} />
|
|
279
|
+
<StackPanel sessionId={sessionId} onTrackAnalysis={onTrackAnalysis} />
|
|
278
280
|
</TabsContent>
|
|
279
281
|
<TabsContent value="slides">
|
|
280
282
|
<SlidesPanel data={data} sessionId={sessionId} />
|
|
@@ -285,6 +285,11 @@ function AnalyticsToggle() {
|
|
|
285
285
|
setConsent(next);
|
|
286
286
|
setLocal(next);
|
|
287
287
|
analytics.settingsChanged("analytics");
|
|
288
|
+
fetch("/api/config", {
|
|
289
|
+
method: "PUT",
|
|
290
|
+
headers: { "Content-Type": "application/json" },
|
|
291
|
+
body: JSON.stringify({ telemetry_consent: next }),
|
|
292
|
+
}).catch(() => {});
|
|
288
293
|
};
|
|
289
294
|
|
|
290
295
|
return (
|
|
@@ -133,7 +133,11 @@ function applyServerState(server: ServerStackState): Partial<StackState> {
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
interface UseStackOptions {
|
|
137
|
+
onTrackAnalysis?: (analysisSessionId: string, prUrl: string) => void;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function useStack(sessionId: string | null | undefined, options?: UseStackOptions) {
|
|
137
141
|
const [state, setState] = useState<StackState>({
|
|
138
142
|
phase: "idle",
|
|
139
143
|
error: null,
|
|
@@ -253,11 +257,29 @@ export function useStack(sessionId: string | null | undefined) {
|
|
|
253
257
|
const data = await res.json();
|
|
254
258
|
if (!res.ok) throw new Error(data.error ?? "Publishing failed");
|
|
255
259
|
|
|
260
|
+
const publishResult = data.publish_result as PublishResultData;
|
|
261
|
+
|
|
256
262
|
setState((s) => ({
|
|
257
263
|
...s,
|
|
258
264
|
phase: "done",
|
|
259
|
-
publishResult
|
|
265
|
+
publishResult,
|
|
260
266
|
}));
|
|
267
|
+
|
|
268
|
+
if (options?.onTrackAnalysis && publishResult?.prs?.length > 0) {
|
|
269
|
+
for (const pr of publishResult.prs) {
|
|
270
|
+
try {
|
|
271
|
+
const analysisRes = await fetch("/api/analysis", {
|
|
272
|
+
method: "POST",
|
|
273
|
+
headers: { "Content-Type": "application/json" },
|
|
274
|
+
body: JSON.stringify({ pr: pr.url }),
|
|
275
|
+
});
|
|
276
|
+
const analysisData = await analysisRes.json() as { sessionId?: string };
|
|
277
|
+
if (analysisData.sessionId) {
|
|
278
|
+
options.onTrackAnalysis(analysisData.sessionId, pr.url);
|
|
279
|
+
}
|
|
280
|
+
} catch {}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
261
283
|
} catch (err) {
|
|
262
284
|
setState((s) => ({
|
|
263
285
|
...s,
|
|
@@ -265,7 +287,7 @@ export function useStack(sessionId: string | null | undefined) {
|
|
|
265
287
|
error: err instanceof Error ? err.message : String(err),
|
|
266
288
|
}));
|
|
267
289
|
}
|
|
268
|
-
}, [sessionId]);
|
|
290
|
+
}, [sessionId, options]);
|
|
269
291
|
|
|
270
292
|
const reset = useCallback(() => {
|
|
271
293
|
eventSourceRef.current?.close();
|
|
@@ -68,8 +68,13 @@ function PipelineTimeline({ phase }: { phase: StackPhase }) {
|
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
interface StackPanelProps {
|
|
72
|
+
sessionId?: string | null;
|
|
73
|
+
onTrackAnalysis?: (analysisSessionId: string, prUrl: string) => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function StackPanel({ sessionId, onTrackAnalysis }: StackPanelProps) {
|
|
77
|
+
const stack = useStack(sessionId, { onTrackAnalysis });
|
|
73
78
|
|
|
74
79
|
if (stack.phase === "idle") {
|
|
75
80
|
return (
|
package/src/web/server/routes.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { detectAgents, runAgent } from "../../workspace/agent.ts";
|
|
|
17
17
|
import { randomBytes } from "node:crypto";
|
|
18
18
|
import { publishStack } from "../../stack/publish.ts";
|
|
19
19
|
import { startStack, getStackState, cancelStack, subscribeStack, restoreCompletedStacks } from "./stack-manager.ts";
|
|
20
|
+
import { getTelemetryConsent, setTelemetryConsent, telemetry } from "../../telemetry/index.ts";
|
|
20
21
|
|
|
21
22
|
function json(data: unknown, status = 200): Response {
|
|
22
23
|
return new Response(JSON.stringify(data), {
|
|
@@ -610,6 +611,7 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
|
|
|
610
611
|
const pluginList = getAllPlugins().map((p) => ({ id: p.id, name: p.name }));
|
|
611
612
|
const enabledPlugins = stored.enabled_plugins ?? pluginList.map((p) => p.id);
|
|
612
613
|
const agents = await detectAgents();
|
|
614
|
+
const telemetryConsent = await getTelemetryConsent();
|
|
613
615
|
return json({
|
|
614
616
|
model: config.model,
|
|
615
617
|
agent: config.agent ?? null,
|
|
@@ -622,6 +624,7 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
|
|
|
622
624
|
has_github_token: !!token,
|
|
623
625
|
enabled_plugins: enabledPlugins,
|
|
624
626
|
available_plugins: pluginList,
|
|
627
|
+
telemetry_consent: telemetryConsent,
|
|
625
628
|
defaults: {
|
|
626
629
|
model: DEFAULT_CONFIG.model,
|
|
627
630
|
language: DEFAULT_CONFIG.language,
|
|
@@ -673,6 +676,11 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
|
|
|
673
676
|
update.enabled_plugins = (body as Record<string, unknown>).enabled_plugins as string[];
|
|
674
677
|
}
|
|
675
678
|
|
|
679
|
+
const telemetryConsentVal = (body as Record<string, unknown>).telemetry_consent as string | undefined;
|
|
680
|
+
if (telemetryConsentVal === "granted" || telemetryConsentVal === "denied") {
|
|
681
|
+
await setTelemetryConsent(telemetryConsentVal);
|
|
682
|
+
}
|
|
683
|
+
|
|
676
684
|
await writeStoredConfig(update);
|
|
677
685
|
return json({ ok: true });
|
|
678
686
|
},
|
|
@@ -748,6 +756,7 @@ Before posting an inline comment, ALWAYS call \`get_file_diff\` first to find th
|
|
|
748
756
|
return json({ error: errBody.message ?? `GitHub API error: ${res.status}` }, res.status);
|
|
749
757
|
}
|
|
750
758
|
const data = await res.json() as { id: number; state: string; html_url: string };
|
|
759
|
+
telemetry.reviewSubmitted(body.event);
|
|
751
760
|
return json({ ok: true, id: data.id, state: data.state, html_url: data.html_url });
|
|
752
761
|
} catch (err) {
|
|
753
762
|
return json({ error: err instanceof Error ? err.message : String(err) }, 500);
|
|
@@ -4,6 +4,7 @@ import type { ProgressEvent } from "../../analyzer/progress.ts";
|
|
|
4
4
|
import { analyzePr } from "../../analyzer/pipeline.ts";
|
|
5
5
|
import { parsePrInput } from "../../github/parse-pr.ts";
|
|
6
6
|
import { saveSession, savePatchesSidecar } from "../../history/store.ts";
|
|
7
|
+
import { telemetry } from "../../telemetry/index.ts";
|
|
7
8
|
|
|
8
9
|
type SessionStatus = "running" | "done" | "error" | "canceled";
|
|
9
10
|
|
|
@@ -65,6 +66,7 @@ export function startAnalysis(
|
|
|
65
66
|
};
|
|
66
67
|
sessions.set(id, session);
|
|
67
68
|
|
|
69
|
+
telemetry.analysisStarted(0);
|
|
68
70
|
runPipeline(session, prInput, token, config);
|
|
69
71
|
|
|
70
72
|
return { sessionId: id };
|
|
@@ -100,6 +102,9 @@ async function runPipeline(
|
|
|
100
102
|
session.result = result;
|
|
101
103
|
session.finishedAt = Date.now();
|
|
102
104
|
|
|
105
|
+
const durationSec = Math.round((session.finishedAt - session.startedAt) / 1000);
|
|
106
|
+
telemetry.analysisCompleted(result.files?.length ?? 0, durationSec);
|
|
107
|
+
|
|
103
108
|
for (const sub of session.subscribers) {
|
|
104
109
|
sub({ type: "done" });
|
|
105
110
|
}
|
|
@@ -118,6 +123,8 @@ async function runPipeline(
|
|
|
118
123
|
session.error = msg;
|
|
119
124
|
session.finishedAt = Date.now();
|
|
120
125
|
|
|
126
|
+
telemetry.analysisError(msg);
|
|
127
|
+
|
|
121
128
|
for (const sub of session.subscribers) {
|
|
122
129
|
sub({ type: "error", data: msg });
|
|
123
130
|
}
|
package/src/web/server.ts
CHANGED
|
@@ -5,6 +5,7 @@ import index from "./index.html";
|
|
|
5
5
|
|
|
6
6
|
import type { PreflightResult } from "../cli/preflight.ts";
|
|
7
7
|
import { getVersion } from "../version.ts";
|
|
8
|
+
import { telemetry } from "../telemetry/index.ts";
|
|
8
9
|
|
|
9
10
|
interface WebServerOptions {
|
|
10
11
|
port: number;
|
|
@@ -211,6 +212,8 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
|
|
|
211
212
|
|
|
212
213
|
const url = `http://localhost:${server.port}`;
|
|
213
214
|
|
|
215
|
+
telemetry.serverStarted(getVersion());
|
|
216
|
+
|
|
214
217
|
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
215
218
|
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
216
219
|
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
|