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:
@@ -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 })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -32,6 +32,10 @@ function getUtcHourString(date: Date): string {
|
|||||||
return date.toISOString().slice(0, 13)
|
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 {
|
function readPostHogActivityState(): PostHogActivityState {
|
||||||
const stateFilePath = getPostHogActivityStateFilePath()
|
const stateFilePath = getPostHogActivityStateFilePath()
|
||||||
if (!existsSync(stateFilePath)) {
|
if (!existsSync(stateFilePath)) {
|
||||||
@@ -40,7 +44,10 @@ function readPostHogActivityState(): PostHogActivityState {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const content = readFileSync(stateFilePath, "utf-8")
|
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
|
return parsed
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log("[posthog-activity-state] Failed to read activity state", {
|
log("[posthog-activity-state] Failed to read activity state", {
|
||||||
|
|||||||
Reference in New Issue
Block a user