Skip to content
Open
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
35 changes: 35 additions & 0 deletions apps/roam/src/components/settings/HomePersonalSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
DISCOURSE_CONTEXT_OVERLAY_IN_CANVAS_KEY,
DISCOURSE_TOOL_SHORTCUT_KEY,
STREAMLINE_STYLING_KEY,
DISALLOW_TRACKING,
} from "~/data/userSettings";
import { enablePostHog, disablePostHog } from "~/utils/posthog";
import internalError from "~/utils/internalError";
import KeyboardShortcutInput from "./KeyboardShortcutInput";
import { getSetting, setSetting } from "~/utils/extensionSettings";
import streamlineStyling from "~/styles/streamlineStyling";
Expand Down Expand Up @@ -263,6 +266,38 @@ const HomePersonalSettings = ({ onloadArgs }: { onloadArgs: OnloadArgs }) => {
</>
}
/>
<Checkbox
defaultChecked={getSetting(DISALLOW_TRACKING, false)}
onChange={(e) => {
const target = e.target as HTMLInputElement;
const disallow = target.checked;
void setSetting(DISALLOW_TRACKING, disallow)
.then(() => {
if (disallow) {
disablePostHog();
} else {
enablePostHog();
}
})
.catch((error) => {
target.checked = !disallow;
internalError({
error,
userMessage: "Could not change settings",
});
});
}}
labelElement={
<>
Disable tracking
<Description
description={
"Disable our recording of errors and minimal activity we use to improve our product."
}
/>
</>
}
/>
</div>
);
};
Expand Down
1 change: 1 addition & 0 deletions apps/roam/src/data/userSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export const DISCOURSE_TOOL_SHORTCUT_KEY = "discourse-tool-shortcut";
export const DISCOURSE_CONTEXT_OVERLAY_IN_CANVAS_KEY =
"discourse-context-overlay-in-canvas";
export const STREAMLINE_STYLING_KEY = "streamline-styling";
export const DISALLOW_TRACKING = "disallow-tracking";
47 changes: 4 additions & 43 deletions apps/roam/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { addStyle } from "roamjs-components/dom";
import { render as renderToast } from "roamjs-components/components/Toast";
import getCurrentUserUid from "roamjs-components/queries/getCurrentUserUid";
import { runExtension } from "roamjs-components/util";
import { queryBuilderLoadedToast } from "./data/toastMessages";
import runQuery from "./utils/runQuery";
Expand All @@ -21,7 +20,6 @@ import discourseFloatingMenuStyles from "./styles/discourseFloatingMenuStyles.cs
import settingsStyles from "./styles/settingsStyles.css";
import discourseGraphStyles from "./styles/discourseGraphStyles.css";
import streamlineStyling from "./styles/streamlineStyling";
import posthog from "posthog-js";
import getDiscourseNodes from "./utils/getDiscourseNodes";
import { initFeedbackWidget } from "./components/BirdEatsBugs";
import {
Expand All @@ -38,53 +36,16 @@ import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByPar
import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
import { DISCOURSE_CONFIG_PAGE_TITLE } from "./utils/renderNodeConfigPage";
import { getSetting } from "./utils/extensionSettings";
import { STREAMLINE_STYLING_KEY } from "./data/userSettings";
import { getVersionWithDate } from "~/utils/getVersion";

const initPostHog = () => {
posthog.init("phc_SNMmBqwNfcEpNduQ41dBUjtGNEUEKAy6jTn63Fzsrax", {
api_host: "https://us.i.posthog.com",
person_profiles: "identified_only",
capture_pageview: false,
autocapture: false,
loaded: (posthog) => {
const { version, buildDate } = getVersionWithDate();
const userUid = getCurrentUserUid();
const graphName = window.roamAlphaAPI.graph.name;
posthog.identify(userUid, {
graphName,
});
posthog.register({
version: version || "-",
buildDate: buildDate || "-",
graphName,
});
posthog.capture("Extension Loaded");
},
property_denylist: [
"$ip", // Still seeing ip in the event
"$device_id",
"$geoip_city_name",
"$geoip_latitude",
"$geoip_longitude",
"$geoip_postal_code",
"$geoip_subdivision_1_name",
"$raw_user_agent",
"$current_url",
"$referrer",
"$referring_domain",
"$initial_current_url",
"$pathname",
],
});
};
import { initPostHog } from "./utils/posthog";
import { STREAMLINE_STYLING_KEY, DISALLOW_TRACKING } from "./data/userSettings";

export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*";

export default runExtension(async (onloadArgs) => {
const isEncrypted = window.roamAlphaAPI.graph.isEncrypted;
const isOffline = window.roamAlphaAPI.graph.type === "offline";
if (!isEncrypted && !isOffline) {
const disallowTracking = getSetting(DISALLOW_TRACKING, false);
if (!isEncrypted && !isOffline && !disallowTracking) {
initPostHog();
}

Expand Down
7 changes: 6 additions & 1 deletion apps/roam/src/utils/internalError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import posthog from "posthog-js";
import type { Properties } from "posthog-js";
import renderToast from "roamjs-components/components/Toast";
import sendErrorEmail from "~/utils/sendErrorEmail";
import { getSetting } from "~/utils/extensionSettings";
import { DISALLOW_TRACKING } from "~/data/userSettings";

const NON_WORD = /\W+/g;

Expand All @@ -20,7 +22,10 @@ const internalError = ({
sendEmail?: boolean;
forceSendInDev?: boolean;
}): void => {
if (process.env.NODE_ENV === "development" && forceSendInDev !== true) {
if (
getSetting(DISALLOW_TRACKING, false) ||
(process.env.NODE_ENV === "development" && forceSendInDev !== true)
) {
console.error(error, context);
} else {
type = type || "Internal Error";
Expand Down
77 changes: 77 additions & 0 deletions apps/roam/src/utils/posthog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import getCurrentUserUid from "roamjs-components/queries/getCurrentUserUid";
import { getVersionWithDate } from "./getVersion";
import posthog from "posthog-js";
import type { CaptureResult } from "posthog-js";
import { getSetting } from "./extensionSettings";
import { DISALLOW_TRACKING } from "~/data/userSettings";

let initialized = false;

const doInitPostHog = (): void => {
if (initialized) return;
Comment on lines +8 to +11
Copy link
Contributor

@coderabbitai coderabbitai bot Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix race condition: set initialized flag before async initialization.

The initialized flag is checked at line 11 but only set to true inside the loaded callback (line 58). If doInitPostHog() is called multiple times in quick succession before the loaded callback fires, posthog.init() will be invoked multiple times, potentially causing duplicate initialization.

🔎 Apply this diff to fix the race condition:
 const doInitPostHog = (): void => {
   if (initialized) return;
+  initialized = true;
   const propertyDenylist = new Set([

Then remove the assignment from the loaded callback since it's no longer needed there:

       posthog.capture("Extension Loaded");
-      initialized = true;
     },

Also applies to: 58-58

🤖 Prompt for AI Agents
In apps/roam/src/utils/posthog.ts around lines 8-11 and 58, the boolean
initialized is only set inside the async loaded callback which allows multiple
concurrent calls to doInitPostHog to trigger posthog.init; move the assignment
so initialized = true is set immediately before starting async initialization
(i.e., before calling posthog.init or loading the script) to prevent re-entry,
and remove the redundant initialized assignment from the loaded callback.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think multiple calls in quick succession are unlikely, and oth I don't want to set the flag before a successful initialization.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maparent, that's a reasonable approach. Prioritizing successful initialization verification over preventing duplicate initialization attempts makes sense given your architecture and the low likelihood of concurrent calls. Your current implementation favors being able to retry on failure, which is a valid trade-off.


🧠 Learnings used
Learnt from: CR
Repo: DiscourseGraphs/discourse-graph PR: 0
File: .cursor/rules/roam.mdc:0-0
Timestamp: 2025-11-25T00:52:41.934Z
Learning: Applies to apps/roam/**/*.{ts,tsx,js,jsx} : Use Roam Depot/Extension API docs from https://roamresearch.com/#/app/developer-documentation/page/y31lhjIqU when implementing extension functionality

Learnt from: maparent
Repo: DiscourseGraphs/discourse-graph PR: 0
File: :0-0
Timestamp: 2025-08-30T17:03:26.927Z
Learning: For maparent: Takes a focused approach to PR scope - acknowledges good suggestions that are out of scope for current PR rather than expanding scope.

const propertyDenylist = new Set([
"$ip",
"$device_id",
"$geoip_city_name",
"$geoip_latitude",
"$geoip_longitude",
"$geoip_postal_code",
"$geoip_subdivision_1_name",
"$raw_user_agent",
"$current_url",
"$referrer",
"$referring_domain",
"$initial_current_url",
"$pathname",
]);
posthog.init("phc_SNMmBqwNfcEpNduQ41dBUjtGNEUEKAy6jTn63Fzsrax", {
/* eslint-disable @typescript-eslint/naming-convention */
api_host: "https://us.i.posthog.com",
person_profiles: "identified_only",
capture_pageview: false,
property_denylist: [...propertyDenylist],
before_send: (result: CaptureResult | null) => {
if (result !== null) {
result.properties = Object.fromEntries(
Object.entries(result.properties).filter(
([k]) => !propertyDenylist.has(k),
),
);
}
return result;
},
/* eslint-enable @typescript-eslint/naming-convention */
autocapture: false,
loaded: (posthog) => {
const { version, buildDate } = getVersionWithDate();
const userUid = getCurrentUserUid();
const graphName = window.roamAlphaAPI.graph.name;
posthog.identify(userUid, {
graphName,
});
posthog.register({
version: version || "-",
buildDate: buildDate || "-",
graphName,
});
posthog.capture("Extension Loaded");
initialized = true;
},
});
};

export const enablePostHog = (): void => {
doInitPostHog();
posthog.opt_in_capturing();
};

export const disablePostHog = (): void => {
if (initialized) posthog.opt_out_capturing();
};

export const initPostHog = (): void => {
const disabled = getSetting(DISALLOW_TRACKING, false);
if (!disabled) {
doInitPostHog();
}
};