From f79f5766a66db1e3e9efdbb6b94315f43aba0455 Mon Sep 17 00:00:00 2001 From: ankurmans <12939357+ankurmans@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:25:02 +0100 Subject: [PATCH] Say in the privacy policy what the website's sign-up tracking counts, and report which website sign-up button an account came from as signup_started and a session property on signup_completed, with session recording still described as off until its switch and retention are set. --- README.md | 4 +-- app/(site)/privacy/page.js | 6 ++-- app/signin/page.js | 2 +- components/marketing/ProviderAnalytics.jsx | 5 +-- components/marketing/SiteAnalytics.jsx | 12 +++++--- lib/legal.js | 16 +++++++++- lib/marketing/analytics-client.js | 1 + lib/marketing/analytics-privacy.js | 31 +++++++++++++++++++ lib/subprocessors.js | 6 ++-- tests/analytics-client.test.mjs | 16 ++++++++++ tests/analytics-privacy.test.mjs | 36 ++++++++++++++++++++-- tests/legal.test.mjs | 12 ++++++++ 12 files changed, 128 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ae3e843e..3cd80df8 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ FlockStack Cloud counts how people use its own pages with PostHog: one project o | --- | --- | | `POSTHOG_KEY` | The project's public key (`phc_…`). Unset or malformed: nothing loads and nothing renders. Read when a request renders. | | `POSTHOG_HOST` | Optional, an https address. Default `https://us.i.posthog.com`. | -| `POSTHOG_SESSION_REPLAY` | `true` turns session recording on, with every input and all text masked. Default off. The Privacy page says it is off, so change `ANALYTICS_RULE` in `lib/legal.js` before turning it on. | +| `POSTHOG_SESSION_REPLAY` | `true` turns session recording on, with every input and all text masked. Default off. The Privacy page says it is off while `SESSION_RECORDING_ON` in `lib/legal.js` is `false`: set it to `true` and fill in `SESSION_RECORDING_RETENTION` (until then the legal pages answer 404) in the same change. The marketing website has its own setting, `POSTHOG_SESSION_REPLAY=on`, which records only visitors who pressed Accept; the same constant covers both. | **Where it loads.** Only in the layouts of FlockStack's own pages: the docs and the legal pages (a `layout.js` in each folder of `app/(site)`), sign-in, `/welcome` and the signed-in dashboard (`components/LayoutDashboard.jsx`, used by `app/(app)` and `app/(account)`). Never in the root layout, a newsletter's pages and archive (also on their own hosts), unsubscribe, confirm, keep and referral pages, the reader and comment pages, shop and subscribe pages, the recommendations directory, the OAuth consent page or the admin. `tests/analytics-routes.test.mjs` scans every route and fails when a page carries it that shouldn't, or the scrubber's list of untracked paths (`lib/marketing/analytics-privacy.js`) disagrees with the layouts. That list is a second lock: PostHog drops any event from such a page, even if the browser navigates there without a reload. The marketing website loads its own copy of the analytics on its home, product, pricing, comparison, migration and blog pages (`ANALYTICS_PATHS` in its `data/site.js`), under the same consent cookie, and `ANALYTICS_RULE` in `lib/legal.js` names those pages for the privacy policy: change them together. @@ -203,7 +203,7 @@ FlockStack Cloud counts how people use its own pages with PostHog: one project o The choice is the first-party cookie `fs_consent` (`accepted` or `declined`, 12 months, `SameSite=Lax`, `Secure`). Before it, nothing is sent and no `ph_` cookie or local storage is written; posthog-js is not even downloaded. Declining deletes them. `fs_goal` is a ten-minute cookie holding the name of a step only the server sees (a new account, a first newsletter, a plan started: `siteExtensions.sendGoalOnNextPage`); the next page reports it and removes it. It is set only while analytics is on. FlockStack never calls PostHog from the server. -**What it collects.** Page views (including route changes), autocapture, device, browser and approximate location from PostHog; for a signed-in person, their user ID as the distinct ID with `email` and `plan` as the person's properties (`person_profiles: "identified_only"`); and four events, `signup_completed`, `newsletter_created`, `campaign_sent` (with `when`: `now` or `scheduled`) and `plan_started`, reported through `reportGoal` (`lib/goals.js`) from client code or the `fs_goal` cookie. Steps in shared markup with a `data-goal` attribute are kept on the autocaptured clicks. Every event goes through `sanitizeEvent` first: query strings and hashes are cut from addresses (a contacts search can hold an email address), and on the dashboard no element text, page title or label leaves, because a link there can be a subscriber's address or an issue's subject. `tests/analytics-privacy.test.mjs` fixes the property names. Session recording, surveys, heatmaps, dead clicks, error tracking and request timing are off. +**What it collects.** Page views (including route changes), autocapture, device, browser and approximate location from PostHog; for a signed-in person, their user ID as the distinct ID with `email` and `plan` as the person's properties (`person_profiles: "identified_only"`); and five events, `signup_started` (with `cta`, one of the fixed names in `SIGNUP_SOURCES`), `signup_completed`, `newsletter_created`, `campaign_sent` (with `when`: `now` or `scheduled`) and `plan_started`, reported through `reportGoal` (`lib/goals.js`) from client code or the `fs_goal` cookie. Steps in shared markup with a `data-goal` attribute are kept on the autocaptured clicks. Every event goes through `sanitizeEvent` first: query strings and hashes are cut from addresses (a contacts search can hold an email address), and on the dashboard no element text, page title or label leaves, because a link there can be a subscriber's address or an issue's subject. `tests/analytics-privacy.test.mjs` fixes the property names. Session recording, surveys, heatmaps, dead clicks, error tracking and request timing are off. **Headers.** `next.config.mjs` sets a Content-Security-Policy without `script-src`, `connect-src` or `img-src`, so PostHog's host needs no entry and is allowed on no page in particular. If those directives are ever added, allow `https://us.i.posthog.com` and `https://us-assets.i.posthog.com` only for the routes that carry the analytics. diff --git a/app/(site)/privacy/page.js b/app/(site)/privacy/page.js index 9a340b3c..402f4239 100644 --- a/app/(site)/privacy/page.js +++ b/app/(site)/privacy/page.js @@ -449,10 +449,12 @@ export default function PrivacyPage() {

- Analytics. FlockStack's own pages load PostHog, which sets its own cookies or local storage (their names start with{" "} + Analytics. FlockStack's own pages load PostHog, which sets its own cookies, local storage or session storage (their names start with{" "} ph_) to tell visits apart. {ANALYTICS_RULE} Those are not on this list because they are not needed for the service. When you sign up, set up your first newsletter or start a plan, FlockStack also sets fs_goal for ten minutes, which the next page reads - once to count that step and removes. It is set only while analytics is on. Change your choice any time with Cookie settings{" "} + once to count that step and removes. It is set only while analytics is on. When you press a sign-up button on our website while analytics is on, + the button's name (such as hero-primary) is added to the address of the sign-in page and kept in PostHog's session storage until + the browser session ends or you create the account, so we can see which page brought sign-ups. It is never your email address or anything you typed. Change your choice any time with Cookie settings{" "} in the footer of the home page, the product, pricing, comparison, migration and blog pages, the docs and the legal pages, and on the sign-in page. Declining deletes the PostHog cookies and local storage.

diff --git a/app/signin/page.js b/app/signin/page.js index 037943a3..1380852d 100644 --- a/app/signin/page.js +++ b/app/signin/page.js @@ -100,7 +100,7 @@ export default async function SigninPage({ searchParams }) { )} - {SiteAnalytics && } + {SiteAnalytics && } ); } diff --git a/components/marketing/ProviderAnalytics.jsx b/components/marketing/ProviderAnalytics.jsx index 25dbd1f0..98c621bb 100644 --- a/components/marketing/ProviderAnalytics.jsx +++ b/components/marketing/ProviderAnalytics.jsx @@ -13,7 +13,7 @@ import { useIsClient } from "@/lib/use-is-client"; // and passes what it read from the request). The browser applies the rules again: a privacy signal means nothing is // loaded, sent or stored, a visitor who must consent sees the banner first, and posthog-js is imported only once // they may be tracked. It renders the marker below for the server-rendered page and the banner once it is needed. -export default function ProviderAnalytics({ token, host, replay, region, requiresConsent, consent: serverConsent, identity, privacyHref = null }) { +export default function ProviderAnalytics({ token, host, replay, region, requiresConsent, consent: serverConsent, identity, signupSource = null, privacyHref = null }) { const isClient = useIsClient(); const [choice, setChoice] = useState(null); const [isSettingsOpen, setIsSettingsOpen] = useState(false); @@ -35,6 +35,7 @@ export default function ProviderAnalytics({ token, host, replay, region, require let isCurrent = true; const person = userId ? { id: userId, email: userEmail, plan: userPlan } : null; analytics.start({ token, host, replay, region, identity: person }).then(() => { + if (isCurrent && signupSource) analytics.track("signup_started", { cta: signupSource }); const goal = isCurrent ? takeGoalCookie() : null; if (goal) analytics.track(goal); }); @@ -43,7 +44,7 @@ export default function ProviderAnalytics({ token, host, replay, region, require isCurrent = false; analytics.pause(); }; - }, [isClient, status, token, host, replay, region, userId, userEmail, userPlan]); + }, [isClient, status, token, host, replay, region, userId, userEmail, userPlan, signupSource]); useEffect(() => { const signOut = () => analytics.reset(); diff --git a/components/marketing/SiteAnalytics.jsx b/components/marketing/SiteAnalytics.jsx index 735308f9..7dd3449d 100644 --- a/components/marketing/SiteAnalytics.jsx +++ b/components/marketing/SiteAnalytics.jsx @@ -3,6 +3,7 @@ import { isCloud } from "@/lib/config"; import { isLegalPublished } from "@/lib/legal"; import { getAccountPlan } from "@/lib/plans"; import { readAnalyticsEnv } from "@/lib/marketing/analytics-config"; +import { toSignupSource } from "@/lib/marketing/analytics-privacy"; import { readAnalyticsRequest } from "@/lib/marketing/analytics-request"; import { getRegion } from "@/lib/region"; @@ -10,17 +11,18 @@ import { getRegion } from "@/lib/region"; // layouts of FlockStack's own pages only: the docs, legal pages, sign-in and the signed-in dashboard. Never in the root // layout, and never on a newsletter's pages, the pages readers use, or the admin (tests/analytics-routes.test.mjs lists // where it is). The marketing website, a separate Worker, loads its own copy on its home, product, pricing, comparison, migration -// and blog pages. user and account identify a signed-in person. +// and blog pages. user and account identify a signed-in person; signupSource is the ?cta= of the sign-in address (the website's sign-up button), kept only if toSignupSource knows it. // No POSTHOG_KEY renders nothing, without waiting for the request. -export default function SiteAnalytics({ user = null, account = null }) { +export default function SiteAnalytics({ user = null, account = null, signupSource = null }) { if (!isCloud() || !readAnalyticsEnv()) return null; - return ; + return ; } -async function AnalyticsForRequest({ user, account }) { +async function AnalyticsForRequest({ user, account, signupSource }) { const request = await readAnalyticsRequest(); if (!request) return null; const { token, host, replay, requiresConsent, consent } = request; + const source = toSignupSource(signupSource); const identity = user && account ? { id: String(user.id), email: user.email, plan: getAccountPlan(account).id } : null; - return ; + return ; } diff --git a/lib/legal.js b/lib/legal.js index 9a22ced8..0f3a663e 100644 --- a/lib/legal.js +++ b/lib/legal.js @@ -181,8 +181,21 @@ export const OPTIONAL_SERVICES_IN_USE = // Where PostHog's analytics runs, and for whom. One PostHog project on PostHog US Cloud serves both deployments // (components/marketing/SiteAnalytics.jsx, lib/marketing/analytics-consent.js, and ANALYTICS_PATHS in data/site.js of the // marketing website, which serves the home, product, pricing, comparison, migration and blog pages): change them together. +// +// Session recording: SESSION_RECORDING_ON says whether the Privacy page may describe it as running. Turn it on in the same +// change as the settings: POSTHOG_SESSION_REPLAY="on" on the marketing website's Worker (recording its pages for visitors who +// pressed Accept; CRO phase 4) and, if wanted, "true" on the app's Workers (the dashboard and the pages here, every input and +// all text masked). while it is on and the retention below is still a TODO, the legal pages answer 404 like any other placeholder. +export const SESSION_RECORDING_ON = false; +export const SESSION_RECORDING_RETENTION = "TODO: PostHog's recording retention for FlockStack's project, as set in PostHog"; +const SESSION_RECORDING_SENTENCE = SESSION_RECORDING_ON + ? `Session recording runs only for visitors who pressed Accept (never where analytics merely runs by default), with every input hidden, and a Global Privacy Control or Do Not Track signal stops it. Retention: ${SESSION_RECORDING_RETENTION}` + : "Session recording is off."; +// Besides page views and clicks: which sign-up button on the website led to a sign-in page and the account created from it +// (a short fixed name such as "hero-primary", kept for that browser session only), that the cost calculator was used (never +// what was entered), and which wording of a headline or button was shown on a page that tests wording. export const ANALYTICS_RULE = - "One PostHog project, on PostHog US Cloud, serves both flockstack.io and eu.flockstack.io. It runs on FlockStack's own pages only (the home page, the product, pricing, comparison, migration and blog pages, the docs, the legal pages, sign-in and the dashboard), never on a newsletter's public pages or on the pages readers use (the archive, unsubscribe, confirm, keep, referral and comment pages, and the recommendations directory). It uses its own cookies or local storage, so it runs only after you accept if you are in the EU, the EEA, the UK or Switzerland, or on eu.flockstack.io. Elsewhere it runs unless you decline in Cookie settings. A Global Privacy Control or Do Not Track signal from your browser turns it off, without a banner. Session recording is off."; + `One PostHog project, on PostHog US Cloud, serves both flockstack.io and eu.flockstack.io. It runs on FlockStack's own pages only (the home page, the product, pricing, comparison, migration and blog pages, the docs, the legal pages, sign-in and the dashboard), never on a newsletter's public pages or on the pages readers use (the archive, unsubscribe, confirm, keep, referral and comment pages, and the recommendations directory). It uses its own cookies, local storage or session storage, so it runs only after you accept if you are in the EU, the EEA, the UK or Switzerland, or on eu.flockstack.io. Elsewhere it runs unless you decline in Cookie settings. A Global Privacy Control or Do Not Track signal from your browser turns it off, without a banner. Besides page views and clicks it counts which sign-up button on our website you pressed and the account you then create, that you used the cost calculator (never what you entered), and which wording you were shown on a page that tests wording. ${SESSION_RECORDING_SENTENCE}`; // What a newsletter's website counts about its visitors, for its owner's reports (lib/web-analytics.js, components/TrackVisit.jsx): // change them together. This is the newsletters' own pages; FlockStack's analytics above is only FlockStack's. @@ -202,6 +215,7 @@ const VALUES = { OPTIONAL_SERVICES_IN_USE, ANALYTICS_RULE, WEB_ANALYTICS_RULE, + ...(SESSION_RECORDING_ON && { SESSION_RECORDING_RETENTION }), SUPPORT_CHAT_STORED, SUPPORT_CHAT_RETENTION, AI_WRITING_STORED, diff --git a/lib/marketing/analytics-client.js b/lib/marketing/analytics-client.js index 341b8e3f..76194bef 100644 --- a/lib/marketing/analytics-client.js +++ b/lib/marketing/analytics-client.js @@ -123,6 +123,7 @@ export function createAnalytics({ load = () => import("posthog-js").then((module const event = toEvent(goal, props); if (!client || !isWanted || !event) return false; client.capture(event.name, event.props); + if (event.name === "signup_started" && event.props.cta) client.register_for_session({ signup_cta: event.props.cta }); return true; } diff --git a/lib/marketing/analytics-privacy.js b/lib/marketing/analytics-privacy.js index 2b7e2a8b..f540e811 100644 --- a/lib/marketing/analytics-privacy.js +++ b/lib/marketing/analytics-privacy.js @@ -3,8 +3,34 @@ // listed, and everything PostHog collects by itself (page views, clicks) is scrubbed of text and query strings before it // leaves the browser (sanitizeEvent). tests/analytics-privacy.test.mjs checks the names. +// The buttons of the marketing website that lead to sign-in, by the fixed name it gives them (data-cta, and ?cta= on the +// sign-in address once a visitor who may be tracked presses one). The same list as SIGNUP_SOURCES in the website's +// lib/signup-sources.js (docs/reference/signup-sources.js there): change them together. Anything else is not a source. +export const SIGNUP_SOURCES = Object.freeze([ + "header-signup", + "hero-primary", + "start-section", + "pricing-summary", + "plan-free", + "plan-starter", + "plan-growth", + "plan-pro", + "plan-scale", + "plan-max", + "sticky-compare", + "post-next-signup", + "footer-signup", +]); + +// The source in a ?cta= value, or null for anything not on the list. +export function toSignupSource(value) { + return typeof value === "string" && SIGNUP_SOURCES.includes(value) ? value : null; +} + // The explicit events, each with the properties it may carry and the values they may take. track() drops the rest. export const EVENTS = Object.freeze({ + // A visitor who pressed a sign-up button on the website reached the sign-in page. + signup_started: { cta: SIGNUP_SOURCES }, signup_completed: {}, newsletter_created: {}, campaign_sent: { when: ["now", "scheduled"] }, @@ -20,8 +46,13 @@ export const PERSON_PROPERTIES = Object.freeze(["email", "plan"]); // On every event: which deployment the visitor was on. export const SUPER_PROPERTIES = Object.freeze(["region"]); +// On the events of one browser session only, from the sign-in page on (posthog-js register_for_session): the sign-up button +// that brought the visitor, so that signup_completed says where the account came from. +export const SESSION_PROPERTIES = Object.freeze(["signup_cta"]); + // The steps the server reports for the browser's next page (siteExtensions.sendGoalOnNextPage), as events. export const GOAL_EVENTS = Object.freeze({ + signup_started: "signup_started", signup_completed: "signup_completed", onboarding_completed: "newsletter_created", newsletter_created: "newsletter_created", diff --git a/lib/subprocessors.js b/lib/subprocessors.js index 712bf8b0..4fd16629 100644 --- a/lib/subprocessors.js +++ b/lib/subprocessors.js @@ -1,4 +1,4 @@ -import { SES_REGIONS, getElevenLabsStatus } from "./legal.js"; +import { SES_REGIONS, SESSION_RECORDING_ON, getElevenLabsStatus } from "./legal.js"; // Creator-chosen external content is loaded by the reader's browser, not sent through FlockStack's database. export const WEBSITE_ICON_NOTICE = "Website icons load from the website linked by the creator, with Google's favicon service as a fallback. The image host receives the browser's request, including its IP address. Icon requests omit the page's referrer."; @@ -50,8 +50,8 @@ export const SUBPROCESSORS = [ { name: "PostHog", link: "https://posthog.com/privacy", - purpose: "FlockStack's own product analytics, to see how people use FlockStack's pages. Only on those pages: the home page, the product, pricing, comparison, migration and blog pages, the docs, the legal pages, sign-in and the dashboard. Never on a newsletter's public pages or the pages readers use. It uses its own cookies or local storage. One PostHog project serves both flockstack.io and eu.flockstack.io.", - data: "Page views, clicks and actions, device and browser, approximate location, and for signed-in people the user ID, email address and plan. Session recording is off. Never a newsletter's content or a subscriber's data.", + purpose: "FlockStack's own product analytics, to see how people use FlockStack's pages. Only on those pages: the home page, the product, pricing, comparison, migration and blog pages, the docs, the legal pages, sign-in and the dashboard. Never on a newsletter's public pages or the pages readers use. It uses its own cookies, local storage or session storage. One PostHog project serves both flockstack.io and eu.flockstack.io.", + data: `Page views, clicks and actions, device and browser, approximate location, which sign-up button on the website was pressed and the account then created, that the cost calculator was used (never what was entered), which wording of a page that tests wording was shown, and for signed-in people the user ID, email address and plan. ${SESSION_RECORDING_ON ? "Session recording runs only for visitors who pressed Accept, with every input hidden." : "Session recording is off."} Never a newsletter's content or a subscriber's data.`, us: "PostHog US Cloud. It runs unless the visitor declines in Cookie settings; visitors in the EU, the EEA, the UK and Switzerland are asked first. A Global Privacy Control or Do Not Track signal turns it off.", eu: "The same PostHog project on PostHog US Cloud, so events from eu.flockstack.io are processed in the United States, not in the EU. It runs only after the visitor accepts. A Global Privacy Control or Do Not Track signal turns it off.", }, diff --git a/tests/analytics-client.test.mjs b/tests/analytics-client.test.mjs index 4e9417ec..6ea1d165 100644 --- a/tests/analytics-client.test.mjs +++ b/tests/analytics-client.test.mjs @@ -34,6 +34,7 @@ function fakePosthog({ optedOut = false, distinctId = "anon-1" } = {}) { identify: record("identify"), setPersonProperties: record("setPersonProperties"), register: record("register"), + register_for_session: record("register_for_session"), capture: record("capture"), reset: record("reset"), stopSessionRecording: record("stopSessionRecording"), @@ -188,6 +189,21 @@ describe("explicit events", () => { ); }); + test("signup_started keeps the website's sign-up button for this browser session, so signup_completed can say where the account came from", async () => { + const { analytics, posthog } = setup(); + await analytics.start(SETTINGS); + assert.equal(analytics.track("signup_started", { cta: "hero-primary", email: "ada@example.com" }), true); + assert.equal(analytics.track("signup_started", { cta: "not-a-button" }), true); + assert.deepEqual( + posthog.calls.filter(([name]) => ["capture", "register_for_session"].includes(name)), + [ + ["capture", "signup_started", { cta: "hero-primary" }], + ["register_for_session", { signup_cta: "hero-primary" }], + ["capture", "signup_started", {}], + ] + ); + }); + test("reportGoal on the window is what ProviderAnalytics turns into track", () => { const events = []; const fakeWindow = { dispatchEvent: (event) => events.push(event) }; diff --git a/tests/analytics-privacy.test.mjs b/tests/analytics-privacy.test.mjs index 83d38330..a9f9b4df 100644 --- a/tests/analytics-privacy.test.mjs +++ b/tests/analytics-privacy.test.mjs @@ -8,12 +8,15 @@ import { FORBIDDEN_PROPERTY_PATTERN, GOAL_EVENTS, PERSON_PROPERTIES, + SESSION_PROPERTIES, + SIGNUP_SOURCES, SUPER_PROPERTIES, isTrackedPath, sanitizeEvent, stripUrl, toEvent, toPersonProperties, + toSignupSource, } from "../lib/marketing/analytics-privacy.js"; // What FlockStack's PostHog may carry: no newsletter content, no subscriber or contact data. The names of every property @@ -34,7 +37,7 @@ const pageview = (url, extra = {}) => ({ event: "$pageview", properties: { $curr describe("the property names", () => { test("an event carries at most a few fixed properties, none of them named for content or contacts", () => { - assert.deepEqual(Object.keys(EVENTS), ["signup_completed", "newsletter_created", "campaign_sent", "plan_started", "support_opened", "support_answered"]); + assert.deepEqual(Object.keys(EVENTS), ["signup_started", "signup_completed", "newsletter_created", "campaign_sent", "plan_started", "support_opened", "support_answered"]); for (const [event, properties] of Object.entries(EVENTS)) { for (const [name, values] of Object.entries(properties)) { assert.doesNotMatch(name, FORBIDDEN_PROPERTY_PATTERN, `${event}.${name}`); @@ -65,6 +68,33 @@ describe("the property names", () => { }); }); +describe("where a sign-up came from", () => { + test("only a button the website names counts, each a short fixed name", () => { + assert.ok(SIGNUP_SOURCES.length > 0); + for (const name of SIGNUP_SOURCES) assert.match(name, /^[a-z0-9][a-z0-9-]{0,18}$/, name); + assert.equal(new Set(SIGNUP_SOURCES).size, SIGNUP_SOURCES.length); + assert.equal(toSignupSource("hero-primary"), "hero-primary"); + for (const value of ["", "Hero-Primary", "hero-primary?x=1", "ada@example.com", "constructor", "__proto__", undefined, null, ["hero-primary"], 5]) assert.equal(toSignupSource(value), null, String(value)); + }); + + test("signup_started carries that name and nothing else, and the session keeps one property for it", () => { + assert.deepEqual(EVENTS.signup_started, { cta: SIGNUP_SOURCES }); + assert.deepEqual(toEvent("signup_started", { cta: "plan-starter", email: "ada@example.com" }), { name: "signup_started", props: { cta: "plan-starter" } }); + assert.deepEqual(toEvent("signup_started", { cta: "ada@example.com" }), { name: "signup_started", props: {} }); + assert.deepEqual(SESSION_PROPERTIES, ["signup_cta"]); + for (const name of SESSION_PROPERTIES) assert.doesNotMatch(name, FORBIDDEN_PROPERTY_PATTERN); + }); + + test("the sign-in page reaches PostHog through the extension with the raw ?cta=, which SiteAnalytics checks", async () => { + const page = await readFile(join(ROOT, "app/signin/page.js"), "utf8"); + assert.match(page, //); + assert.doesNotMatch(page, /lib\/marketing/); + const site = await readFile(join(ROOT, "components/marketing/SiteAnalytics.jsx"), "utf8"); + assert.match(site, /toSignupSource\(signupSource\)/); + assert.match(site, /\.\.\.\(source && \{ signupSource: source \}\)/); + }); +}); + describe("track and the goals", () => { test("keep a listed event and its listed properties, and drop everything else", () => { assert.deepEqual(toEvent("campaign_sent", { when: "now" }), { name: "campaign_sent", props: { when: "now" } }); @@ -76,7 +106,7 @@ describe("track and the goals", () => { }); test("ignore a step that isn't counted", () => { - for (const goal of ["page_viewed", "constructor", "__proto__", "toString", "", undefined, null, "signup_started"]) assert.equal(toEvent(goal, {}), null, String(goal)); + for (const goal of ["page_viewed", "constructor", "__proto__", "toString", "", undefined, null, "signup_failed"]) assert.equal(toEvent(goal, {}), null, String(goal)); }); test("the code calls them only with names from the list", async () => { @@ -91,7 +121,7 @@ describe("track and the goals", () => { } } } - for (const name of ["campaign_sent", "newsletter_created", "plan_started", "signup_completed", "onboarding_completed"]) assert.ok(names.has(name), `${name} is reported somewhere`); + for (const name of ["campaign_sent", "newsletter_created", "plan_started", "signup_completed", "onboarding_completed", "signup_started"]) assert.ok(names.has(name), `${name} is reported somewhere`); }); test("posthog-js is imported in one file, and nothing else captures an event or sets a person's property", async () => { diff --git a/tests/legal.test.mjs b/tests/legal.test.mjs index a3138603..99a24d59 100644 --- a/tests/legal.test.mjs +++ b/tests/legal.test.mjs @@ -141,6 +141,9 @@ describe("the legal pages", () => { "it runs only after you accept if you are in the EU, the EEA, the UK or Switzerland, or on eu.flockstack.io", "A Global Privacy Control or Do Not Track signal from your browser turns it off", "Session recording is off", + "which sign-up button on our website you pressed", + "that you used the cost calculator (never what you entered)", + "which wording you were shown on a page that tests wording", "user ID, email address and plan", "fs_consent", "Cookie settings", @@ -161,6 +164,15 @@ describe("the legal pages", () => { assert.doesNotMatch(processors + privacy, /PostHog EU Cloud/); }); + test("the policy says session recording is off while it is off, and a recording that runs cannot be published without its retention", async () => { + assert.equal(m.legal.SESSION_RECORDING_ON, false); + assert.match(m.legal.ANALYTICS_RULE, /Session recording is off\.$/); + assert.deepEqual(m.legal.getLegalTodos().map(([name]) => name).filter((name) => name === "SESSION_RECORDING_RETENTION"), []); + const source = await readFile(join(ROOT, "lib/legal.js"), "utf8"); + assert.match(source, /\.\.\.\(SESSION_RECORDING_ON && \{ SESSION_RECORDING_RETENTION \}\)/); + assert.match(m.legal.SESSION_RECORDING_RETENTION, /^TODO:/); + }); + test("Sign in with Apple is described as in use, with what Apple gets and what FlockStack keeps", () => { const privacy = textOf(renderPage("privacy")); const processors = textOf(renderPage("sub-processors"));