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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.203",
3
+ "version": "10.0.205",
4
4
  "name": "snow-flow",
5
5
  "description": "Snow-Flow - ServiceNow Multi-Agent Development Framework powered by AI",
6
6
  "license": "Elastic-2.0",
@@ -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
- async function sendPing(payload: Record<string, unknown>): Promise<void> {
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