Skip to content

feat(analytics): instrument official templates with PostHog - #60

Open
xiongxz wants to merge 8 commits into
InsForge:mainfrom
lexmount:codex/posthog-official-templates
Open

feat(analytics): instrument official templates with PostHog#60
xiongxz wants to merge 8 commits into
InsForge:mainfrom
lexmount:codex/posthog-official-templates

Conversation

@xiongxz

@xiongxz xiongxz commented Sep 4, 2026

Copy link
Copy Markdown

Summary

  • initialize PostHog in all 14 official templates from platform-injected runtime config
  • capture page views, page leaves, performance, and semantic conversion events
  • apply privacy-safe defaults: no autocapture, masked inputs, URL and property sanitization, sampled replay
  • add an analytics contract validator to CI

Validation

  • analytics contract validated for all 14 templates
  • registry validation passed
  • registry test suite: 17 passed
  • production builds passed for insight-flow-agent-chat, booking, and admin-dashboard
  • git diff --check

Companion PR

  • insforge-platform: injects per-application PostHog runtime configuration

Summary by cubic

Instruments all 14 official templates with PostHog analytics driven by the platform's runtime config, adds the Insight Flow Agent Chat template, and moves the chatbot template onto the Insight Flow runtime.

Analytics

  • Templates load PostHog config from /.well-known/insforge-runtime-config.js and fall back to NEXT_PUBLIC_* or VITE_* env vars for standalone deployments.
  • Analytics are privacy-safe: no autocapture, masked inputs, sanitized property names and values, and sampled session replay.
  • CI validates the analytics contract for every template and builds publishable templates.
  • Booking and e-commerce templates replace Vercel Analytics with PostHog.

Templates

  • Adds insight-flow-agent-chat, a Vite chat UI that proxies to an Insight Flow Agent through two new Edge Functions with per-user, RLS-protected config storage.
  • The chatbot template now uses the Insight Flow Model Gateway by default and removes the Vercel AI Gateway integration.

Written for commit f6c2a77. Summary will update on new commits.

Review in cubic

xiongxz and others added 8 commits August 18, 2026 16:52
Co-authored-by: bruinxz <bruinxz@bruinxzdeMacBook-Pro.local>
Co-authored-by: bruinxz <bruinxz@bruinxzdeMacBook-Pro.local>
Co-authored-by: bruinxz <bruinxz@bruinxzdeMacBook-Pro.local>
Co-authored-by: bruinxz <bruinxz@bruinxzdeMacBook-Pro.local>
* feat: add Insight Flow Agent chat template

* refactor: move agent config behind chat UI

* refactor: simplify agent key storage

* fix: harden streaming agent template

* fix: normalize agent endpoint hosts

* fix: surface function errors in the template

---------

Co-authored-by: bruinxz <bruinxz@bruinxzdeMacBook-Pro.local>
* feat: classify and gate publishable templates

* fix: keep agent chat template on npm

---------

Co-authored-by: bruinxz <bruinxz@bruinxzdeMacBook-Pro.local>
@agent-zhang-beihai

Copy link
Copy Markdown

Thanks for the PR, @xiongxz! A quick note on our workflow: we ask contributors to open an issue first, get it assigned, then submit a PR that links it (e.g. "Closes #123"). This PR isn't linked to any issue. It'll still be reviewed, but please open an issue and claim it (comment that you'd like it assigned to you) so the work is tracked.

@agent-zhang-beihai agent-zhang-beihai Bot added the needs-issue PR isn't linked to any issue — open and claim an issue first, then link it label Sep 4, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

40 issues found across 181 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="chatbot/lib/analytics.ts">

<violation number="1" location="chatbot/lib/analytics.ts:30">
P2: When a caller passes camelCase PII keys or custom URL properties, this guard forwards them to PostHog; only snake_case keys and four built-in URL keys are sanitized. Normalize key matching and sanitize or reject URL-valued custom properties before capture.</violation>

<violation number="2" location="chatbot/lib/analytics.ts:77">
P1: In sampled chatbot replays, rendered user prompts, assistant responses, and attachment names remain readable because the UI has no `data-private` region. Mark the transcript and attachments private, or disable replay for this template.</violation>

<violation number="3" location="chatbot/lib/analytics.ts:110">
P2: These helpers are never called in the chatbot, so successful sign-ins, sign-ups, and other conversion actions emit no semantic events. Wire them into the relevant success and interaction paths, or remove the unused contract from this template.</violation>
</file>

<file name="e-commerce/lib/analytics.ts">

<violation number="1" location="e-commerce/lib/analytics.ts:8">
P1: CamelCase PII keys bypass the final privacy guard and can send names or phone numbers to PostHog. Normalize property keys before matching, or restrict `AnalyticsProperties` to an approved non-PII schema.</violation>

<violation number="2" location="e-commerce/lib/analytics.ts:41">
P1: Custom URL properties bypass query and fragment stripping, so event callers can send sensitive URL parameters to PostHog. Sanitize every URL-valued property, not only PostHog’s four automatic URL keys.</violation>

<violation number="3" location="e-commerce/lib/analytics.ts:77">
P1: Sampled replay captures customer email, address, recipient, and phone text in this template because no rendered regions use `data-private`. Mark every customer-content region before enabling replay, or configure replay to mask text by default.</violation>
</file>

<file name="insight-flow-agent-chat/src/lib/analytics.ts">

<violation number="1" location="insight-flow-agent-chat/src/lib/analytics.ts:79">
P1: When a session is sampled for replay, customer prompts, agent replies, and the displayed account email are recorded in plain text because this template marks none of them `data-private`. Mask these rendered-content selectors or disable replay for this template.</violation>

<violation number="2" location="insight-flow-agent-chat/src/lib/analytics.ts:111">
P2: The semantic analytics API is never used in this template, so login, form, and conversion events never reach PostHog. Wire the helpers into the successful auth and domain flows.</violation>
</file>

<file name="admin-dashboard/src/lib/analytics.ts">

<violation number="1" location="admin-dashboard/src/lib/analytics.ts:39">
P1: When a custom event contains a URL under any key other than the four PostHog URL keys, this sanitizer leaves query parameters and fragments intact. Sanitize every absolute URL-valued property here so custom analytics cannot send tokenized or otherwise sensitive URLs.</violation>

<violation number="2" location="admin-dashboard/src/lib/analytics.ts:73">
P1: The dashboard enables sampled replay without marking its rendered customer content as private, so replay can record emails, task details, and chat messages even though inputs are masked. Add `data-private` to every customer-content region before enabling replay, or disable replay until that coverage exists.</violation>

<violation number="3" location="admin-dashboard/src/lib/analytics.ts:112">
P2: This dashboard initializes PostHog but never calls the exported semantic helpers, so production records automatic events without login, signup, form, or purchase conversions. Wire these helpers into the corresponding successful flows before claiming semantic conversion instrumentation.</violation>
</file>

<file name="web-research-agent/src/main.tsx">

<violation number="1" location="web-research-agent/src/main.tsx:88">
P2: When a connected workspace is loading or its initial query fails, the UI shows the public showcase data as if it were the user's research. Initialize empty workspace state when `connected` and reserve `demo*` state for showcase mode.</violation>

<violation number="2" location="web-research-agent/src/main.tsx:171">
P1: Sampled session replay records the workspace's customer research content unmasked because it is rendered outside form controls. Mark this workspace root `data-private` before rendering user data.</violation>
</file>

<file name="website-change-monitor/src/lib/analytics.ts">

<violation number="1" location="website-change-monitor/src/lib/analytics.ts:39">
P1: When an event includes a URL under any property other than the four PostHog defaults, its query and hash are sent unchanged. Sanitize every HTTP(S) URL-valued property, not only the built-in URL keys.</violation>

<violation number="2" location="website-change-monitor/src/lib/analytics.ts:70">
P1: Sampled replay records the authenticated email and customer content in this template because no rendered sensitive regions use `data-private`. Mark those regions private before enabling replay, or disable replay for this template.</violation>
</file>

<file name="web-research-agent/functions/research-run.ts">

<violation number="1" location="web-research-agent/functions/research-run.ts:88">
P1: When the AI returns a missing, non-numeric, or out-of-range source, claim construction either fails the whole run or stores a citation label that disagrees with its source. Normalize the source index and derive `citation_label` from the bounded index before inserting claims.</violation>

<violation number="2" location="web-research-agent/functions/research-run.ts:89">
P1: If inserting claims fails, this line ignores the error, then the function marks the project ready and returns 200 with claims that were not persisted. Check the insert result and throw into the existing failure path.</violation>

<violation number="3" location="web-research-agent/functions/research-run.ts:90">
P2: If the final project update fails, the function still returns success and leaves the project `running`, so the UI can show “Collecting sources” indefinitely. Check this update error and route it through the failure response.</violation>
</file>

<file name="scripts/validate-registry.mjs">

<violation number="1" location="scripts/validate-registry.mjs:79">
P1: Prefixed provider key variables bypass this AI contract check because `_` is a word character before `OPENAI` (and the other provider names). Match provider-key tokens anywhere within uppercase environment variable names so templates cannot evade the platform-managed gateway check with names such as `SERVER_OPENAI_API_KEY`.</violation>
</file>

<file name="booking/lib/analytics.ts">

<violation number="1" location="booking/lib/analytics.ts:39">
P1: When event authors use camelCase PII keys or custom URL properties, this helper sends them unchanged. Normalize property names and sanitize or drop URL-like values before calling PostHog.</violation>

<violation number="2" location="booking/lib/analytics.ts:110">
P2: Booking never calls the semantic helpers defined here, so successful sign-ups, logins, and bookings emit no conversion events. Import `analytics` at the success paths and call the corresponding helper after each operation succeeds.</violation>
</file>

<file name="insight-flow-agent-chat/src/App.tsx">

<violation number="1" location="insight-flow-agent-chat/src/App.tsx:24">
P1: When `getCurrentUser()` rejects, `authLoading` never becomes false, so the app stays on the login-recovery spinner indefinitely. Handle the rejection and clear the loading state in a `finally` block.</violation>
</file>

<file name="crm/lib/analytics.ts">

<violation number="1" location="crm/lib/analytics.ts:8">
P1: The privacy guard misses common camelCase PII keys such as `firstName` and `accessToken`. Normalize property keys before applying `PII_KEY`, or extend the boundary matching to cover camelCase names before sending them to PostHog.</violation>
</file>

<file name="web-research-agent/src/lib/analytics.ts">

<violation number="1" location="web-research-agent/src/lib/analytics.ts:73">
P1: When a production session is selected for replay, this config masks inputs but leaves ordinary DOM text unmasked. The template renders `user?.email` and customer content without `data-private`, so mark those regions private before enabling replay.</violation>
</file>

<file name="todo/src/lib/analytics.ts">

<violation number="1" location="todo/src/lib/analytics.ts:8">
P1: Properties using camelCase or other common naming (`firstName`, `phoneNumber`, `accessToken`) bypass this guard because it only recognizes underscore-delimited tokens. Normalize keys or extend the matcher to cover camelCase before sending properties.</violation>

<violation number="2" location="todo/src/lib/analytics.ts:41">
P1: Custom URL properties retain query strings because this sanitizer only processes PostHog's four reserved URL keys. Sanitize every URL-valued property before capture so custom properties cannot send tokens or query-string PII.</violation>

<violation number="3" location="todo/src/lib/analytics.ts:77">
P1: Sampled recordings can capture users' todo text because this configuration masks only explicitly marked elements, while the todo template marks none. Mark the rendered customer-content region with `data-private`, or disable replay for this template.</violation>
</file>

<file name="scripts/verify-publishable-template.mjs">

<violation number="1" location="scripts/verify-publishable-template.mjs:41">
P2: When `dist` or `out` itself is a symlink, this guard never rejects it because `statSync` follows the link, and `visit` scans its target. Use `lstatSync` and reject the root before traversing it.</violation>
</file>

<file name="web-research-agent/migrations/db_init.sql">

<violation number="1" location="web-research-agent/migrations/db_init.sql:57">
P2: When an authenticated user knows another project’s UUID, these child-table policies allow them to insert a source or claim owned by themselves while attaching it to that project, and claims can point at unrelated sources. Require `project_id` to belong to `auth.uid()` and require each claim’s source to belong to that same project and user in both `using` and `with check`.</violation>
</file>

<file name="react/src/lib/analytics.ts">

<violation number="1" location="react/src/lib/analytics.ts:37">
P2: When a custom event property contains a URL outside PostHog's four reserved URL keys, this helper sends its query and hash unchanged. Sanitize every parseable URL-valued property before capture so `track` cannot emit query-bearing URLs.</violation>
</file>

<file name="insight-flow-agent-chat/src/components/ChatPage.tsx">

<violation number="1" location="insight-flow-agent-chat/src/components/ChatPage.tsx:117">
P2: When a user starts a new chat while a reply is streaming, the aborted request can clear the newer request's streaming state in `finally`. This enables overlapping sends and can corrupt the active chat flow; only clear `isStreaming` when this controller is still current.</violation>
</file>

<file name="website-change-monitor/src/main.tsx">

<violation number="1" location="website-change-monitor/src/main.tsx:64">
P2: When a connected workspace load is pending or fails, the authenticated UI continues showing hard-coded demo targets and changes. Users can click actions such as `Check now` for fake IDs instead of their workspace; use empty initial state when `connected` and clear or replace it on load failure.</violation>

<violation number="2" location="website-change-monitor/src/main.tsx:73">
P2: When `getCurrentUser()` rejects, the app remains stuck on the loading spinner because no handler clears `authLoading`. Catch the request failure and set `authLoading` to `false` so the user can sign in or see an error.</violation>
</file>

<file name="nextjs/src/lib/analytics.ts">

<violation number="1" location="nextjs/src/lib/analytics.ts:97">
P2: When a custom event includes a URL-valued property, `track` can send its query parameters unchanged because the URL sanitizer only handles PostHog's reserved URL keys. Sanitize or reject URL-valued custom properties before capture so query parameters cannot leak.</violation>
</file>

<file name="ai-pdf-chatbot/lib/analytics.ts">

<violation number="1" location="ai-pdf-chatbot/lib/analytics.ts:8">
P2: When an event uses a camelCase PII key, both privacy filters forward the value because `PII_KEY` only recognizes underscore-delimited words. Normalize keys or include camelCase boundaries in this matcher before sending properties.</violation>
</file>

<file name="website-change-monitor/functions/monitor-check.ts">

<violation number="1" location="website-change-monitor/functions/monitor-check.ts:56">
P2: When two checks for the same target overlap, both can read the same previous snapshot before either inserts. Each then records a change from that snapshot, producing duplicate notifications; atomically claim or serialize the target, or make snapshot/change recording idempotent.</violation>

<violation number="2" location="website-change-monitor/functions/monitor-check.ts:73">
P2: When `monitor_changes` insertion fails, this handler still returns 200 with `changed: true`, losing the change record. Check the insert error and fail the run instead of reporting a successful check.</violation>

<violation number="3" location="website-change-monitor/functions/monitor-check.ts:77">
P2: When the final target update fails, the function still returns success while the target can remain `checking`, which excludes it from scheduled checks. Check this update error and return a failed check when the state cannot be persisted.</violation>
</file>

<file name="insight-flow-agent-chat/src/lib/insforge.ts">

<violation number="1" location="insight-flow-agent-chat/src/lib/insforge.ts:3">
P2: Vitest now fails before `config-errors.test.ts` runs because importing `config.ts` evaluates this module in Node, where `window` is undefined. Guard the browser access (or read the value from `globalThis`) so shared config utilities remain importable in tests and non-browser contexts.</violation>
</file>

<file name="insight-flow-agent-chat/functions/insight-flow-config.ts">

<violation number="1" location="insight-flow-agent-chat/functions/insight-flow-config.ts:174">
P2: Valid JSON with a null or incorrectly typed body causes this handler to throw before returning a 4xx response. Parse into `unknown`, validate the object and field types in both PUT and POST branches, then return a validation error before accessing them.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread chatbot/lib/analytics.ts
disable_session_recording: Math.random() >= replaySampleRate,
session_recording: {
maskAllInputs: true,
maskTextSelector: '[data-private]',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: In sampled chatbot replays, rendered user prompts, assistant responses, and attachment names remain readable because the UI has no data-private region. Mark the transcript and attachments private, or disable replay for this template.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At chatbot/lib/analytics.ts, line 77:

<comment>In sampled chatbot replays, rendered user prompts, assistant responses, and attachment names remain readable because the UI has no `data-private` region. Mark the transcript and attachments private, or disable replay for this template.</comment>

<file context>
@@ -0,0 +1,121 @@
+    disable_session_recording: Math.random() >= replaySampleRate,
+    session_recording: {
+      maskAllInputs: true,
+      maskTextSelector: '[data-private]',
+      blockSelector: '[data-private]',
+    },
</file context>

disable_session_recording: Math.random() >= replaySampleRate,
session_recording: {
maskAllInputs: true,
maskTextSelector: '[data-private]',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Sampled replay captures customer email, address, recipient, and phone text in this template because no rendered regions use data-private. Mark every customer-content region before enabling replay, or configure replay to mask text by default.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At e-commerce/lib/analytics.ts, line 77:

<comment>Sampled replay captures customer email, address, recipient, and phone text in this template because no rendered regions use `data-private`. Mark every customer-content region before enabling replay, or configure replay to mask text by default.</comment>

<file context>
@@ -0,0 +1,121 @@
+    disable_session_recording: Math.random() >= replaySampleRate,
+    session_recording: {
+      maskAllInputs: true,
+      maskTextSelector: '[data-private]',
+      blockSelector: '[data-private]',
+    },
</file context>

disable_session_recording: Math.random() >= replaySampleRate,
session_recording: {
maskAllInputs: true,
maskTextSelector: '[data-private]',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: When a session is sampled for replay, customer prompts, agent replies, and the displayed account email are recorded in plain text because this template marks none of them data-private. Mask these rendered-content selectors or disable replay for this template.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At insight-flow-agent-chat/src/lib/analytics.ts, line 79:

<comment>When a session is sampled for replay, customer prompts, agent replies, and the displayed account email are recorded in plain text because this template marks none of them `data-private`. Mask these rendered-content selectors or disable replay for this template.</comment>

<file context>
@@ -0,0 +1,123 @@
+    disable_session_recording: Math.random() >= replaySampleRate,
+    session_recording: {
+      maskAllInputs: true,
+      maskTextSelector: '[data-private]',
+      blockSelector: '[data-private]',
+    },
</file context>

disable_session_recording: Math.random() >= replaySampleRate,
session_recording: {
maskAllInputs: true,
maskTextSelector: '[data-private]',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The dashboard enables sampled replay without marking its rendered customer content as private, so replay can record emails, task details, and chat messages even though inputs are masked. Add data-private to every customer-content region before enabling replay, or disable replay until that coverage exists.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At admin-dashboard/src/lib/analytics.ts, line 73:

<comment>The dashboard enables sampled replay without marking its rendered customer content as private, so replay can record emails, task details, and chat messages even though inputs are masked. Add `data-private` to every customer-content region before enabling replay, or disable replay until that coverage exists.</comment>

<file context>
@@ -0,0 +1,115 @@
+    disable_session_recording: Math.random() >= replaySampleRate,
+    session_recording: {
+      maskAllInputs: true,
+      maskTextSelector: '[data-private]',
+      blockSelector: '[data-private]',
+    },
</file context>

</div>
</header>

<main className="grid min-h-[calc(100vh-4rem)] min-w-0 lg:grid-cols-[270px_minmax(0,1fr)_330px]">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Sampled session replay records the workspace's customer research content unmasked because it is rendered outside form controls. Mark this workspace root data-private before rendering user data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web-research-agent/src/main.tsx, line 171:

<comment>Sampled session replay records the workspace's customer research content unmasked because it is rendered outside form controls. Mark this workspace root `data-private` before rendering user data.</comment>

<file context>
@@ -0,0 +1,271 @@
+        </div>
+      </header>
+
+      <main className="grid min-h-[calc(100vh-4rem)] min-w-0 lg:grid-cols-[270px_minmax(0,1fr)_330px]">
+        <aside className="min-w-0 overflow-hidden border-b border-[#cfd7e5] bg-[#e9eef7] p-4 lg:border-b-0 lg:border-r lg:p-5">
+          <button className="focus-ring flex w-full items-center justify-center gap-2 bg-[#1f5eff] px-4 py-3 text-sm font-semibold text-white hover:bg-[#164bd3]" onClick={() => setComposerOpen(true)}><Plus size={17} />New research</button>
</file context>
Suggested change
<main className="grid min-h-[calc(100vh-4rem)] min-w-0 lg:grid-cols-[270px_minmax(0,1fr)_330px]">
<main data-private className="grid min-h-[calc(100vh-4rem)] min-w-0 lg:grid-cols-[270px_minmax(0,1fr)_330px]">

}
const now = new Date();
const next = new Date(now.getTime() + Number(target.interval_minutes) * 60000);
await client.database.from("monitor_targets").update({ status: changed ? "changed" : "quiet", last_hash: hash, last_checked_at: now.toISOString(), next_check_at: next.toISOString(), error_message: "" }).eq("id", target.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When the final target update fails, the function still returns success while the target can remain checking, which excludes it from scheduled checks. Check this update error and return a failed check when the state cannot be persisted.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At website-change-monitor/functions/monitor-check.ts, line 77:

<comment>When the final target update fails, the function still returns success while the target can remain `checking`, which excludes it from scheduled checks. Check this update error and return a failed check when the state cannot be persisted.</comment>

<file context>
@@ -0,0 +1,84 @@
+    }
+    const now = new Date();
+    const next = new Date(now.getTime() + Number(target.interval_minutes) * 60000);
+    await client.database.from("monitor_targets").update({ status: changed ? "changed" : "quiet", last_hash: hash, last_checked_at: now.toISOString(), next_check_at: next.toISOString(), error_message: "" }).eq("id", target.id);
+    return json(200, { targetId: target.id, changed, snapshotId: snapshots[0].id, fetchMode: page.mode });
+  } catch (error) {
</file context>
Suggested change
await client.database.from("monitor_targets").update({ status: changed ? "changed" : "quiet", last_hash: hash, last_checked_at: now.toISOString(), next_check_at: next.toISOString(), error_message: "" }).eq("id", target.id);
const { error: targetUpdateError } = await client.database.from("monitor_targets").update({ status: changed ? "changed" : "quiet", last_hash: hash, last_checked_at: now.toISOString(), next_check_at: next.toISOString(), error_message: "" }).eq("id", target.id);
if (targetUpdateError) throw new Error(targetUpdateError.message || "Target state was not saved");

changeType = String(parsed.changeType || changeType).slice(0, 80);
significance = ["low", "medium", "high"].includes(parsed.significance) ? parsed.significance : significance;
} catch { /* A deterministic change record is still created. */ }
await client.database.from("monitor_changes").insert([{ target_id: target.id, previous_snapshot_id: previous.id, current_snapshot_id: snapshots[0].id, summary, change_type: changeType, significance, before_excerpt: previous.content_excerpt, after_excerpt: excerpt }]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When monitor_changes insertion fails, this handler still returns 200 with changed: true, losing the change record. Check the insert error and fail the run instead of reporting a successful check.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At website-change-monitor/functions/monitor-check.ts, line 73:

<comment>When `monitor_changes` insertion fails, this handler still returns 200 with `changed: true`, losing the change record. Check the insert error and fail the run instead of reporting a successful check.</comment>

<file context>
@@ -0,0 +1,84 @@
+        changeType = String(parsed.changeType || changeType).slice(0, 80);
+        significance = ["low", "medium", "high"].includes(parsed.significance) ? parsed.significance : significance;
+      } catch { /* A deterministic change record is still created. */ }
+      await client.database.from("monitor_changes").insert([{ target_id: target.id, previous_snapshot_id: previous.id, current_snapshot_id: snapshots[0].id, summary, change_type: changeType, significance, before_excerpt: previous.content_excerpt, after_excerpt: excerpt }]);
+    }
+    const now = new Date();
</file context>
Suggested change
await client.database.from("monitor_changes").insert([{ target_id: target.id, previous_snapshot_id: previous.id, current_snapshot_id: snapshots[0].id, summary, change_type: changeType, significance, before_excerpt: previous.content_excerpt, after_excerpt: excerpt }]);
const { error: changeError } = await client.database.from("monitor_changes").insert([{ target_id: target.id, previous_snapshot_id: previous.id, current_snapshot_id: snapshots[0].id, summary, change_type: changeType, significance, before_excerpt: previous.content_excerpt, after_excerpt: excerpt }]);
if (changeError) throw new Error(changeError.message || "Change was not saved");

@@ -0,0 +1,9 @@
import { createClient } from '@insforge/sdk';

const runtime = (window as Window & { __INSFORGE_RUNTIME_CONFIG__?: { apiBaseURL?: string; anonKey?: string } }).__INSFORGE_RUNTIME_CONFIG__;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Vitest now fails before config-errors.test.ts runs because importing config.ts evaluates this module in Node, where window is undefined. Guard the browser access (or read the value from globalThis) so shared config utilities remain importable in tests and non-browser contexts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At insight-flow-agent-chat/src/lib/insforge.ts, line 3:

<comment>Vitest now fails before `config-errors.test.ts` runs because importing `config.ts` evaluates this module in Node, where `window` is undefined. Guard the browser access (or read the value from `globalThis`) so shared config utilities remain importable in tests and non-browser contexts.</comment>

<file context>
@@ -0,0 +1,9 @@
+import { createClient } from '@insforge/sdk';
+
+const runtime = (window as Window & { __INSFORGE_RUNTIME_CONFIG__?: { apiBaseURL?: string; anonKey?: string } }).__INSFORGE_RUNTIME_CONFIG__;
+const baseUrl = (runtime?.apiBaseURL ?? import.meta.env.VITE_INSFORGE_URL)?.trim().replace(/\/$/, '');
+const anonKey = (runtime?.anonKey ?? import.meta.env.VITE_INSFORGE_ANON_KEY)?.trim();
</file context>


let input: ConfigInput;
try {
input = (await req.json()) as ConfigInput;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Valid JSON with a null or incorrectly typed body causes this handler to throw before returning a 4xx response. Parse into unknown, validate the object and field types in both PUT and POST branches, then return a validation error before accessing them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At insight-flow-agent-chat/functions/insight-flow-config.ts, line 174:

<comment>Valid JSON with a null or incorrectly typed body causes this handler to throw before returning a 4xx response. Parse into `unknown`, validate the object and field types in both PUT and POST branches, then return a validation error before accessing them.</comment>

<file context>
@@ -0,0 +1,222 @@
+
+  let input: ConfigInput;
+  try {
+    input = (await req.json()) as ConfigInput;
+  } catch {
+    return json(400, { error: 'invalid_json' });
</file context>

function App() {
const [user, setUser] = useState<{ id: string; email?: string } | null>(null);
const [authLoading, setAuthLoading] = useState(connected);
const [projects, setProjects] = useState<Project[]>(demoProjects);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When a connected workspace is loading or its initial query fails, the UI shows the public showcase data as if it were the user's research. Initialize empty workspace state when connected and reserve demo* state for showcase mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At web-research-agent/src/main.tsx, line 88:

<comment>When a connected workspace is loading or its initial query fails, the UI shows the public showcase data as if it were the user's research. Initialize empty workspace state when `connected` and reserve `demo*` state for showcase mode.</comment>

<file context>
@@ -0,0 +1,271 @@
+function App() {
+  const [user, setUser] = useState<{ id: string; email?: string } | null>(null);
+  const [authLoading, setAuthLoading] = useState(connected);
+  const [projects, setProjects] = useState<Project[]>(demoProjects);
+  const [sources, setSources] = useState<Source[]>(demoSources);
+  const [claims, setClaims] = useState<Claim[]>(demoClaims);
</file context>

@xiongxz

xiongxz commented Sep 4, 2026

Copy link
Copy Markdown
Author

Review (round 1)

Reviewed together with the two companion PRs (lexmount/insforge-platform#81 and lexmount/lex-insforge#4). Local checks: node scripts/check-analytics.mjs passes for 14 templates; scripts vitest suite 17/17. I did not re-run the production builds. cubic's 40 findings are mostly variations of three themes (camelCase PII keys, custom URL-valued properties, no data-private regions); I won't repeat them, only +1 where it matters.

Scope: 6 of the 8 commits are not about analytics

upstream/main..HEAD is:

f6c2a77 feat(analytics): instrument insight flow chat template
92a5c74 feat(analytics): instrument official templates with PostHog
6a91a73 feat: classify and gate publishable templates (#6)
1edc135 feat: add Insight Flow Agent chat template (#5)
0eba29f chore: localize browser template descriptions (#4)
cae5183 feat: add browser agent templates (#3)
c123b10 validate platform-managed AI templates (#2)
b8b30bd feat: add platform-managed AI and storage starter (#1)

The six (#1)(#6) commits are fork history: 110 of the 181 files and ~15.5k of the ~19k added lines. They add a new insight-flow-agent-chat template, move chatbot from the Vercel AI Gateway onto the "Insight Flow Model Gateway" (deleting chatbot/lib/ai/providers/vercel.ts), and add registry gating. For the upstream repo that is a different conversation from "add PostHog to the official templates", and the maintainers already asked for issue-first. Suggest rebasing just 92a5c74 + f6c2a77 onto upstream/main as this PR (88 files), and either keeping the rest fork-only or opening them as their own issues/PRs.

Contract check against the platform side

Verified against insforge-platform#81: the global (window.__INSFORGE_RUNTIME_CONFIG__), the field names (posthogHost, posthogProjectToken, applicationId, environmentId, templateVersionId, releaseId) and the path /.well-known/insforge-runtime-config.js (written by the platform's web-runtime layer) all line up. Good.

P2-1 Session replay is configured here but the provisioned PostHog projects have it switched off

New PostHog projects default to session_recording_opt_in = false, and the platform PR creates projects with only a name. So session_recording, capture_performance and the 10% sampling in every lib/analytics.ts produce nothing until someone flips the project setting, and ANALYTICS.md's replay section describes behaviour that does not happen. Either enable it at provisioning time (raised on #81) or state in ANALYTICS.md that replay is off by default.

P2-2 URL sanitization misses $set_once person properties

sanitizePostHogProperties strips query/hash from four top-level keys only. posthog-js (the pinned 1.425.x included) also sends the initial URL and referrer inside $set_once on early events and on identify, as $initial_person_info: { r: <referrer>, u: <full url> } (older versions: $initial_current_url). Those are nested, so a first visit via ?token=…, an OAuth callback ?code=… or a magic-link URL lands in the PostHog person profile untouched. Verified in the pinned build: dist/array.full.js of 1.425.1 registers $initial_person_info via register_once, gated by the mask_personal_data_properties config option, which none of the 14 helpers set. Cheapest fix is mask_personal_data_properties: true (plus custom_personal_data_properties for app-specific params) in posthog.init; otherwise recurse into $set / $set_once in the sanitizer. This complements cubic's point about custom URL-valued properties.

P2-3 The description promises semantic conversion events; only one template emits any

grep for analytics.(identify|track|…Completed|…Clicked|…) outside lib/analytics.ts finds two files, both in ai-pdf-chatbot (sign-in / sign-up forms). The other 13 templates ship the helper module and call only initializeAnalytics(), so they emit page views, page leaves and performance, nothing else. Likewise data-private is used in 0 files, so replay masks inputs only. cubic flagged both; +1. Either wire the six baseline events into each template's success paths, or scope the PR description and ANALYTICS.md to "pageview + performance, semantic events available" for now.

P3

  • Replay sampling re-rolls per page load. disable_session_recording: Math.random() >= rate is evaluated on every full load, so one user's session is recorded in fragments. Since the platform owns the project, set the sample rate on the project (server-side) and leave the client at the default; that also fixes P2-1 in one place.
  • ai-pdf-chatbot: the /ingest/* rewrites in next.config.js are now dead because instrumentation-client.ts points straight at us.i.posthog.com. Keep the reverse proxy (api_host: '/ingest' + ui_host) for ad-blocker resilience, or delete the rewrites. Also posthog-js is ^1.383.3 there vs ^1.425.1 everywhere else.
  • chatbot/lib/auth-actions.ts (from the bundled (#1) commit): the OAuth origin is now built from x-forwarded-host / host. Host-header-derived redirect URIs are a classic open-redirect vector unless the auth server validates redirect_uri against an allowlist; worth a look if that commit stays in this PR.
  • PII_KEY matches (^|_)name(_|$), which also drops PostHog's own $os_name/$browser_name-style keys if any are ever present; harmless today, but the regex is doing double duty as both a PII filter and a PostHog-property filter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-issue PR isn't linked to any issue — open and claim an issue first, then link it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants