fix(telemetry): validate activity state shape

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-11 21:25:27 +09:00
parent acb0f6b006
commit e2f304f1a8
2 changed files with 130 additions and 1 deletions
+122
View File
@@ -0,0 +1,122 @@
import { afterEach, describe, expect, it } from "bun:test"
import { mkdirSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { tmpdir } from "node:os"
const originalXdgDataHome = process.env.XDG_DATA_HOME
function createDataHomePath(): string {
return join(tmpdir(), `posthog-activity-state-${Date.now()}-${Math.random()}`)
}
async function importPostHogActivityStateModule(): Promise<typeof import("./posthog-activity-state")> {
return import(`./posthog-activity-state?test=${Date.now()}-${Math.random()}`)
}
afterEach(() => {
if (originalXdgDataHome === undefined) {
delete process.env.XDG_DATA_HOME
} else {
process.env.XDG_DATA_HOME = originalXdgDataHome
}
})
describe("getPostHogActivityCaptureState", () => {
it("returns default state when activity file contains null", async () => {
// given
const dataHomePath = createDataHomePath()
const cachePath = join(dataHomePath, "oh-my-opencode")
mkdirSync(cachePath, { recursive: true })
writeFileSync(join(cachePath, "posthog-activity.json"), "null\n")
process.env.XDG_DATA_HOME = dataHomePath
const { getPostHogActivityCaptureState } = await importPostHogActivityStateModule()
// when
const result = getPostHogActivityCaptureState(new Date("2026-04-11T10:15:00.000Z"))
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
captureDaily: true,
captureHourly: true,
})
rmSync(dataHomePath, { recursive: true, force: true })
})
it("returns default state when activity file contains an array", async () => {
// given
const dataHomePath = createDataHomePath()
const cachePath = join(dataHomePath, "oh-my-opencode")
mkdirSync(cachePath, { recursive: true })
writeFileSync(join(cachePath, "posthog-activity.json"), "[]\n")
process.env.XDG_DATA_HOME = dataHomePath
const { getPostHogActivityCaptureState } = await importPostHogActivityStateModule()
// when
const result = getPostHogActivityCaptureState(new Date("2026-04-11T10:15:00.000Z"))
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
captureDaily: true,
captureHourly: true,
})
rmSync(dataHomePath, { recursive: true, force: true })
})
it("returns default state when activity file contains a number", async () => {
// given
const dataHomePath = createDataHomePath()
const cachePath = join(dataHomePath, "oh-my-opencode")
mkdirSync(cachePath, { recursive: true })
writeFileSync(join(cachePath, "posthog-activity.json"), "42\n")
process.env.XDG_DATA_HOME = dataHomePath
const { getPostHogActivityCaptureState } = await importPostHogActivityStateModule()
// when
const result = getPostHogActivityCaptureState(new Date("2026-04-11T10:15:00.000Z"))
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
captureDaily: true,
captureHourly: true,
})
rmSync(dataHomePath, { recursive: true, force: true })
})
it("reads valid activity state JSON", async () => {
// given
const dataHomePath = createDataHomePath()
const cachePath = join(dataHomePath, "oh-my-opencode")
mkdirSync(cachePath, { recursive: true })
writeFileSync(
join(cachePath, "posthog-activity.json"),
`${JSON.stringify({
lastActiveDayUTC: "2026-04-11",
lastActiveHourUTC: "2026-04-11T10",
})}\n`,
)
process.env.XDG_DATA_HOME = dataHomePath
const { getPostHogActivityCaptureState } = await importPostHogActivityStateModule()
// when
const result = getPostHogActivityCaptureState(new Date("2026-04-11T10:15:00.000Z"))
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
captureDaily: false,
captureHourly: false,
})
rmSync(dataHomePath, { recursive: true, force: true })
})
})
+8 -1
View File
@@ -32,6 +32,10 @@ function getUtcHourString(date: Date): string {
return date.toISOString().slice(0, 13)
}
function isPostHogActivityState(value: unknown): value is PostHogActivityState {
return value !== null && typeof value === "object" && !Array.isArray(value)
}
function readPostHogActivityState(): PostHogActivityState {
const stateFilePath = getPostHogActivityStateFilePath()
if (!existsSync(stateFilePath)) {
@@ -40,7 +44,10 @@ function readPostHogActivityState(): PostHogActivityState {
try {
const content = readFileSync(stateFilePath, "utf-8")
const parsed = JSON.parse(content) as PostHogActivityState
const parsed: unknown = JSON.parse(content)
if (!isPostHogActivityState(parsed)) {
return {}
}
return parsed
} catch (error) {
log("[posthog-activity-state] Failed to read activity state", {