From 651c7f936f3ef253ce52be41567fd8cfb230a7b9 Mon Sep 17 00:00:00 2001 From: digua Date: Tue, 23 Jun 2026 22:31:30 +0800 Subject: [PATCH] fix(analytics): retry daily activity after failed posts --- .../src/services/analytics.test.ts | 79 +++++++++++++++++++ .../node-runtime/src/services/analytics.ts | 13 +-- 2 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 packages/node-runtime/src/services/analytics.test.ts diff --git a/packages/node-runtime/src/services/analytics.test.ts b/packages/node-runtime/src/services/analytics.test.ts new file mode 100644 index 00000000..bcef3d25 --- /dev/null +++ b/packages/node-runtime/src/services/analytics.test.ts @@ -0,0 +1,79 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { AnalyticsService } from './analytics' + +const APP_KEY = 'app-US-test' + +function createTempSystemDir(): string { + return mkdtempSync(join(tmpdir(), 'chatlab-analytics-')) +} + +function readAnalyticsData(systemDir: string): { + firstReportDate?: string | null + lastReportDate?: string | null +} { + return JSON.parse(readFileSync(join(systemDir, 'analytics.json'), 'utf-8')) +} + +test('trackDailyActive does not mark the day as reported when fetch rejects', async () => { + const systemDir = createTempSystemDir() + const originalFetch = globalThis.fetch + const originalConsoleError = console.error + globalThis.fetch = (() => Promise.reject(new Error('offline'))) as typeof fetch + console.error = () => {} + + try { + await new AnalyticsService(systemDir, APP_KEY, '1.0.0').trackDailyActive() + + assert.equal(existsSync(join(systemDir, 'analytics.json')), false) + } finally { + console.error = originalConsoleError + globalThis.fetch = originalFetch + rmSync(systemDir, { recursive: true, force: true }) + } +}) + +test('trackDailyActive does not mark the day as reported when Aptabase returns non-OK', async () => { + const systemDir = createTempSystemDir() + const originalFetch = globalThis.fetch + globalThis.fetch = (() => Promise.resolve(new Response(null, { status: 500 }))) as typeof fetch + + try { + await new AnalyticsService(systemDir, APP_KEY, '1.0.0').trackDailyActive() + + assert.equal(existsSync(join(systemDir, 'analytics.json')), false) + } finally { + globalThis.fetch = originalFetch + rmSync(systemDir, { recursive: true, force: true }) + } +}) + +test('trackDailyActive marks the day as reported after a successful post', async () => { + const systemDir = createTempSystemDir() + const originalFetch = globalThis.fetch + const requests: Array<{ url: string; init?: RequestInit }> = [] + globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { + requests.push({ url: String(input), init }) + return Promise.resolve(new Response(null, { status: 204 })) + }) as typeof fetch + + try { + await new AnalyticsService(systemDir, APP_KEY, '1.0.0').trackDailyActive({ platform: 'cli-web' }) + + const today = new Date().toISOString().slice(0, 10) + assert.equal(requests.length, 1) + assert.equal(requests[0].url, 'https://us.aptabase.com/api/v0/event') + assert.equal(JSON.parse(String(requests[0].init?.body)).eventName, 'app_active_new') + assert.deepEqual(readAnalyticsData(systemDir), { + enabled: true, + firstReportDate: today, + lastReportDate: today, + }) + } finally { + globalThis.fetch = originalFetch + rmSync(systemDir, { recursive: true, force: true }) + } +}) diff --git a/packages/node-runtime/src/services/analytics.ts b/packages/node-runtime/src/services/analytics.ts index 9de9bad0..00fc3488 100644 --- a/packages/node-runtime/src/services/analytics.ts +++ b/packages/node-runtime/src/services/analytics.ts @@ -62,11 +62,11 @@ export class AnalyticsService { this.save(data) } - async track(eventName: string, props?: Record): Promise { - if (!this.endpoint) return - if (!this.getEnabled()) return + async track(eventName: string, props?: Record): Promise { + if (!this.endpoint) return false + if (!this.getEnabled()) return false try { - await fetch(this.endpoint, { + const resp = await fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'App-Key': this.appKey }, body: JSON.stringify({ @@ -86,8 +86,10 @@ export class AnalyticsService { props, }), }) + return resp.ok } catch (e) { console.error(`[Analytics] Failed to track ${eventName}:`, e) + return false } } @@ -102,7 +104,8 @@ export class AnalyticsService { if (isNew) this.save(data) return } - await this.track(isNew ? 'app_active_new' : 'app_active', props) + const tracked = await this.track(isNew ? 'app_active_new' : 'app_active', props) + if (!tracked) return data.lastReportDate = today this.save(data) }