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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "10.0.204",
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
 
@@ -90,23 +146,30 @@ export namespace AnonymousTelemetry {
90
146
  }),
91
147
  ]
92
148
 
93
- const flushEndPing = async (reason?: "normal" | "error" | "interrupt", errorMessage?: string) => {
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 sessionDurationSec = Math.round((Date.now() - startTime) / 1000)
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 {