Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## 0.0.11-beta.3 — 2026-05-25

### Features
- Stamp `product: "failproofai-oss"` on every PostHog event across all four telemetry channels — hooks/audit (`trackHookEvent`), server (`trackEvent`), web UI (`captureClientEvent`), and npm-lifecycle install/uninstall (`trackInstallEvent`) — so OSS events stay distinguishable from any future hosted surface. The value lives in a single `POSTHOG_PRODUCT` constant in `src/posthog-key.ts`, reused by the three TypeScript channels; the standalone `scripts/install-telemetry.mjs` inlines the same literal because it can't import the TS module at install time. Honors `FAILPROOFAI_TELEMETRY_DISABLED=1` like all other telemetry (#380).

## 0.0.11-beta.2 — 2026-05-21

### Features
Expand Down
50 changes: 50 additions & 0 deletions __tests__/hooks/hook-telemetry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// @vitest-environment node
/**
* Real-payload coverage for the hook-binary telemetry choke point.
* Every hook + audit event flows through trackHookEvent, so asserting the
* fetch body here guarantees `product: failproofai-oss` is stamped on all of them.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { trackHookEvent } from "../../src/hooks/hook-telemetry";

describe("hook-telemetry trackHookEvent", () => {
let fetchSpy: ReturnType<typeof vi.fn>;
const originalEnv = { ...process.env };

beforeEach(() => {
fetchSpy = vi.fn().mockResolvedValue(new Response("{}", { status: 200 }));
vi.stubGlobal("fetch", fetchSpy);
delete process.env.FAILPROOFAI_TELEMETRY_DISABLED;
});

afterEach(() => {
vi.unstubAllGlobals();
process.env = { ...originalEnv };
});

it("stamps product: failproofai-oss on every event", async () => {
await trackHookEvent("inst-id", "hooks_installed", { count: 1 });

expect(fetchSpy).toHaveBeenCalledOnce();
const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
expect(body.event).toBe("hooks_installed");
expect(body.distinct_id).toBe("inst-id");
expect(body.properties.product).toBe("failproofai-oss");
expect(body.properties.$lib).toBe("failproofai-hooks");
expect(body.properties.failproofai_version).toEqual(expect.any(String));
expect(body.properties.count).toBe(1);
});

it("stamps product even when no extra properties are passed", async () => {
await trackHookEvent("inst-id", "audit_started");

const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
expect(body.properties.product).toBe("failproofai-oss");
});

it("is a no-op when telemetry is disabled", async () => {
process.env.FAILPROOFAI_TELEMETRY_DISABLED = "1";
await trackHookEvent("inst-id", "hooks_installed");
expect(fetchSpy).not.toHaveBeenCalled();
});
});
1 change: 1 addition & 0 deletions __tests__/lib/client-telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ describe("client-telemetry", () => {
expect(body.distinct_id).toBe("test-id");
expect(body.properties.$lib).toBe("failproofai-web");
expect(body.properties.failproofai_version).toBe("1.0.0-test");
expect(body.properties.product).toBe("failproofai-oss");
expect(body.properties.extra).toBe("prop");
});

Expand Down
4 changes: 2 additions & 2 deletions __tests__/lib/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ describe("lib/telemetry", () => {
expect(mockCapture).toHaveBeenCalledWith({
distinctId: "test-instance-id",
event: "feature_used",
properties: expect.objectContaining({ feature: "dashboard", $lib: "failproofai", failproofai_version: expect.any(String) }),
properties: expect.objectContaining({ feature: "dashboard", $lib: "failproofai", failproofai_version: expect.any(String), product: "failproofai-oss" }),
});
});

Expand All @@ -132,7 +132,7 @@ describe("lib/telemetry", () => {
expect(mockCapture).toHaveBeenCalledWith({
distinctId: "test-instance-id",
event: "app_started",
properties: { $lib: "failproofai", failproofai_version: expect.any(String) },
properties: { $lib: "failproofai", failproofai_version: expect.any(String), product: "failproofai-oss" },
});
});
});
Expand Down
43 changes: 43 additions & 0 deletions __tests__/scripts/install-telemetry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @vitest-environment node
/**
* Real-payload coverage for the npm-lifecycle telemetry choke point.
* package_installed / package_uninstalled both flow through trackInstallEvent,
* so asserting the fetch body here guarantees `product: failproofai-oss` is
* stamped on install/uninstall events too.
*/
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { trackInstallEvent } from "../../scripts/install-telemetry.mjs";

describe("install-telemetry trackInstallEvent", () => {
let fetchSpy: ReturnType<typeof vi.fn>;
const originalEnv = { ...process.env };

beforeEach(() => {
fetchSpy = vi.fn().mockResolvedValue(new Response("{}", { status: 200 }));
vi.stubGlobal("fetch", fetchSpy);
delete process.env.FAILPROOFAI_TELEMETRY_DISABLED;
process.env.npm_package_version = "9.9.9-test";
});

afterEach(() => {
vi.unstubAllGlobals();
process.env = { ...originalEnv };
});

it("stamps product: failproofai-oss on every event", async () => {
await trackInstallEvent("package_installed", { hooks_configured: true });

expect(fetchSpy).toHaveBeenCalledOnce();
const body = JSON.parse(fetchSpy.mock.calls[0][1].body);
expect(body.event).toBe("package_installed");
expect(body.properties.product).toBe("failproofai-oss");
expect(body.properties.$lib).toBe("failproofai-install");
expect(body.properties.hooks_configured).toBe(true);
});

it("is a no-op when telemetry is disabled", async () => {
process.env.FAILPROOFAI_TELEMETRY_DISABLED = "1";
await trackInstallEvent("package_installed");
expect(fetchSpy).not.toHaveBeenCalled();
});
});
2 changes: 2 additions & 0 deletions lib/client-telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { TelemetryConfig } from "@/app/actions/get-telemetry-config";
import { POSTHOG_PRODUCT } from "@/src/posthog-key";

let config: TelemetryConfig | null = null;

Expand All @@ -21,6 +22,7 @@ export function captureClientEvent(
...properties,
$lib: "failproofai-web",
failproofai_version: config.version,
product: POSTHOG_PRODUCT,
$current_url: window.location.href,
$pathname: window.location.pathname,
},
Expand Down
4 changes: 2 additions & 2 deletions lib/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import { getInstanceId } from "./telemetry-id";
import { version } from "../package.json";
import { POSTHOG_API_KEY } from "../src/posthog-key";
import { POSTHOG_API_KEY, POSTHOG_PRODUCT } from "../src/posthog-key";

const DEFAULT_API_KEY = POSTHOG_API_KEY;
const DEFAULT_HOST = "https://us.i.posthog.com";
Expand Down Expand Up @@ -129,7 +129,7 @@ export function trackEvent(
client.capture({
distinctId: getInstanceId(),
event: name,
properties: { ...properties, $lib: "failproofai", failproofai_version: version },
properties: { ...properties, $lib: "failproofai", failproofai_version: version, product: POSTHOG_PRODUCT },
});
}

Expand Down
4 changes: 4 additions & 0 deletions scripts/install-telemetry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { join } from "node:path";
const NAMESPACE = "failproofai-telemetry-v1";
const API_KEY = "phc_Ac1Ww1GqKc0z1SyrRWbmatEeQdlOQIsDEEdP8l8JRgX";
const CAPTURE_URL = "https://us.i.posthog.com/capture/";
// Mirrors POSTHOG_PRODUCT in src/posthog-key.ts — this standalone npm-lifecycle
// script can't import the TS module at install time, so the literal is inlined.
const PRODUCT = "failproofai-oss";

function hashToId(raw) {
return createHmac("sha256", NAMESPACE).update(raw).digest("hex");
Expand Down Expand Up @@ -91,6 +94,7 @@ export async function trackInstallEvent(event, properties = {}) {
...properties,
$lib: "failproofai-install",
failproofai_version: version,
product: PRODUCT,
},
});

Expand Down
4 changes: 2 additions & 2 deletions src/hooks/hook-telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { version } from "../../package.json";
import { POSTHOG_API_KEY } from "../posthog-key";
import { POSTHOG_API_KEY, POSTHOG_PRODUCT } from "../posthog-key";

const API_KEY = POSTHOG_API_KEY;
const CAPTURE_URL = "https://us.i.posthog.com/capture/";
Expand All @@ -22,7 +22,7 @@ export async function trackHookEvent(
api_key: process.env.FAILPROOFAI_POSTHOG_KEY ?? API_KEY,
event,
distinct_id: distinctId,
properties: { ...properties, $lib: "failproofai-hooks", failproofai_version: version },
properties: { ...properties, $lib: "failproofai-hooks", failproofai_version: version, product: POSTHOG_PRODUCT },
});

try {
Expand Down
9 changes: 9 additions & 0 deletions src/posthog-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,12 @@
* and the compiled hook binary telemetry. Write-only (safe to commit).
*/
export const POSTHOG_API_KEY = "phc_Ac1Ww1GqKc0z1SyrRWbmatEeQdlOQIsDEEdP8l8JRgX";

/**
* Product identifier attached to the `product` property of every PostHog
* event, across all telemetry channels (hooks, web, server, install).
* Single source of truth so the value never drifts between channels.
* The standalone npm-lifecycle script (scripts/install-telemetry.mjs) can't
* import this TS module at install time, so it inlines the same literal.
*/
export const POSTHOG_PRODUCT = "failproofai-oss";