snow-flow 10.0.203 → 10.0.205
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/usage/anonymous-telemetry.ts +96 -17
package/package.json
CHANGED
|
@@ -5,11 +5,31 @@ import { Log } from "@/util/log"
|
|
|
5
5
|
import { Installation } from "@/installation"
|
|
6
6
|
import { Flag } from "@/flag/flag"
|
|
7
7
|
import { Config } from "@/config/config"
|
|
8
|
+
import { Global } from "@/global"
|
|
8
9
|
import { machineIdSync } from "node-machine-id"
|
|
10
|
+
import fs from "fs"
|
|
11
|
+
import path from "path"
|
|
9
12
|
|
|
10
13
|
const log = Log.create({ service: "usage.anonymous-telemetry" })
|
|
11
14
|
|
|
12
15
|
const PORTAL_URL = process.env.SNOW_FLOW_PORTAL_URL || "https://portal.snow-flow.dev"
|
|
16
|
+
const PENDING_END_PING_PATH = path.join(Global.Path.state, "anonymous-telemetry-pending-end.json")
|
|
17
|
+
|
|
18
|
+
interface TelemetryPingPayload {
|
|
19
|
+
machineId: string
|
|
20
|
+
sessionId: string
|
|
21
|
+
version: string
|
|
22
|
+
channel: string
|
|
23
|
+
os: string
|
|
24
|
+
arch: string
|
|
25
|
+
installMethod: string
|
|
26
|
+
type: "start" | "end"
|
|
27
|
+
sessionDurationSec: number
|
|
28
|
+
messageCount: number
|
|
29
|
+
timestamp: number
|
|
30
|
+
exitReason?: "normal" | "error" | "interrupt"
|
|
31
|
+
exitErrorMessage?: string
|
|
32
|
+
}
|
|
13
33
|
|
|
14
34
|
function isDisabled(): boolean {
|
|
15
35
|
const dnt = process.env.DO_NOT_TRACK?.toLowerCase()
|
|
@@ -41,7 +61,41 @@ function detectInstallMethod(): string {
|
|
|
41
61
|
return "other"
|
|
42
62
|
}
|
|
43
63
|
|
|
44
|
-
|
|
64
|
+
function writePendingEndPing(payload: TelemetryPingPayload): void {
|
|
65
|
+
try {
|
|
66
|
+
fs.mkdirSync(path.dirname(PENDING_END_PING_PATH), { recursive: true })
|
|
67
|
+
fs.writeFileSync(PENDING_END_PING_PATH, JSON.stringify(payload), "utf-8")
|
|
68
|
+
} catch (error) {
|
|
69
|
+
log.info("failed to persist pending telemetry ping", { error: String(error) })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readPendingEndPing(): TelemetryPingPayload | undefined {
|
|
74
|
+
try {
|
|
75
|
+
if (!fs.existsSync(PENDING_END_PING_PATH)) return undefined
|
|
76
|
+
return JSON.parse(fs.readFileSync(PENDING_END_PING_PATH, "utf-8")) as TelemetryPingPayload
|
|
77
|
+
} catch (error) {
|
|
78
|
+
log.info("failed to read pending telemetry ping", { error: String(error) })
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function clearPendingEndPing(): void {
|
|
84
|
+
try {
|
|
85
|
+
if (fs.existsSync(PENDING_END_PING_PATH)) fs.unlinkSync(PENDING_END_PING_PATH)
|
|
86
|
+
} catch (error) {
|
|
87
|
+
log.info("failed to clear pending telemetry ping", { error: String(error) })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function flushPendingEndPing(): Promise<void> {
|
|
92
|
+
const pending = readPendingEndPing()
|
|
93
|
+
if (!pending) return
|
|
94
|
+
const sent = await sendPing(pending)
|
|
95
|
+
if (sent) clearPendingEndPing()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function sendPing(payload: TelemetryPingPayload): Promise<boolean> {
|
|
45
99
|
try {
|
|
46
100
|
const response = await fetch(`${PORTAL_URL}/api/telemetry/ping`, {
|
|
47
101
|
method: "POST",
|
|
@@ -53,11 +107,13 @@ async function sendPing(payload: Record<string, unknown>): Promise<void> {
|
|
|
53
107
|
if (process.env.SNOW_FLOW_DEBUG_TELEMETRY) {
|
|
54
108
|
console.error(`[telemetry] ping OK (${payload.type}) → ${response.status}`)
|
|
55
109
|
}
|
|
110
|
+
return response.ok
|
|
56
111
|
} catch (error) {
|
|
57
112
|
log.info("telemetry ping failed", { error: String(error), type: payload.type })
|
|
58
113
|
if (process.env.SNOW_FLOW_DEBUG_TELEMETRY) {
|
|
59
114
|
console.error(`[telemetry] ping failed (${payload.type}):`, String(error))
|
|
60
115
|
}
|
|
116
|
+
return false
|
|
61
117
|
}
|
|
62
118
|
}
|
|
63
119
|
|
|
@@ -79,6 +135,7 @@ export namespace AnonymousTelemetry {
|
|
|
79
135
|
let exitReason: "normal" | "error" | "interrupt" = "normal"
|
|
80
136
|
let exitErrorMessage: string | undefined
|
|
81
137
|
let configDisabled = false
|
|
138
|
+
let endPingSent = false
|
|
82
139
|
const startTime = Date.now()
|
|
83
140
|
const sessionId = crypto.randomUUID()
|
|
84
141
|
const installMethod = detectInstallMethod()
|
|
@@ -89,15 +146,43 @@ export namespace AnonymousTelemetry {
|
|
|
89
146
|
}),
|
|
90
147
|
]
|
|
91
148
|
|
|
149
|
+
const flushEndPing = async (
|
|
150
|
+
reason?: "normal" | "error" | "interrupt",
|
|
151
|
+
errorMessage?: string,
|
|
152
|
+
options?: { persist?: boolean },
|
|
153
|
+
) => {
|
|
154
|
+
if (configDisabled || endPingSent) return
|
|
155
|
+
if (reason) exitReason = reason
|
|
156
|
+
if (errorMessage) exitErrorMessage = errorMessage.slice(0, 500)
|
|
157
|
+
endPingSent = true
|
|
158
|
+
|
|
159
|
+
const payload: TelemetryPingPayload = {
|
|
160
|
+
...basePayload,
|
|
161
|
+
type: "end",
|
|
162
|
+
exitReason,
|
|
163
|
+
exitErrorMessage,
|
|
164
|
+
sessionDurationSec: Math.round((Date.now() - startTime) / 1000),
|
|
165
|
+
messageCount,
|
|
166
|
+
timestamp: Date.now(),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (options?.persist) writePendingEndPing(payload)
|
|
170
|
+
|
|
171
|
+
const sent = await sendPing(payload)
|
|
172
|
+
if (sent) clearPendingEndPing()
|
|
173
|
+
}
|
|
174
|
+
|
|
92
175
|
// Track exit reason via process signals
|
|
93
176
|
const onError = (err: unknown) => {
|
|
94
177
|
exitReason = "error"
|
|
95
178
|
if (err instanceof Error) exitErrorMessage = `${err.name}: ${err.message}`.slice(0, 500)
|
|
96
179
|
else if (typeof err === "string") exitErrorMessage = err.slice(0, 500)
|
|
97
180
|
else exitErrorMessage = String(err).slice(0, 500)
|
|
181
|
+
void flushEndPing("error", exitErrorMessage, { persist: true })
|
|
98
182
|
}
|
|
99
183
|
const onInterrupt = () => {
|
|
100
184
|
exitReason = "interrupt"
|
|
185
|
+
void flushEndPing("interrupt", undefined, { persist: true })
|
|
101
186
|
}
|
|
102
187
|
process.on("uncaughtException", onError)
|
|
103
188
|
process.on("unhandledRejection", onError)
|
|
@@ -117,7 +202,7 @@ export namespace AnonymousTelemetry {
|
|
|
117
202
|
log.info("anonymous telemetry initializing", { machineId: machineId.slice(0, 8) + "..." })
|
|
118
203
|
|
|
119
204
|
Config.get()
|
|
120
|
-
.then((config) => {
|
|
205
|
+
.then(async (config) => {
|
|
121
206
|
if (config.telemetry === false) {
|
|
122
207
|
configDisabled = true
|
|
123
208
|
log.info("anonymous telemetry disabled (config)")
|
|
@@ -125,12 +210,14 @@ export namespace AnonymousTelemetry {
|
|
|
125
210
|
unsubs.length = 0
|
|
126
211
|
return
|
|
127
212
|
}
|
|
213
|
+
await flushPendingEndPing()
|
|
128
214
|
log.info("anonymous telemetry active, sending start ping")
|
|
129
|
-
sendPing({ ...basePayload, type: "start", sessionDurationSec: 0, messageCount: 0, timestamp: Date.now() })
|
|
215
|
+
await sendPing({ ...basePayload, type: "start", sessionDurationSec: 0, messageCount: 0, timestamp: Date.now() })
|
|
130
216
|
})
|
|
131
|
-
.catch(() => {
|
|
217
|
+
.catch(async () => {
|
|
218
|
+
await flushPendingEndPing()
|
|
132
219
|
log.info("anonymous telemetry active (config unavailable), sending start ping")
|
|
133
|
-
sendPing({ ...basePayload, type: "start", sessionDurationSec: 0, messageCount: 0, timestamp: Date.now() })
|
|
220
|
+
await sendPing({ ...basePayload, type: "start", sessionDurationSec: 0, messageCount: 0, timestamp: Date.now() })
|
|
134
221
|
})
|
|
135
222
|
|
|
136
223
|
return {
|
|
@@ -152,6 +239,9 @@ export namespace AnonymousTelemetry {
|
|
|
152
239
|
get configDisabled() {
|
|
153
240
|
return configDisabled
|
|
154
241
|
},
|
|
242
|
+
async flushEndPing() {
|
|
243
|
+
await flushEndPing()
|
|
244
|
+
},
|
|
155
245
|
cleanup() {
|
|
156
246
|
process.removeListener("uncaughtException", onError)
|
|
157
247
|
process.removeListener("unhandledRejection", onError)
|
|
@@ -165,18 +255,7 @@ export namespace AnonymousTelemetry {
|
|
|
165
255
|
if (current.disabled || current.configDisabled) return
|
|
166
256
|
|
|
167
257
|
if ("cleanup" in current) current.cleanup()
|
|
168
|
-
|
|
169
|
-
const sessionDurationSec = Math.round((Date.now() - current.startTime) / 1000)
|
|
170
|
-
|
|
171
|
-
await sendPing({
|
|
172
|
-
...current.basePayload,
|
|
173
|
-
type: "end",
|
|
174
|
-
exitReason: current.exitReason,
|
|
175
|
-
exitErrorMessage: current.exitErrorMessage,
|
|
176
|
-
sessionDurationSec,
|
|
177
|
-
messageCount: current.messageCount,
|
|
178
|
-
timestamp: Date.now(),
|
|
179
|
-
})
|
|
258
|
+
if ("flushEndPing" in current) await current.flushEndPing()
|
|
180
259
|
},
|
|
181
260
|
)
|
|
182
261
|
|