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,305 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { KannaAnalyticsReporter, getLaunchAnalyticsProperties } from "./analytics"
|
|
3
|
+
|
|
4
|
+
const originalLogAnalytics = process.env.KANNA_LOG_ANALYTICS
|
|
5
|
+
|
|
6
|
+
function restoreAnalyticsLoggingEnv() {
|
|
7
|
+
if (originalLogAnalytics === undefined) {
|
|
8
|
+
delete process.env.KANNA_LOG_ANALYTICS
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
process.env.KANNA_LOG_ANALYTICS = originalLogAnalytics
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("getLaunchAnalyticsProperties", () => {
|
|
15
|
+
test("expands launch flags into app_launch properties", () => {
|
|
16
|
+
expect(getLaunchAnalyticsProperties({
|
|
17
|
+
port: 4000,
|
|
18
|
+
host: "0.0.0.0",
|
|
19
|
+
openBrowser: false,
|
|
20
|
+
share: "quick",
|
|
21
|
+
password: "secret",
|
|
22
|
+
strictPort: true,
|
|
23
|
+
})).toEqual({
|
|
24
|
+
custom_port_enabled: true,
|
|
25
|
+
no_open_enabled: true,
|
|
26
|
+
password_enabled: true,
|
|
27
|
+
strict_port_enabled: true,
|
|
28
|
+
remote_enabled: true,
|
|
29
|
+
host_enabled: false,
|
|
30
|
+
share_quick_enabled: true,
|
|
31
|
+
share_token_enabled: false,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe("KannaAnalyticsReporter", () => {
|
|
37
|
+
test("posts the userId, event name, and shared properties", async () => {
|
|
38
|
+
const originalLog = console.log
|
|
39
|
+
const calls: Array<{ url: string; init?: RequestInit }> = []
|
|
40
|
+
console.log = () => {}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const reporter = new KannaAnalyticsReporter({
|
|
44
|
+
endpoint: "https://kanna.sh/api/t",
|
|
45
|
+
currentVersion: "0.33.9",
|
|
46
|
+
environment: "dev",
|
|
47
|
+
settings: {
|
|
48
|
+
getState: () => ({
|
|
49
|
+
analyticsEnabled: true,
|
|
50
|
+
analyticsUserId: "anon_123",
|
|
51
|
+
warning: null,
|
|
52
|
+
filePathDisplay: "~/.kanna/data/settings.json",
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
fetchImpl: async (url, init) => {
|
|
56
|
+
calls.push({ url: String(url), init })
|
|
57
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 })
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
reporter.track("message_sent")
|
|
62
|
+
await (reporter as any).queue
|
|
63
|
+
|
|
64
|
+
expect(calls).toHaveLength(1)
|
|
65
|
+
expect(calls[0]?.url).toBe("https://kanna.sh/api/t")
|
|
66
|
+
expect(calls[0]?.init?.method).toBe("POST")
|
|
67
|
+
expect(calls[0]?.init?.headers).toEqual({
|
|
68
|
+
"content-type": "application/json",
|
|
69
|
+
})
|
|
70
|
+
expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({
|
|
71
|
+
userId: "anon_123",
|
|
72
|
+
environment: "dev",
|
|
73
|
+
event: {
|
|
74
|
+
name: "message_sent",
|
|
75
|
+
properties: {
|
|
76
|
+
current_version: "0.33.9",
|
|
77
|
+
environment: "dev",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
} finally {
|
|
82
|
+
console.log = originalLog
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test("posts app_launch with launch flags as properties", async () => {
|
|
87
|
+
const calls: Array<{ url: string; init?: RequestInit }> = []
|
|
88
|
+
const reporter = new KannaAnalyticsReporter({
|
|
89
|
+
endpoint: "https://kanna.sh/api/t",
|
|
90
|
+
currentVersion: "0.33.9",
|
|
91
|
+
environment: "prod",
|
|
92
|
+
settings: {
|
|
93
|
+
getState: () => ({
|
|
94
|
+
analyticsEnabled: true,
|
|
95
|
+
analyticsUserId: "anon_123",
|
|
96
|
+
warning: null,
|
|
97
|
+
filePathDisplay: "~/.kanna/data/settings.json",
|
|
98
|
+
}),
|
|
99
|
+
},
|
|
100
|
+
fetchImpl: async (url, init) => {
|
|
101
|
+
calls.push({ url: String(url), init })
|
|
102
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 })
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
reporter.trackLaunch({
|
|
107
|
+
port: 4000,
|
|
108
|
+
host: "localhost",
|
|
109
|
+
openBrowser: false,
|
|
110
|
+
share: false,
|
|
111
|
+
password: null,
|
|
112
|
+
strictPort: true,
|
|
113
|
+
})
|
|
114
|
+
await (reporter as any).queue
|
|
115
|
+
|
|
116
|
+
expect(JSON.parse(String(calls[0]?.init?.body))).toEqual({
|
|
117
|
+
userId: "anon_123",
|
|
118
|
+
environment: "prod",
|
|
119
|
+
event: {
|
|
120
|
+
name: "app_launch",
|
|
121
|
+
properties: {
|
|
122
|
+
current_version: "0.33.9",
|
|
123
|
+
environment: "prod",
|
|
124
|
+
custom_port_enabled: true,
|
|
125
|
+
no_open_enabled: true,
|
|
126
|
+
password_enabled: false,
|
|
127
|
+
strict_port_enabled: true,
|
|
128
|
+
remote_enabled: false,
|
|
129
|
+
host_enabled: false,
|
|
130
|
+
share_quick_enabled: false,
|
|
131
|
+
share_token_enabled: false,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test("skips requests when analytics is disabled", async () => {
|
|
138
|
+
let called = false
|
|
139
|
+
const reporter = new KannaAnalyticsReporter({
|
|
140
|
+
currentVersion: "0.33.9",
|
|
141
|
+
environment: "prod",
|
|
142
|
+
settings: {
|
|
143
|
+
getState: () => ({
|
|
144
|
+
analyticsEnabled: false,
|
|
145
|
+
analyticsUserId: "anon_123",
|
|
146
|
+
warning: null,
|
|
147
|
+
filePathDisplay: "~/.kanna/data/settings.json",
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
150
|
+
fetchImpl: async () => {
|
|
151
|
+
called = true
|
|
152
|
+
return new Response(null, { status: 200 })
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
reporter.track("message_sent")
|
|
157
|
+
await (reporter as any).queue
|
|
158
|
+
|
|
159
|
+
expect(called).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test("does not warn when analytics request logging is disabled and the request fails", async () => {
|
|
163
|
+
const originalWarn = console.warn
|
|
164
|
+
const warnings: unknown[][] = []
|
|
165
|
+
console.warn = (...args: unknown[]) => {
|
|
166
|
+
warnings.push(args)
|
|
167
|
+
}
|
|
168
|
+
delete process.env.KANNA_LOG_ANALYTICS
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const reporter = new KannaAnalyticsReporter({
|
|
172
|
+
endpoint: "https://kanna.sh/api/t",
|
|
173
|
+
currentVersion: "0.33.9",
|
|
174
|
+
environment: "dev",
|
|
175
|
+
settings: {
|
|
176
|
+
getState: () => ({
|
|
177
|
+
analyticsEnabled: true,
|
|
178
|
+
analyticsUserId: "anon_123",
|
|
179
|
+
warning: null,
|
|
180
|
+
filePathDisplay: "~/.kanna/data/settings.json",
|
|
181
|
+
}),
|
|
182
|
+
},
|
|
183
|
+
fetchImpl: async () => new Response(JSON.stringify({ error: "bad request" }), { status: 400 }),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
reporter.track("message_sent")
|
|
187
|
+
await (reporter as any).queue
|
|
188
|
+
|
|
189
|
+
expect(warnings).toHaveLength(0)
|
|
190
|
+
} finally {
|
|
191
|
+
console.warn = originalWarn
|
|
192
|
+
restoreAnalyticsLoggingEnv()
|
|
193
|
+
}
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test("warns when analytics request logging is enabled and the request fails", async () => {
|
|
197
|
+
const originalWarn = console.warn
|
|
198
|
+
const warnings: unknown[][] = []
|
|
199
|
+
console.warn = (...args: unknown[]) => {
|
|
200
|
+
warnings.push(args)
|
|
201
|
+
}
|
|
202
|
+
process.env.KANNA_LOG_ANALYTICS = "1"
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const reporter = new KannaAnalyticsReporter({
|
|
206
|
+
endpoint: "https://kanna.sh/api/t",
|
|
207
|
+
currentVersion: "0.33.9",
|
|
208
|
+
environment: "prod",
|
|
209
|
+
settings: {
|
|
210
|
+
getState: () => ({
|
|
211
|
+
analyticsEnabled: true,
|
|
212
|
+
analyticsUserId: "anon_123",
|
|
213
|
+
warning: null,
|
|
214
|
+
filePathDisplay: "~/.kanna/data/settings.json",
|
|
215
|
+
}),
|
|
216
|
+
},
|
|
217
|
+
fetchImpl: async () => new Response(JSON.stringify({ error: "bad request" }), { status: 400 }),
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
reporter.track("message_sent")
|
|
221
|
+
await (reporter as any).queue
|
|
222
|
+
|
|
223
|
+
expect(warnings).toHaveLength(1)
|
|
224
|
+
expect(warnings[0]?.[0]).toBe("[kanna/analytics] Failed to send analytics event:")
|
|
225
|
+
expect(warnings[0]?.[1]).toBe("message_sent")
|
|
226
|
+
expect(warnings[0]?.[2]).toBeInstanceOf(Error)
|
|
227
|
+
} finally {
|
|
228
|
+
console.warn = originalWarn
|
|
229
|
+
restoreAnalyticsLoggingEnv()
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test("logs when analytics request logging is enabled and the request succeeds", async () => {
|
|
234
|
+
const originalLog = console.log
|
|
235
|
+
const logs: unknown[][] = []
|
|
236
|
+
console.log = (...args: unknown[]) => {
|
|
237
|
+
logs.push(args)
|
|
238
|
+
}
|
|
239
|
+
process.env.KANNA_LOG_ANALYTICS = "1"
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const reporter = new KannaAnalyticsReporter({
|
|
243
|
+
endpoint: "https://kanna.sh/api/t",
|
|
244
|
+
currentVersion: "0.33.9",
|
|
245
|
+
environment: "dev",
|
|
246
|
+
settings: {
|
|
247
|
+
getState: () => ({
|
|
248
|
+
analyticsEnabled: true,
|
|
249
|
+
analyticsUserId: "anon_123",
|
|
250
|
+
warning: null,
|
|
251
|
+
filePathDisplay: "~/.kanna/data/settings.json",
|
|
252
|
+
}),
|
|
253
|
+
},
|
|
254
|
+
fetchImpl: async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
reporter.track("message_sent")
|
|
258
|
+
await (reporter as any).queue
|
|
259
|
+
|
|
260
|
+
expect(logs).toHaveLength(1)
|
|
261
|
+
expect(logs[0]).toEqual([
|
|
262
|
+
"[kanna/analytics] Sent analytics event:",
|
|
263
|
+
"message_sent",
|
|
264
|
+
200,
|
|
265
|
+
])
|
|
266
|
+
} finally {
|
|
267
|
+
console.log = originalLog
|
|
268
|
+
restoreAnalyticsLoggingEnv()
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test("does not log when analytics request logging is disabled and the request succeeds", async () => {
|
|
273
|
+
const originalLog = console.log
|
|
274
|
+
const logs: unknown[][] = []
|
|
275
|
+
console.log = (...args: unknown[]) => {
|
|
276
|
+
logs.push(args)
|
|
277
|
+
}
|
|
278
|
+
delete process.env.KANNA_LOG_ANALYTICS
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const reporter = new KannaAnalyticsReporter({
|
|
282
|
+
endpoint: "https://kanna.sh/api/t",
|
|
283
|
+
currentVersion: "0.33.9",
|
|
284
|
+
environment: "prod",
|
|
285
|
+
settings: {
|
|
286
|
+
getState: () => ({
|
|
287
|
+
analyticsEnabled: true,
|
|
288
|
+
analyticsUserId: "anon_123",
|
|
289
|
+
warning: null,
|
|
290
|
+
filePathDisplay: "~/.kanna/data/settings.json",
|
|
291
|
+
}),
|
|
292
|
+
},
|
|
293
|
+
fetchImpl: async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
reporter.track("message_sent")
|
|
297
|
+
await (reporter as any).queue
|
|
298
|
+
|
|
299
|
+
expect(logs).toHaveLength(0)
|
|
300
|
+
} finally {
|
|
301
|
+
console.log = originalLog
|
|
302
|
+
restoreAnalyticsLoggingEnv()
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { ANALYTICS_ENDPOINT } from "../shared/analytics"
|
|
2
|
+
import { PROD_SERVER_PORT } from "../shared/ports"
|
|
3
|
+
import type { ShareMode } from "../shared/share"
|
|
4
|
+
import { isTokenShareMode } from "../shared/share"
|
|
5
|
+
import type { AppSettingsManager } from "./app-settings"
|
|
6
|
+
|
|
7
|
+
interface AnalyticsRequestBody {
|
|
8
|
+
userId: string
|
|
9
|
+
environment: AnalyticsEnvironment
|
|
10
|
+
event: {
|
|
11
|
+
name: string
|
|
12
|
+
properties: Record<string, unknown>
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LaunchAnalyticsOptions {
|
|
17
|
+
port: number
|
|
18
|
+
host: string
|
|
19
|
+
openBrowser: boolean
|
|
20
|
+
share: ShareMode
|
|
21
|
+
password: string | null
|
|
22
|
+
strictPort: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>
|
|
26
|
+
type AnalyticsEnvironment = "dev" | "prod"
|
|
27
|
+
|
|
28
|
+
function isAnalyticsLoggingEnabled() {
|
|
29
|
+
return process.env.KANNA_LOG_ANALYTICS === "1"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AnalyticsReporter {
|
|
33
|
+
track: (eventName: string, properties?: Record<string, unknown>) => void
|
|
34
|
+
trackLaunch: (options: LaunchAnalyticsOptions) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class KannaAnalyticsReporter implements AnalyticsReporter {
|
|
38
|
+
private readonly settings: Pick<AppSettingsManager, "getState">
|
|
39
|
+
private readonly endpoint: string
|
|
40
|
+
private readonly fetchImpl: FetchLike
|
|
41
|
+
private readonly currentVersion: string
|
|
42
|
+
private readonly environment: AnalyticsEnvironment
|
|
43
|
+
private queue = Promise.resolve()
|
|
44
|
+
|
|
45
|
+
constructor(args: {
|
|
46
|
+
settings: Pick<AppSettingsManager, "getState">
|
|
47
|
+
currentVersion: string
|
|
48
|
+
environment: AnalyticsEnvironment
|
|
49
|
+
endpoint?: string
|
|
50
|
+
fetchImpl?: FetchLike
|
|
51
|
+
}) {
|
|
52
|
+
this.settings = args.settings
|
|
53
|
+
this.currentVersion = args.currentVersion
|
|
54
|
+
this.environment = args.environment
|
|
55
|
+
this.endpoint = args.endpoint ?? ANALYTICS_ENDPOINT
|
|
56
|
+
this.fetchImpl = args.fetchImpl ?? fetch
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
track(eventName: string, properties?: Record<string, unknown>) {
|
|
60
|
+
const { analyticsEnabled, analyticsUserId } = this.settings.getState()
|
|
61
|
+
if (!analyticsEnabled || !analyticsUserId) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const body: AnalyticsRequestBody = {
|
|
66
|
+
userId: analyticsUserId,
|
|
67
|
+
environment: this.environment,
|
|
68
|
+
event: {
|
|
69
|
+
name: eventName,
|
|
70
|
+
properties: this.buildEventProperties(properties),
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.queue = this.queue
|
|
75
|
+
.then(async () => {
|
|
76
|
+
const response = await this.fetchImpl(this.endpoint, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: {
|
|
79
|
+
"content-type": "application/json",
|
|
80
|
+
},
|
|
81
|
+
body: JSON.stringify(body),
|
|
82
|
+
})
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(`Analytics request failed with status ${response.status}`)
|
|
85
|
+
}
|
|
86
|
+
if (isAnalyticsLoggingEnabled()) {
|
|
87
|
+
console.log("[kanna/analytics] Sent analytics event:", eventName, response.status)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.catch((error) => {
|
|
91
|
+
if (isAnalyticsLoggingEnabled()) {
|
|
92
|
+
console.warn("[kanna/analytics] Failed to send analytics event:", eventName, error)
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
trackLaunch(options: LaunchAnalyticsOptions) {
|
|
98
|
+
this.track("app_launch", getLaunchAnalyticsProperties(options))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private buildEventProperties(properties?: Record<string, unknown>) {
|
|
102
|
+
return {
|
|
103
|
+
current_version: this.currentVersion,
|
|
104
|
+
environment: this.environment,
|
|
105
|
+
...(properties ?? {}),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getLaunchAnalyticsProperties(options: LaunchAnalyticsOptions) {
|
|
111
|
+
return {
|
|
112
|
+
custom_port_enabled: options.port !== PROD_SERVER_PORT,
|
|
113
|
+
no_open_enabled: !options.openBrowser,
|
|
114
|
+
password_enabled: Boolean(options.password),
|
|
115
|
+
strict_port_enabled: options.strictPort,
|
|
116
|
+
remote_enabled: options.host === "0.0.0.0",
|
|
117
|
+
host_enabled: options.host !== "0.0.0.0" && options.host !== "127.0.0.1" && options.host !== "localhost",
|
|
118
|
+
share_quick_enabled: options.share === "quick",
|
|
119
|
+
share_token_enabled: isTokenShareMode(options.share),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const NoopAnalyticsReporter: AnalyticsReporter = {
|
|
124
|
+
track: () => {},
|
|
125
|
+
trackLaunch: () => {},
|
|
126
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { AppSettingsManager, readAppSettingsSnapshot } from "./app-settings"
|
|
6
|
+
|
|
7
|
+
let tempDirs: string[] = []
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
await Promise.all(tempDirs.map((dir) => rm(dir, { recursive: true, force: true })))
|
|
11
|
+
tempDirs = []
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
async function createTempFilePath() {
|
|
15
|
+
const dir = await mkdtemp(path.join(tmpdir(), "kanna-settings-"))
|
|
16
|
+
tempDirs.push(dir)
|
|
17
|
+
return path.join(dir, "settings.json")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("readAppSettingsSnapshot", () => {
|
|
21
|
+
test("returns defaults when the file does not exist", async () => {
|
|
22
|
+
const filePath = await createTempFilePath()
|
|
23
|
+
const snapshot = await readAppSettingsSnapshot(filePath)
|
|
24
|
+
|
|
25
|
+
expect(snapshot).toEqual({
|
|
26
|
+
analyticsEnabled: true,
|
|
27
|
+
warning: null,
|
|
28
|
+
filePathDisplay: filePath,
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("returns a warning when the file contains invalid json", async () => {
|
|
33
|
+
const filePath = await createTempFilePath()
|
|
34
|
+
await writeFile(filePath, "{not-json", "utf8")
|
|
35
|
+
|
|
36
|
+
const snapshot = await readAppSettingsSnapshot(filePath)
|
|
37
|
+
expect(snapshot.analyticsEnabled).toBe(true)
|
|
38
|
+
expect(snapshot.warning).toContain("invalid JSON")
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
describe("AppSettingsManager", () => {
|
|
43
|
+
test("creates a settings file with analytics enabled and a stable anonymous id", async () => {
|
|
44
|
+
const filePath = await createTempFilePath()
|
|
45
|
+
const manager = new AppSettingsManager(filePath)
|
|
46
|
+
|
|
47
|
+
await manager.initialize()
|
|
48
|
+
|
|
49
|
+
const payload = JSON.parse(await readFile(filePath, "utf8")) as {
|
|
50
|
+
analyticsEnabled: boolean
|
|
51
|
+
analyticsUserId: string
|
|
52
|
+
}
|
|
53
|
+
expect(payload.analyticsEnabled).toBe(true)
|
|
54
|
+
expect(payload.analyticsUserId).toMatch(/^anon_/)
|
|
55
|
+
expect(manager.getSnapshot()).toEqual({
|
|
56
|
+
analyticsEnabled: true,
|
|
57
|
+
warning: null,
|
|
58
|
+
filePathDisplay: filePath,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
manager.dispose()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("writes analyticsEnabled without replacing the stored user id", async () => {
|
|
65
|
+
const filePath = await createTempFilePath()
|
|
66
|
+
const manager = new AppSettingsManager(filePath)
|
|
67
|
+
|
|
68
|
+
await manager.initialize()
|
|
69
|
+
const initialPayload = JSON.parse(await readFile(filePath, "utf8")) as {
|
|
70
|
+
analyticsEnabled: boolean
|
|
71
|
+
analyticsUserId: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const snapshot = await manager.write({ analyticsEnabled: false })
|
|
75
|
+
const nextPayload = JSON.parse(await readFile(filePath, "utf8")) as {
|
|
76
|
+
analyticsEnabled: boolean
|
|
77
|
+
analyticsUserId: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
expect(snapshot).toEqual({
|
|
81
|
+
analyticsEnabled: false,
|
|
82
|
+
warning: null,
|
|
83
|
+
filePathDisplay: filePath,
|
|
84
|
+
})
|
|
85
|
+
expect(nextPayload.analyticsEnabled).toBe(false)
|
|
86
|
+
expect(nextPayload.analyticsUserId).toBe(initialPayload.analyticsUserId)
|
|
87
|
+
|
|
88
|
+
manager.dispose()
|
|
89
|
+
})
|
|
90
|
+
})
|