kanna-code 0.33.9 → 0.34.1
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/dist/client/assets/{index-BldnkLtN.js → index-BiI7fqiX.js} +192 -192
- package/dist/client/assets/index-ZfBgbTjt.css +32 -0
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/agent.test.ts +65 -0
- package/src/server/agent.ts +10 -0
- package/src/server/analytics.test.ts +305 -0
- package/src/server/analytics.ts +126 -0
- package/src/server/app-settings.test.ts +90 -0
- package/src/server/app-settings.ts +263 -0
- package/src/server/server.ts +28 -1
- package/src/server/update-manager.test.ts +35 -0
- package/src/server/update-manager.ts +19 -0
- package/src/server/ws-router.test.ts +262 -1
- package/src/server/ws-router.ts +57 -4
- package/src/shared/analytics.ts +30 -0
- package/src/shared/branding.ts +8 -0
- package/src/shared/protocol.ts +4 -0
- package/src/shared/types.ts +6 -0
- package/dist/client/assets/index-FQ1dOuTJ.css +0 -32
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import { watch, type FSWatcher } from "node:fs"
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
4
|
+
import { homedir } from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import { getSettingsFilePath, LOG_PREFIX } from "../shared/branding"
|
|
7
|
+
import type { AppSettingsSnapshot } from "../shared/types"
|
|
8
|
+
|
|
9
|
+
interface AppSettingsFile {
|
|
10
|
+
analyticsEnabled?: unknown
|
|
11
|
+
analyticsUserId?: unknown
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface AppSettingsState extends AppSettingsSnapshot {
|
|
15
|
+
analyticsUserId: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface NormalizedAppSettings {
|
|
19
|
+
payload: {
|
|
20
|
+
analyticsEnabled: boolean
|
|
21
|
+
analyticsUserId: string
|
|
22
|
+
}
|
|
23
|
+
warning: string | null
|
|
24
|
+
shouldWrite: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatDisplayPath(filePath: string) {
|
|
28
|
+
const homePath = homedir()
|
|
29
|
+
if (filePath === homePath) return "~"
|
|
30
|
+
if (filePath.startsWith(`${homePath}${path.sep}`)) {
|
|
31
|
+
return `~${filePath.slice(homePath.length)}`
|
|
32
|
+
}
|
|
33
|
+
return filePath
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createAnalyticsUserId() {
|
|
37
|
+
return `anon_${randomUUID()}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeAppSettings(
|
|
41
|
+
value: unknown,
|
|
42
|
+
filePath = getSettingsFilePath(homedir())
|
|
43
|
+
): NormalizedAppSettings {
|
|
44
|
+
const source = value && typeof value === "object" && !Array.isArray(value)
|
|
45
|
+
? value as AppSettingsFile
|
|
46
|
+
: null
|
|
47
|
+
const warnings: string[] = []
|
|
48
|
+
|
|
49
|
+
if (value !== undefined && value !== null && !source) {
|
|
50
|
+
warnings.push("Settings file must contain a JSON object")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const analyticsEnabled = typeof source?.analyticsEnabled === "boolean"
|
|
54
|
+
? source.analyticsEnabled
|
|
55
|
+
: true
|
|
56
|
+
if (source?.analyticsEnabled !== undefined && typeof source.analyticsEnabled !== "boolean") {
|
|
57
|
+
warnings.push("analyticsEnabled must be a boolean")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const rawAnalyticsUserId = typeof source?.analyticsUserId === "string"
|
|
61
|
+
? source.analyticsUserId.trim()
|
|
62
|
+
: ""
|
|
63
|
+
if (source?.analyticsUserId !== undefined && typeof source.analyticsUserId !== "string") {
|
|
64
|
+
warnings.push("analyticsUserId must be a string")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const analyticsUserId = rawAnalyticsUserId || createAnalyticsUserId()
|
|
68
|
+
if (!rawAnalyticsUserId && source?.analyticsUserId !== undefined) {
|
|
69
|
+
warnings.push("analyticsUserId must be a non-empty string")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const shouldWrite = !source
|
|
73
|
+
|| source.analyticsEnabled !== analyticsEnabled
|
|
74
|
+
|| rawAnalyticsUserId !== analyticsUserId
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
payload: {
|
|
78
|
+
analyticsEnabled,
|
|
79
|
+
analyticsUserId,
|
|
80
|
+
},
|
|
81
|
+
warning: warnings.length > 0
|
|
82
|
+
? `Some settings were reset to defaults: ${warnings.join("; ")}`
|
|
83
|
+
: null,
|
|
84
|
+
shouldWrite,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toSnapshot(state: AppSettingsState): AppSettingsSnapshot {
|
|
89
|
+
return {
|
|
90
|
+
analyticsEnabled: state.analyticsEnabled,
|
|
91
|
+
warning: state.warning,
|
|
92
|
+
filePathDisplay: state.filePathDisplay,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function readAppSettingsSnapshot(filePath = getSettingsFilePath(homedir())) {
|
|
97
|
+
try {
|
|
98
|
+
const text = await readFile(filePath, "utf8")
|
|
99
|
+
if (!text.trim()) {
|
|
100
|
+
const normalized = normalizeAppSettings(undefined, filePath)
|
|
101
|
+
return {
|
|
102
|
+
analyticsEnabled: normalized.payload.analyticsEnabled,
|
|
103
|
+
warning: "Settings file was empty. Using defaults.",
|
|
104
|
+
filePathDisplay: formatDisplayPath(filePath),
|
|
105
|
+
} satisfies AppSettingsSnapshot
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const normalized = normalizeAppSettings(JSON.parse(text), filePath)
|
|
109
|
+
return {
|
|
110
|
+
analyticsEnabled: normalized.payload.analyticsEnabled,
|
|
111
|
+
warning: normalized.warning,
|
|
112
|
+
filePathDisplay: formatDisplayPath(filePath),
|
|
113
|
+
} satisfies AppSettingsSnapshot
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
116
|
+
return {
|
|
117
|
+
analyticsEnabled: true,
|
|
118
|
+
warning: null,
|
|
119
|
+
filePathDisplay: formatDisplayPath(filePath),
|
|
120
|
+
} satisfies AppSettingsSnapshot
|
|
121
|
+
}
|
|
122
|
+
if (error instanceof SyntaxError) {
|
|
123
|
+
return {
|
|
124
|
+
analyticsEnabled: true,
|
|
125
|
+
warning: "Settings file is invalid JSON. Using defaults.",
|
|
126
|
+
filePathDisplay: formatDisplayPath(filePath),
|
|
127
|
+
} satisfies AppSettingsSnapshot
|
|
128
|
+
}
|
|
129
|
+
throw error
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class AppSettingsManager {
|
|
134
|
+
readonly filePath: string
|
|
135
|
+
private watcher: FSWatcher | null = null
|
|
136
|
+
private state: AppSettingsState
|
|
137
|
+
private readonly listeners = new Set<(snapshot: AppSettingsSnapshot) => void>()
|
|
138
|
+
|
|
139
|
+
constructor(filePath = getSettingsFilePath(homedir())) {
|
|
140
|
+
this.filePath = filePath
|
|
141
|
+
const displayPath = formatDisplayPath(this.filePath)
|
|
142
|
+
this.state = {
|
|
143
|
+
analyticsEnabled: true,
|
|
144
|
+
analyticsUserId: createAnalyticsUserId(),
|
|
145
|
+
warning: null,
|
|
146
|
+
filePathDisplay: displayPath,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async initialize() {
|
|
151
|
+
await mkdir(path.dirname(this.filePath), { recursive: true })
|
|
152
|
+
await this.reload({ persistNormalized: true })
|
|
153
|
+
this.startWatching()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
dispose() {
|
|
157
|
+
this.watcher?.close()
|
|
158
|
+
this.watcher = null
|
|
159
|
+
this.listeners.clear()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getSnapshot() {
|
|
163
|
+
return toSnapshot(this.state)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
getState() {
|
|
167
|
+
return this.state
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
onChange(listener: (snapshot: AppSettingsSnapshot) => void) {
|
|
171
|
+
this.listeners.add(listener)
|
|
172
|
+
return () => {
|
|
173
|
+
this.listeners.delete(listener)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async reload(options?: { persistNormalized?: boolean }) {
|
|
178
|
+
const nextState = await this.readState(options)
|
|
179
|
+
this.setState(nextState)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async write(value: { analyticsEnabled: boolean }) {
|
|
183
|
+
const payload = {
|
|
184
|
+
analyticsEnabled: value.analyticsEnabled,
|
|
185
|
+
analyticsUserId: this.state.analyticsUserId || createAnalyticsUserId(),
|
|
186
|
+
}
|
|
187
|
+
await mkdir(path.dirname(this.filePath), { recursive: true })
|
|
188
|
+
await writeFile(this.filePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8")
|
|
189
|
+
const nextState: AppSettingsState = {
|
|
190
|
+
analyticsEnabled: payload.analyticsEnabled,
|
|
191
|
+
analyticsUserId: payload.analyticsUserId,
|
|
192
|
+
warning: null,
|
|
193
|
+
filePathDisplay: formatDisplayPath(this.filePath),
|
|
194
|
+
}
|
|
195
|
+
this.setState(nextState)
|
|
196
|
+
return toSnapshot(nextState)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async readState(options?: { persistNormalized?: boolean }) {
|
|
200
|
+
const file = Bun.file(this.filePath)
|
|
201
|
+
const displayPath = formatDisplayPath(this.filePath)
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const text = await file.text()
|
|
205
|
+
const hasText = text.trim().length > 0
|
|
206
|
+
const normalized = normalizeAppSettings(hasText ? JSON.parse(text) : undefined, this.filePath)
|
|
207
|
+
if (options?.persistNormalized && (!hasText || normalized.shouldWrite)) {
|
|
208
|
+
await writeFile(this.filePath, `${JSON.stringify(normalized.payload, null, 2)}\n`, "utf8")
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
analyticsEnabled: normalized.payload.analyticsEnabled,
|
|
212
|
+
analyticsUserId: normalized.payload.analyticsUserId,
|
|
213
|
+
warning: !hasText
|
|
214
|
+
? "Settings file was empty. Using defaults."
|
|
215
|
+
: normalized.warning,
|
|
216
|
+
filePathDisplay: displayPath,
|
|
217
|
+
} satisfies AppSettingsState
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if ((error as NodeJS.ErrnoException)?.code !== "ENOENT" && !(error instanceof SyntaxError)) {
|
|
220
|
+
throw error
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const warning = error instanceof SyntaxError
|
|
224
|
+
? "Settings file is invalid JSON. Using defaults."
|
|
225
|
+
: null
|
|
226
|
+
const normalized = normalizeAppSettings(undefined, this.filePath)
|
|
227
|
+
if (options?.persistNormalized) {
|
|
228
|
+
await writeFile(this.filePath, `${JSON.stringify(normalized.payload, null, 2)}\n`, "utf8")
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
analyticsEnabled: normalized.payload.analyticsEnabled,
|
|
232
|
+
analyticsUserId: normalized.payload.analyticsUserId,
|
|
233
|
+
warning,
|
|
234
|
+
filePathDisplay: displayPath,
|
|
235
|
+
} satisfies AppSettingsState
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private setState(state: AppSettingsState) {
|
|
240
|
+
this.state = state
|
|
241
|
+
const snapshot = toSnapshot(state)
|
|
242
|
+
for (const listener of this.listeners) {
|
|
243
|
+
listener(snapshot)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private startWatching() {
|
|
248
|
+
this.watcher?.close()
|
|
249
|
+
try {
|
|
250
|
+
this.watcher = watch(path.dirname(this.filePath), { persistent: false }, (_eventType, filename) => {
|
|
251
|
+
if (filename && filename !== path.basename(this.filePath)) {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
void this.reload().catch((error: unknown) => {
|
|
255
|
+
console.warn(`${LOG_PREFIX} Failed to reload settings:`, error)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
} catch (error) {
|
|
259
|
+
console.warn(`${LOG_PREFIX} Failed to watch settings file:`, error)
|
|
260
|
+
this.watcher = null
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
package/src/server/server.ts
CHANGED
|
@@ -2,9 +2,12 @@ import path from "node:path"
|
|
|
2
2
|
import { stat } from "node:fs/promises"
|
|
3
3
|
import { APP_NAME, getRuntimeProfile } from "../shared/branding"
|
|
4
4
|
import type { ChatAttachment } from "../shared/types"
|
|
5
|
+
import type { ShareMode } from "../shared/share"
|
|
5
6
|
import { createAuthManager } from "./auth"
|
|
6
7
|
import { EventStore } from "./event-store"
|
|
7
8
|
import { AgentCoordinator } from "./agent"
|
|
9
|
+
import { KannaAnalyticsReporter } from "./analytics"
|
|
10
|
+
import { AppSettingsManager } from "./app-settings"
|
|
8
11
|
import { DiffStore } from "./diff-store"
|
|
9
12
|
import { discoverProjects, type DiscoveredProject } from "./discovery"
|
|
10
13
|
import { KeybindingsManager } from "./keybindings"
|
|
@@ -58,6 +61,8 @@ export async function persistUploadedFiles(args: {
|
|
|
58
61
|
export interface StartKannaServerOptions {
|
|
59
62
|
port?: number
|
|
60
63
|
host?: string
|
|
64
|
+
openBrowser?: boolean
|
|
65
|
+
share?: ShareMode
|
|
61
66
|
dataDir?: string
|
|
62
67
|
password?: string | null
|
|
63
68
|
strictPort?: boolean
|
|
@@ -80,6 +85,7 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
80
85
|
const port = options.port ?? 3210
|
|
81
86
|
const hostname = options.host ?? "127.0.0.1"
|
|
82
87
|
const strictPort = options.strictPort ?? false
|
|
88
|
+
const runtimeProfile = getRuntimeProfile()
|
|
83
89
|
const auth = options.password ? createAuthManager(options.password, { trustProxy: options.trustProxy ?? false }) : null
|
|
84
90
|
const store = new EventStore(options.dataDir)
|
|
85
91
|
const diffStore = new DiffStore(store.dataDir)
|
|
@@ -100,17 +106,26 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
100
106
|
let router: ReturnType<typeof createWsRouter>
|
|
101
107
|
const terminals = new TerminalManager()
|
|
102
108
|
const keybindings = new KeybindingsManager()
|
|
109
|
+
const appSettings = new AppSettingsManager(path.join(store.dataDir, "settings.json"))
|
|
110
|
+
await appSettings.initialize()
|
|
103
111
|
await keybindings.initialize()
|
|
112
|
+
const analytics = new KannaAnalyticsReporter({
|
|
113
|
+
settings: appSettings,
|
|
114
|
+
currentVersion: options.update?.version ?? "unknown",
|
|
115
|
+
environment: runtimeProfile === "dev" ? "dev" : "prod",
|
|
116
|
+
})
|
|
104
117
|
const updateManager = options.update
|
|
105
118
|
? new UpdateManager({
|
|
106
119
|
currentVersion: options.update.version,
|
|
107
120
|
fetchLatestVersion: options.update.fetchLatestVersion,
|
|
108
121
|
installVersion: options.update.installVersion,
|
|
109
|
-
devMode:
|
|
122
|
+
devMode: runtimeProfile === "dev",
|
|
123
|
+
trackEvent: analytics.track.bind(analytics),
|
|
110
124
|
})
|
|
111
125
|
: null
|
|
112
126
|
const agent = new AgentCoordinator({
|
|
113
127
|
store,
|
|
128
|
+
analytics,
|
|
114
129
|
onStateChange: (chatId?: string, options?: { immediate?: boolean }) => {
|
|
115
130
|
if (chatId) {
|
|
116
131
|
if (options?.immediate) {
|
|
@@ -129,6 +144,8 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
129
144
|
agent,
|
|
130
145
|
terminals,
|
|
131
146
|
keybindings,
|
|
147
|
+
appSettings,
|
|
148
|
+
analytics,
|
|
132
149
|
llmProvider: {
|
|
133
150
|
read: readLlmProviderSnapshot,
|
|
134
151
|
write: writeLlmProviderSnapshot,
|
|
@@ -256,12 +273,22 @@ export async function startKannaServer(options: StartKannaServerOptions = {}) {
|
|
|
256
273
|
}
|
|
257
274
|
}
|
|
258
275
|
|
|
276
|
+
analytics.trackLaunch({
|
|
277
|
+
port: actualPort,
|
|
278
|
+
host: hostname,
|
|
279
|
+
openBrowser: options.openBrowser ?? true,
|
|
280
|
+
share: options.share ?? false,
|
|
281
|
+
password: options.password ?? null,
|
|
282
|
+
strictPort,
|
|
283
|
+
})
|
|
284
|
+
|
|
259
285
|
const shutdown = async () => {
|
|
260
286
|
clearInterval(staleEmptyChatPruneInterval)
|
|
261
287
|
for (const chatId of [...agent.activeTurns.keys()]) {
|
|
262
288
|
await agent.cancel(chatId)
|
|
263
289
|
}
|
|
264
290
|
router.dispose()
|
|
291
|
+
appSettings.dispose()
|
|
265
292
|
keybindings.dispose()
|
|
266
293
|
terminals.closeAll()
|
|
267
294
|
await store.compact()
|
|
@@ -2,6 +2,41 @@ import { describe, expect, test } from "bun:test"
|
|
|
2
2
|
import { UpdateManager } from "./update-manager"
|
|
3
3
|
|
|
4
4
|
describe("UpdateManager", () => {
|
|
5
|
+
test("tracks update lifecycle events", async () => {
|
|
6
|
+
const events: Array<{ name: string; properties?: Record<string, unknown> }> = []
|
|
7
|
+
const manager = new UpdateManager({
|
|
8
|
+
currentVersion: "0.12.0",
|
|
9
|
+
fetchLatestVersion: async () => "0.13.0",
|
|
10
|
+
installVersion: () => ({
|
|
11
|
+
ok: true,
|
|
12
|
+
errorCode: null,
|
|
13
|
+
userTitle: null,
|
|
14
|
+
userMessage: null,
|
|
15
|
+
}),
|
|
16
|
+
trackEvent: (eventName, properties) => {
|
|
17
|
+
events.push({ name: eventName, properties })
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
await manager.checkForUpdates({ force: true })
|
|
22
|
+
await manager.installUpdate()
|
|
23
|
+
|
|
24
|
+
expect(events).toEqual([
|
|
25
|
+
{
|
|
26
|
+
name: "update_checked",
|
|
27
|
+
properties: {
|
|
28
|
+
latest_version: "0.13.0",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "update_installed",
|
|
33
|
+
properties: {
|
|
34
|
+
latest_version: "0.13.0",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
])
|
|
38
|
+
})
|
|
39
|
+
|
|
5
40
|
test("detects available updates", async () => {
|
|
6
41
|
const manager = new UpdateManager({
|
|
7
42
|
currentVersion: "0.12.0",
|
|
@@ -9,6 +9,7 @@ export interface UpdateManagerDeps {
|
|
|
9
9
|
fetchLatestVersion: (packageName: string) => Promise<string>
|
|
10
10
|
installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
|
|
11
11
|
devMode?: boolean
|
|
12
|
+
trackEvent?: (eventName: string, properties?: Record<string, unknown>) => void
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export class UpdateManager {
|
|
@@ -81,6 +82,9 @@ export class UpdateManager {
|
|
|
81
82
|
|
|
82
83
|
async installUpdate(): Promise<UpdateInstallResult> {
|
|
83
84
|
if (this.deps.devMode) {
|
|
85
|
+
this.deps.trackEvent?.("update_installed", {
|
|
86
|
+
latest_version: this.snapshot.latestVersion,
|
|
87
|
+
})
|
|
84
88
|
this.setSnapshot({
|
|
85
89
|
...this.snapshot,
|
|
86
90
|
status: "updating",
|
|
@@ -145,6 +149,9 @@ export class UpdateManager {
|
|
|
145
149
|
reloadRequestedAt: null,
|
|
146
150
|
}
|
|
147
151
|
this.setSnapshot(nextSnapshot)
|
|
152
|
+
this.deps.trackEvent?.("update_checked", {
|
|
153
|
+
latest_version: latestVersion,
|
|
154
|
+
})
|
|
148
155
|
return nextSnapshot
|
|
149
156
|
} catch (error) {
|
|
150
157
|
const nextSnapshot: UpdateSnapshot = {
|
|
@@ -155,6 +162,9 @@ export class UpdateManager {
|
|
|
155
162
|
reloadRequestedAt: null,
|
|
156
163
|
}
|
|
157
164
|
this.setSnapshot(nextSnapshot)
|
|
165
|
+
this.deps.trackEvent?.("update_failed", {
|
|
166
|
+
latest_version: this.snapshot.latestVersion,
|
|
167
|
+
})
|
|
158
168
|
return nextSnapshot
|
|
159
169
|
}
|
|
160
170
|
}
|
|
@@ -188,6 +198,9 @@ export class UpdateManager {
|
|
|
188
198
|
error: "Unable to determine which version to install.",
|
|
189
199
|
reloadRequestedAt: null,
|
|
190
200
|
})
|
|
201
|
+
this.deps.trackEvent?.("update_failed", {
|
|
202
|
+
latest_version: null,
|
|
203
|
+
})
|
|
191
204
|
return {
|
|
192
205
|
ok: false,
|
|
193
206
|
action: "restart",
|
|
@@ -205,6 +218,9 @@ export class UpdateManager {
|
|
|
205
218
|
error: installed.userMessage ?? "Unable to install the latest version.",
|
|
206
219
|
reloadRequestedAt: null,
|
|
207
220
|
})
|
|
221
|
+
this.deps.trackEvent?.("update_failed", {
|
|
222
|
+
latest_version: targetVersion,
|
|
223
|
+
})
|
|
208
224
|
return {
|
|
209
225
|
ok: false,
|
|
210
226
|
action: "restart",
|
|
@@ -222,6 +238,9 @@ export class UpdateManager {
|
|
|
222
238
|
error: null,
|
|
223
239
|
reloadRequestedAt: Date.now(),
|
|
224
240
|
})
|
|
241
|
+
this.deps.trackEvent?.("update_installed", {
|
|
242
|
+
latest_version: targetVersion,
|
|
243
|
+
})
|
|
225
244
|
return {
|
|
226
245
|
ok: true,
|
|
227
246
|
action: "restart",
|