snow-flow 10.0.204 → 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 +78 -13
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
|
|
|
@@ -90,23 +146,30 @@ export namespace AnonymousTelemetry {
|
|
|
90
146
|
}),
|
|
91
147
|
]
|
|
92
148
|
|
|
93
|
-
const flushEndPing = async (
|
|
149
|
+
const flushEndPing = async (
|
|
150
|
+
reason?: "normal" | "error" | "interrupt",
|
|
151
|
+
errorMessage?: string,
|
|
152
|
+
options?: { persist?: boolean },
|
|
153
|
+
) => {
|
|
94
154
|
if (configDisabled || endPingSent) return
|
|
95
155
|
if (reason) exitReason = reason
|
|
96
156
|
if (errorMessage) exitErrorMessage = errorMessage.slice(0, 500)
|
|
97
157
|
endPingSent = true
|
|
98
158
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
await sendPing({
|
|
159
|
+
const payload: TelemetryPingPayload = {
|
|
102
160
|
...basePayload,
|
|
103
161
|
type: "end",
|
|
104
162
|
exitReason,
|
|
105
163
|
exitErrorMessage,
|
|
106
|
-
sessionDurationSec,
|
|
164
|
+
sessionDurationSec: Math.round((Date.now() - startTime) / 1000),
|
|
107
165
|
messageCount,
|
|
108
166
|
timestamp: Date.now(),
|
|
109
|
-
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (options?.persist) writePendingEndPing(payload)
|
|
170
|
+
|
|
171
|
+
const sent = await sendPing(payload)
|
|
172
|
+
if (sent) clearPendingEndPing()
|
|
110
173
|
}
|
|
111
174
|
|
|
112
175
|
// Track exit reason via process signals
|
|
@@ -115,11 +178,11 @@ export namespace AnonymousTelemetry {
|
|
|
115
178
|
if (err instanceof Error) exitErrorMessage = `${err.name}: ${err.message}`.slice(0, 500)
|
|
116
179
|
else if (typeof err === "string") exitErrorMessage = err.slice(0, 500)
|
|
117
180
|
else exitErrorMessage = String(err).slice(0, 500)
|
|
118
|
-
void flushEndPing("error", exitErrorMessage)
|
|
181
|
+
void flushEndPing("error", exitErrorMessage, { persist: true })
|
|
119
182
|
}
|
|
120
183
|
const onInterrupt = () => {
|
|
121
184
|
exitReason = "interrupt"
|
|
122
|
-
void flushEndPing("interrupt")
|
|
185
|
+
void flushEndPing("interrupt", undefined, { persist: true })
|
|
123
186
|
}
|
|
124
187
|
process.on("uncaughtException", onError)
|
|
125
188
|
process.on("unhandledRejection", onError)
|
|
@@ -139,7 +202,7 @@ export namespace AnonymousTelemetry {
|
|
|
139
202
|
log.info("anonymous telemetry initializing", { machineId: machineId.slice(0, 8) + "..." })
|
|
140
203
|
|
|
141
204
|
Config.get()
|
|
142
|
-
.then((config) => {
|
|
205
|
+
.then(async (config) => {
|
|
143
206
|
if (config.telemetry === false) {
|
|
144
207
|
configDisabled = true
|
|
145
208
|
log.info("anonymous telemetry disabled (config)")
|
|
@@ -147,12 +210,14 @@ export namespace AnonymousTelemetry {
|
|
|
147
210
|
unsubs.length = 0
|
|
148
211
|
return
|
|
149
212
|
}
|
|
213
|
+
await flushPendingEndPing()
|
|
150
214
|
log.info("anonymous telemetry active, sending start ping")
|
|
151
|
-
sendPing({ ...basePayload, type: "start", sessionDurationSec: 0, messageCount: 0, timestamp: Date.now() })
|
|
215
|
+
await sendPing({ ...basePayload, type: "start", sessionDurationSec: 0, messageCount: 0, timestamp: Date.now() })
|
|
152
216
|
})
|
|
153
|
-
.catch(() => {
|
|
217
|
+
.catch(async () => {
|
|
218
|
+
await flushPendingEndPing()
|
|
154
219
|
log.info("anonymous telemetry active (config unavailable), sending start ping")
|
|
155
|
-
sendPing({ ...basePayload, type: "start", sessionDurationSec: 0, messageCount: 0, timestamp: Date.now() })
|
|
220
|
+
await sendPing({ ...basePayload, type: "start", sessionDurationSec: 0, messageCount: 0, timestamp: Date.now() })
|
|
156
221
|
})
|
|
157
222
|
|
|
158
223
|
return {
|