Skip to content

Commit 2cf4e5a

Browse files
authored
ENG-1940 add per install roam extension version telemetry (#1151)
* Update cache control max age in deploy script from 1 day to 60 seconds for improved caching behavior * Enhance loading toast to display version information with date. Integrate version retrieval from utility function for improved user feedback during development. * Refactor PostHog integration to enhance telemetry data collection. Introduced a utility for local storage access and streamlined session telemetry registration with version metadata, improving overall analytics accuracy. * .
1 parent 11f6178 commit 2cf4e5a

4 files changed

Lines changed: 222 additions & 12 deletions

File tree

apps/roam/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import {
4747
} from "./components/settings/utils/settingsEmitter";
4848
import { mountLeftSidebar } from "./components/LeftSidebarView";
4949
import { initDockedSearchSidebarPersistence } from "~/components/AdvancedNodeSearchDialog/mountAdvancedSearchInSidebar";
50+
import { getVersionWithDate } from "./utils/getVersion";
5051

5152
export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*";
5253

@@ -74,9 +75,10 @@ export default runExtension(async (onloadArgs) => {
7475
}
7576

7677
if (process.env.NODE_ENV === "development") {
78+
const { version } = getVersionWithDate();
7779
renderToast({
7880
id: "discourse-graph-loaded",
79-
content: "Successfully loaded",
81+
content: `Successfully loaded v${version}`,
8082
intent: "success",
8183
timeout: 500,
8284
});
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
buildExtensionTelemetryProperties,
4+
getOrCreateExtensionInstallId,
5+
} from "~/utils/extensionTelemetry";
6+
import type { VersionMetadata } from "~/utils/getVersion";
7+
8+
const createStorage = (values = new Map<string, string>()) => ({
9+
getItem: (key: string): string | null => values.get(key) || null,
10+
setItem: (key: string, value: string): void => {
11+
values.set(key, value);
12+
},
13+
});
14+
15+
const versionMetadata: VersionMetadata = {
16+
version: "0.21.0",
17+
buildDate: "2026-06-22",
18+
buildCommit: "1234567890abcdef",
19+
buildBranch: "main",
20+
versionStamp: "0.21.0-2026-06-22-12345678",
21+
};
22+
23+
describe("getOrCreateExtensionInstallId", () => {
24+
it("reuses an existing install ID from storage", () => {
25+
const storage = {
26+
getItem: (): string => " install-id-1 ",
27+
setItem: (): void => {
28+
throw new Error("setItem should not be called");
29+
},
30+
};
31+
32+
expect(
33+
getOrCreateExtensionInstallId({
34+
storage,
35+
createId: () => "new-id",
36+
}),
37+
).toBe("install-id-1");
38+
});
39+
40+
it("creates and persists an install ID when storage is empty", () => {
41+
const values = new Map<string, string>();
42+
const storage = createStorage(values);
43+
44+
expect(
45+
getOrCreateExtensionInstallId({
46+
storage,
47+
createId: () => "new-id",
48+
}),
49+
).toBe("new-id");
50+
expect([...values.values()]).toEqual(["new-id"]);
51+
expect(
52+
getOrCreateExtensionInstallId({
53+
storage,
54+
createId: () => "second-id",
55+
}),
56+
).toBe("new-id");
57+
});
58+
59+
it("falls back to a runtime ID when storage throws", () => {
60+
const storage = {
61+
getItem: (): string => {
62+
throw new Error("storage blocked");
63+
},
64+
setItem: (): void => undefined,
65+
};
66+
67+
expect(
68+
getOrCreateExtensionInstallId({
69+
storage,
70+
createId: () => "runtime-id",
71+
}),
72+
).toBe("runtime-id");
73+
});
74+
75+
it("returns the generated ID when storage cannot persist it", () => {
76+
let calls = 0;
77+
const storage = {
78+
getItem: (): null => null,
79+
setItem: (): void => {
80+
throw new Error("storage blocked");
81+
},
82+
};
83+
84+
expect(
85+
getOrCreateExtensionInstallId({
86+
storage,
87+
createId: () => {
88+
calls += 1;
89+
return `runtime-id-${calls}`;
90+
},
91+
}),
92+
).toBe("runtime-id-1");
93+
});
94+
});
95+
96+
describe("buildExtensionTelemetryProperties", () => {
97+
it("builds direct event and session metadata with install metadata", () => {
98+
expect(
99+
buildExtensionTelemetryProperties({
100+
versionMetadata,
101+
storage: null,
102+
createInstallId: () => "install-id-1",
103+
}),
104+
).toEqual({
105+
...versionMetadata,
106+
extensionInstallId: "install-id-1",
107+
});
108+
});
109+
110+
it("normalizes missing version fields", () => {
111+
expect(
112+
buildExtensionTelemetryProperties({
113+
versionMetadata: {
114+
version: "",
115+
buildDate: " ",
116+
buildCommit: "-",
117+
buildBranch: "main",
118+
versionStamp: "0.21.0-2026-06-22",
119+
},
120+
storage: null,
121+
createInstallId: () => "install-id-1",
122+
}),
123+
).toEqual({
124+
version: "-",
125+
buildDate: "-",
126+
buildCommit: "-",
127+
buildBranch: "main",
128+
versionStamp: "0.21.0-2026-06-22",
129+
extensionInstallId: "install-id-1",
130+
});
131+
});
132+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { VersionMetadata } from "./getVersion";
2+
3+
export type ExtensionTelemetryProperties = VersionMetadata & {
4+
extensionInstallId: string;
5+
};
6+
7+
type StorageLike = {
8+
getItem: (key: string) => string | null;
9+
setItem: (key: string, value: string) => void;
10+
};
11+
12+
const INSTALL_ID_STORAGE_KEY = "discourse-graphs:roam:extension-install-id";
13+
const FALLBACK_VALUE = "-";
14+
15+
const hasTelemetryValue = (value: string | undefined | null): value is string =>
16+
Boolean(value?.trim() && value.trim() !== FALLBACK_VALUE);
17+
18+
const createRuntimeId = (): string => {
19+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function")
20+
return crypto.randomUUID();
21+
22+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
23+
};
24+
25+
const normalizeTelemetryValue = (value: string | undefined | null): string =>
26+
hasTelemetryValue(value) ? value.trim() : FALLBACK_VALUE;
27+
28+
export const getOrCreateExtensionInstallId = ({
29+
storage,
30+
createId = createRuntimeId,
31+
}: {
32+
storage?: StorageLike | null;
33+
createId?: () => string;
34+
} = {}): string => {
35+
if (!storage) return createId();
36+
37+
let installId: string | undefined;
38+
try {
39+
const existingId = storage.getItem(INSTALL_ID_STORAGE_KEY);
40+
if (hasTelemetryValue(existingId)) return existingId.trim();
41+
42+
installId = createId();
43+
storage.setItem(INSTALL_ID_STORAGE_KEY, installId);
44+
return installId;
45+
} catch {
46+
return installId || createId();
47+
}
48+
};
49+
50+
export const buildExtensionTelemetryProperties = ({
51+
versionMetadata,
52+
storage,
53+
createInstallId,
54+
}: {
55+
versionMetadata: VersionMetadata;
56+
storage?: StorageLike | null;
57+
createInstallId?: () => string;
58+
}): ExtensionTelemetryProperties => ({
59+
version: normalizeTelemetryValue(versionMetadata.version),
60+
buildDate: normalizeTelemetryValue(versionMetadata.buildDate),
61+
buildCommit: normalizeTelemetryValue(versionMetadata.buildCommit),
62+
buildBranch: normalizeTelemetryValue(versionMetadata.buildBranch),
63+
versionStamp: normalizeTelemetryValue(versionMetadata.versionStamp),
64+
extensionInstallId: getOrCreateExtensionInstallId({
65+
storage,
66+
createId: createInstallId,
67+
}),
68+
});

apps/roam/src/utils/posthog.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import { getVersionWithDate } from "./getVersion";
2+
import { buildExtensionTelemetryProperties } from "./extensionTelemetry";
23
import posthog from "posthog-js";
34
import type { CaptureResult } from "posthog-js";
45

56
let initialized = false;
67

8+
const getLocalStorage = (): Storage | undefined => {
9+
try {
10+
return window.localStorage;
11+
} catch {
12+
return undefined;
13+
}
14+
};
15+
716
export const initPostHog = (): void => {
817
if (initialized) return;
918
if (window.roamAlphaAPI.graph.isEncrypted) return;
@@ -38,20 +47,19 @@ export const initPostHog = (): void => {
3847
return result;
3948
},
4049
loaded: (posthog) => {
41-
const { version, buildDate, buildCommit, buildBranch, versionStamp } =
42-
getVersionWithDate();
50+
const extensionTelemetry = buildExtensionTelemetryProperties({
51+
versionMetadata: getVersionWithDate(),
52+
storage: getLocalStorage(),
53+
});
4354
const userUid = window.roamAlphaAPI.user.uid() || "";
4455
const graphName = window.roamAlphaAPI.graph.name;
45-
if (userUid) posthog.identify(userUid);
46-
posthog.register_for_session({
47-
version: version || "-",
48-
buildDate: buildDate || "-",
49-
buildCommit: buildCommit || "-",
50-
buildBranch: buildBranch || "-",
51-
versionStamp: versionStamp || "-",
56+
const sessionTelemetry = {
57+
...extensionTelemetry,
5258
graphName,
53-
});
54-
posthog.capture("Extension Loaded");
59+
};
60+
if (userUid) posthog.identify(userUid);
61+
posthog.register_for_session(sessionTelemetry);
62+
posthog.capture("Extension Loaded", sessionTelemetry);
5563
initialized = true;
5664
},
5765
});

0 commit comments

Comments
 (0)