@sprid/cli 0.1.13 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  sprid follows semver: a breaking change to a command's arguments, its output
4
4
  shape or its exit codes is a major release.
5
5
 
6
+ ## 0.1.15 - 2026-09-27
7
+
8
+ - `sprid marketing-review save --app <slug> --file <review.json>` saves a
9
+ finished review's verdict and ranked agenda to the app, where the Reviews
10
+ page, the Monday mail and the next review read it. The full report stays in
11
+ the repo. `sprid marketing-review history --app <slug>` lists past reviews.
12
+
13
+ ## 0.1.14 - 2026-09-27
14
+
15
+ - `sprid docs posthog` explains how to keep your own testing out of the numbers:
16
+ flag your own accounts `is_internal`, list your own devices under `exclude`,
17
+ and what Sprid already drops. Signups (the old Registrations) now count from
18
+ the tracking start date when a period begins before it, and read as zero
19
+ rather than unavailable once that date is set.
20
+
6
21
  ## 0.1.13 - 2026-09-27
7
22
 
8
23
  - `sprid post` lanes can target Pinterest: a `pinterest` block (board, link with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprid/cli",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "The Sprid command line: connect your app, review results, prepare posts and store screenshots, and release mobile apps.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -7,9 +7,9 @@ import { saveEvidence } from '../evidence.mjs';
7
7
 
8
8
  export async function marketingReview(ctx) {
9
9
  const local = readLocalApp(ctx.cwd)?.data;
10
- if ((local?.enabled === false || local?.review?.enabled === false) && !['context', 'collection', 'evidence'].includes(ctx.positionals[0])) throw new UsageError(`Review paused: ${local.review?.disabledReason || local.disabledReason || local.slug}`);
11
- if (ctx.positionals.length > 1 || (ctx.positionals.length && !['query', 'capabilities', 'context', 'collection', 'evidence'].includes(ctx.positionals[0]))) throw new UsageError('Use sprid marketing-review [capabilities | query | context | collection | evidence]');
12
- if (['context', 'collection', 'evidence'].includes(ctx.positionals[0])) return reviewSettings(ctx);
10
+ if ((local?.enabled === false || local?.review?.enabled === false) && !['context', 'collection', 'evidence', 'history'].includes(ctx.positionals[0])) throw new UsageError(`Review paused: ${local.review?.disabledReason || local.disabledReason || local.slug}`);
11
+ if (ctx.positionals.length > 1 || (ctx.positionals.length && !['query', 'capabilities', 'context', 'collection', 'evidence', 'history', 'save'].includes(ctx.positionals[0]))) throw new UsageError('Use sprid marketing-review [capabilities | query | context | collection | evidence | history | save]');
12
+ if (['context', 'collection', 'evidence', 'history', 'save'].includes(ctx.positionals[0])) return reviewSettings(ctx);
13
13
  if (ctx.positionals[0] === 'capabilities' || (ctx.positionals[0] === 'query' && ctx.flags.source)) return connectedQuery(ctx);
14
14
  if (ctx.positionals[0] === 'query') {
15
15
  if (!ctx.flags['query-file']) throw new UsageError('sprid marketing-review query --query-file <file.sql> --app <slug>');
@@ -88,6 +88,19 @@ async function reviewSettings(ctx) {
88
88
  ctx.out({ ...result, localEvidence: saveEvidence(ctx.cwd, 'snapshot', result) });
89
89
  return 0;
90
90
  }
91
+ if (ctx.positionals[0] === 'history') {
92
+ ctx.out(await ctx.api().get(`/api/marketing-review/history?${new URLSearchParams({ workspaceId: String(wid), app: profile.slug })}`));
93
+ return 0;
94
+ }
95
+ if (ctx.positionals[0] === 'save') {
96
+ if (!ctx.flags.file) throw new UsageError('Use marketing-review save --app <slug> --file <review.json> with {verdict, agenda:[{title, why}], period:{start,end}, reportPath}');
97
+ let input;
98
+ try { input = JSON.parse(readFileSync(resolve(ctx.cwd, ctx.flags.file), 'utf8')); } catch { throw new UsageError('Review file must contain JSON with a verdict and a ranked agenda'); }
99
+ if (typeof input?.verdict !== 'string' || !input.verdict.trim()) throw new UsageError('Review file needs a verdict');
100
+ const response = await ctx.api().raw('POST', `/api/marketing-review/history?${new URLSearchParams({ workspaceId: String(wid), app: profile.slug })}`, { surface: 'repo', ...input });
101
+ ctx.out(response.data);
102
+ return response.status >= 200 && response.status < 300 ? 0 : 1;
103
+ }
91
104
  if (ctx.positionals[0] === 'collection') {
92
105
  if (!['true', 'false'].includes(ctx.flags.enabled)) throw new UsageError('Use marketing-review collection --enabled true|false --app <slug>');
93
106
  ctx.out(await patchProfile(ctx, wid, profile.id, { metricsCollectionEnabled: ctx.flags.enabled === 'true' }));
@@ -92,6 +92,8 @@ export const COMMAND_GROUPS = [
92
92
  ['marketing-review', 'sprid marketing-review --view summary|full [--sources gsc,posthog] [--refresh|--cached-only]', 'Summary is a compact entry point; full retains evidence. Exact-window snapshots name freshness. Refresh forces provider reads; cached-only makes none. Daily collection refreshes connected sources automatically for active plans.'],
93
93
  ['marketing-review', 'sprid marketing-review evidence --app <slug> --id <evidence.id>', 'Read the exact app-scoped source snapshot without provider calls, including after key rotation. Saved privately for 90 days; this command also archives the receipt locally.'],
94
94
  ['marketing-review', 'sprid marketing-review context --app <slug> [--file <context.json>]', 'Read shared review memory, or save {baseRevision,context}. Context holds definitions, investigations, decisions, corrections and release references. Concurrent changes return a conflict without overwriting. Explicitly select context to share; never upload keys or raw customer records.'],
95
+ ['marketing-review', 'sprid marketing-review save --app <slug> --file <review.json>', 'Save a finished review to the app: {verdict, agenda:[{title, why}], period:{start,end}, reportPath}. Verdict and agenda only; the full report stays in the repo. A second save the same day replaces that day’s review.'],
96
+ ['marketing-review', 'sprid marketing-review history --app <slug>', 'Past saved reviews, newest first, and when the review numbers were last read.'],
95
97
  ['marketing-review', 'sprid marketing-review collection --app <slug> --enabled true|false', 'Enable or pause daily connected-source collection. Stored evidence remains readable; this grants no publishing or model-spending permission.'],
96
98
  ['marketing-review', 'sprid marketing-review capabilities --app <slug> [--source <source>]', 'Discover read operations, parameter schemas, examples and missing setup for every connected source. Configuration is not a live credential check.'],
97
99
  ['marketing-review', 'sprid marketing-review query --app <slug> --source <source> --operation <operation> [--params-file <params.json>]', 'Run a discovered read through Sprid-held credentials. Parameters are a JSON object; resource IDs come from the App Profile. Continue with returned next.params and keep coverage/truncation in your report.'],
@@ -358,7 +358,7 @@ export const GUIDES = [
358
358
  "id": "metric-definitions",
359
359
  "title": "Metric definitions",
360
360
  "kind": "detail",
361
- "markdown": "In **Settings → Apps → your app → PostHog**, set the account-created event and the first UTC date from which tracking is complete.\n\n| Metric | What counts |\n|---|---|\n| Registrations | First `sprid_signup` event per PostHog person, across all surfaces |\n| New product users | First product activity, including anonymous people |\n| Active product users | Distinct people with product activity in the period |\n| Web visitors | Distinct pageview visitors on the saved website hostname, minus your product |\n\n**Registrations.** Send `sprid_signup` from your server after the account is created, with the account id as `distinct_id`, and identify that id in your clients. Never fire it on login, page load or install. An existing `posthogEvents.signup` mapping also works; `posthogConfig.registration.event` wins over it. No matching event history reads as unavailable; history with no new registrations reads as zero. Periods before the tracking start are unavailable, and comparisons crossing it, or with no confirmed start, are withheld. Backfill with the original creation timestamps.\n\n### What counts as your product\n\nSet this under **What counts as your product** on the same screen. It decides who is an active person, and it is the single setting most likely to make the people card wrong.\n\n| Your product is | Choose | Also give |\n|---|---|---|\n| An iOS or Android app | **A phone app** | Nothing. This is the default |\n| A web app on its own subdomain | **A website or web app** | The hostnames, such as `app.example.com` |\n| A web app under a path on your marketing domain | **A website or web app** | The paths, such as `/app`, `/dashboard` |\n| A phone app with a web client | **Both** | The web hostnames or paths |\n\n**A web product must say where it lives.** Your marketing pages and your product are the same PostHog events; without a hostname or a path there is nothing to tell them apart, and every visitor would be counted as someone using the product. Sprid refuses that configuration rather than reporting it.\n\nWhatever you name here is **subtracted from your website visitor numbers**, so a page inside the product is never also counted as a visit.\n\n**A phone app** needs `posthog-react-native` on iOS, iPadOS or Android and rejects explicit web surfaces: set `app_surface: 'web'` on Expo web and `'native'` on devices. Web counts need a browser and exclude reported bots and crawler user agents. Every metric excludes events or people with `is_internal`, `is_test` or `sprid_test = true`. What remains is observed identities, not guaranteed humans.\n\n### Custom properties\n\nAdvanced rules go under **Custom properties**:\n\n```json\n{\"registration\":{\"identity\":{\"scope\":\"event\",\"property\":\"account_id\"}},\"app\":{\"kind\":\"web\",\"hosts\":[\"app.example.com\"]},\"exclude\":[{\"scope\":\"person\",\"property\":\"staff\",\"operator\":\"in\",\"values\":[true]}]}\n```\n\n- `app.kind` is `native`, `web` or `both`, with `app.hosts` and `app.pathPrefixes` saying where a web product lives. This is what the screen above writes.\n- `app.filters` replaces the whole definition with your own rule; all filters must match. Choosing it shows as **Custom rules** on the screen, and the rule is subtracted from web visitors the same way.\n- `exclude` removes matching traffic from every metric.\n- `registration.filters` narrows the registration event, for example `result = success`.\n- `registration.accounts: false` says the app has no accounts (a local-first app, or one that identifies by device). Registrations then read as not applicable instead of a missing event, and the weekly email stops listing them as unread.\n- `registration.identity` defaults to `person_id`. A configured property must be present and should never change, because it counts accounts.\n- Each rule takes `scope: event|person`, `operator: in|not_in` and string, number or boolean `values`. A missing property fails `in` and passes `not_in`. Property names are literal keys, dots included.\n\nThe same object is `posthogConfig` in REST `PATCH /api/app-profiles/:id`, MCP `upsert_app_profile` and the App Profile JSON used by `sprid init`, where `registration.event` and `registration.since` also live. No credentials or SQL belong in it. After saving, fetch Insights and check its registration notes and totals against your database before trusting a funnel."
361
+ "markdown": "In **Settings → Apps → your app → PostHog**, set the account-created event and the first UTC date from which tracking is complete.\n\n| Metric | What counts |\n|---|---|\n| Signups | First `sprid_signup` event per PostHog person, across all surfaces |\n| New product users | First product activity, including anonymous people |\n| Active product users | Distinct people with product activity in the period |\n| Web visitors | Distinct pageview visitors on the saved website hostname, minus your product |\n\n**Signups.** Send `sprid_signup` from your server after the account is created, with the account id as `distinct_id`, and identify that id in your clients. Never fire it on login, page load or install. An existing `posthogEvents.signup` mapping also works; `posthogConfig.registration.event` wins over it. With no tracking start date, no matching event history reads as unavailable; with one, it reads as zero signups. A period that starts before the tracking date is counted from that date and says so. Comparisons crossing it, or with no confirmed start, are withheld. Backfill with the original creation timestamps.\n\nWhen an app reports signups, they are its user count on Home and in Insights. First product activity counts anonymous devices, so it moves into that card's detail.\n\n### What counts as your product\n\nSet this under **What counts as your product** on the same screen. It decides who is an active person, and it is the single setting most likely to make the people card wrong.\n\n| Your product is | Choose | Also give |\n|---|---|---|\n| An iOS or Android app | **A phone app** | Nothing. This is the default |\n| A web app on its own subdomain | **A website or web app** | The hostnames, such as `app.example.com` |\n| A web app under a path on your marketing domain | **A website or web app** | The paths, such as `/app`, `/dashboard` |\n| A phone app with a web client | **Both** | The web hostnames or paths |\n\n**A web product must say where it lives.** Your marketing pages and your product are the same PostHog events; without a hostname or a path there is nothing to tell them apart, and every visitor would be counted as someone using the product. Sprid refuses that configuration rather than reporting it.\n\nWhatever you name here is **subtracted from your website visitor numbers**, so a page inside the product is never also counted as a visit.\n\n**A phone app** needs `posthog-react-native` on iOS, iPadOS or Android and rejects explicit web surfaces: set `app_surface: 'web'` on Expo web and `'native'` on devices. Web counts need a browser and exclude reported bots and crawler user agents. Every metric excludes events or people with `is_internal`, `is_test` or `sprid_test = true`. What remains is observed identities, not guaranteed humans.\n\n### Keep your own testing out\n\nOn a new app, your own phone, simulators and store reviewers can outnumber real people. Sprid already drops Google Play's pre-launch test devices, events reporting `$is_emulator`, reported bots, and anything flagged `is_internal`, `is_test` or `sprid_test`. Two things are yours to add:\n\n- **Flag your own accounts.** After sign-in, register `is_internal: true` when the account's email is on your own domain, store-review accounts included, and set it on the person if you identify. A web client that never identifies has to put it on every event, before the first pageview. Do the same on the server event that records a signup.\n- **List your own devices** under `exclude`, for what happens before sign-in, such as a fresh install. Open your app, then read the newest events in PostHog for the `$device_name` values:\n\n```json\n{\"exclude\":[{\"scope\":\"event\",\"property\":\"$device_name\",\"operator\":\"in\",\"values\":[\"Pixel 8\",\"Simulator iOS\",\"sdk_gphone64_arm64\"]}]}\n```\n\nApple's review devices have reported `$device_name` `iPhone99,7`, with locale `en-US` and time zone `US/Pacific`, in every app we checked (September 2026). Add it if it shows up in yours.\n\n### Custom properties\n\nAdvanced rules go under **Custom properties**:\n\n```json\n{\"registration\":{\"identity\":{\"scope\":\"event\",\"property\":\"account_id\"}},\"app\":{\"kind\":\"web\",\"hosts\":[\"app.example.com\"]},\"exclude\":[{\"scope\":\"person\",\"property\":\"staff\",\"operator\":\"in\",\"values\":[true]}]}\n```\n\n- `app.kind` is `native`, `web` or `both`, with `app.hosts` and `app.pathPrefixes` saying where a web product lives. This is what the screen above writes.\n- `app.filters` replaces the whole definition with your own rule; all filters must match. Choosing it shows as **Custom rules** on the screen, and the rule is subtracted from web visitors the same way.\n- `exclude` removes matching traffic from every metric.\n- `registration.filters` narrows the registration event, for example `result = success`.\n- `registration.accounts: false` says the app has no accounts (a local-first app, or one that identifies by device). Registrations then read as not applicable instead of a missing event, and the weekly email stops listing them as unread.\n- `registration.identity` defaults to `person_id`. A configured property must be present and should never change, because it counts accounts.\n- Each rule takes `scope: event|person`, `operator: in|not_in` and string, number or boolean `values`. A missing property fails `in` and passes `not_in`. Property names are literal keys, dots included.\n\nThe same object is `posthogConfig` in REST `PATCH /api/app-profiles/:id`, MCP `upsert_app_profile` and the App Profile JSON used by `sprid init`, where `registration.event` and `registration.since` also live. No credentials or SQL belong in it. After saving, fetch Insights and check its registration notes and totals against your database before trusting a funnel."
362
362
  },
363
363
  {
364
364
  "id": "review-suspected-automated-traffic",
@@ -391,8 +391,8 @@ export const GUIDES = [
391
391
  "markdown": "Setup and live reads checked 2026-09-09.\n\n- [PostHog personal API keys](https://posthog.com/docs/api/personal-api-keys)\n- [Project identity endpoint](https://posthog.com/docs/api/projects)\n- [HogQL query endpoint](https://posthog.com/docs/api/query)\n- [SDK and framework guides](https://posthog.com/docs/libraries)\n- [React Native screen tracking](https://posthog.com/docs/libraries/react-native)\n- [Identifying users](https://posthog.com/docs/product-analytics/identify)\n- [Funnels](https://posthog.com/docs/product-analytics/funnels)"
392
392
  }
393
393
  ],
394
- "markdown": "# PostHog\n\n**What Sprid does with this:** See how people use your app and where they stop.\n\n## You need\n\nAccess to your app’s PostHog project and permission to create a **personal API key**. Your app must already send events to PostHog; connecting Sprid adds no tracking. The public key inside your app does not work here.\n\n## Click path (us.posthog.com or eu.posthog.com)\n\n1. **Settings → Account → Personal API keys → Create personal API key**, named `Sprid <app name>`.\n2. Under access, select **Projects** and choose your app’s project. Leave **All access** off.\n3. Select **Query → Read** and **Project → Read**. Leave write access off.\n4. Create the key and keep the dialog open: PostHog shows it once. Use a separate key per app.\n5. Copy the key and leave it on your clipboard.\n\n## Project id and host\n\n- **Project id:** the number after `/project/` in your PostHog address, such as `12345`.\n- **Host:** `eu` for `eu.posthog.com`, `us` for `us.posthog.com`, or your full self-hosted address, whichever you open the dashboard on.\n\n## Then run\n\n```sh\nsprid connect posthog --app myapp --key-from-clipboard --project 12345 --host eu\n```\n\nUse your own app slug, project id and host. Add `--workspace <slug>` if needed. Keep the key out of chat.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead.\n\n## How to check it worked\n\nAsk your agent: “Check that Sprid can read recent PostHog events for this app, and confirm the project is correct.” The check reports recent activity or explains why there is none; a saved connection alone does not prove events arrive.\n\n## Metric definitions\n\nIn **Settings → Apps → your app → PostHog**, set the account-created event and the first UTC date from which tracking is complete.\n\n| Metric | What counts |\n|---|---|\n| Registrations | First `sprid_signup` event per PostHog person, across all surfaces |\n| New product users | First product activity, including anonymous people |\n| Active product users | Distinct people with product activity in the period |\n| Web visitors | Distinct pageview visitors on the saved website hostname, minus your product |\n\n**Registrations.** Send `sprid_signup` from your server after the account is created, with the account id as `distinct_id`, and identify that id in your clients. Never fire it on login, page load or install. An existing `posthogEvents.signup` mapping also works; `posthogConfig.registration.event` wins over it. No matching event history reads as unavailable; history with no new registrations reads as zero. Periods before the tracking start are unavailable, and comparisons crossing it, or with no confirmed start, are withheld. Backfill with the original creation timestamps.\n\n### What counts as your product\n\nSet this under **What counts as your product** on the same screen. It decides who is an active person, and it is the single setting most likely to make the people card wrong.\n\n| Your product is | Choose | Also give |\n|---|---|---|\n| An iOS or Android app | **A phone app** | Nothing. This is the default |\n| A web app on its own subdomain | **A website or web app** | The hostnames, such as `app.example.com` |\n| A web app under a path on your marketing domain | **A website or web app** | The paths, such as `/app`, `/dashboard` |\n| A phone app with a web client | **Both** | The web hostnames or paths |\n\n**A web product must say where it lives.** Your marketing pages and your product are the same PostHog events; without a hostname or a path there is nothing to tell them apart, and every visitor would be counted as someone using the product. Sprid refuses that configuration rather than reporting it.\n\nWhatever you name here is **subtracted from your website visitor numbers**, so a page inside the product is never also counted as a visit.\n\n**A phone app** needs `posthog-react-native` on iOS, iPadOS or Android and rejects explicit web surfaces: set `app_surface: 'web'` on Expo web and `'native'` on devices. Web counts need a browser and exclude reported bots and crawler user agents. Every metric excludes events or people with `is_internal`, `is_test` or `sprid_test = true`. What remains is observed identities, not guaranteed humans.\n\n### Custom properties\n\nAdvanced rules go under **Custom properties**:\n\n```json\n{\"registration\":{\"identity\":{\"scope\":\"event\",\"property\":\"account_id\"}},\"app\":{\"kind\":\"web\",\"hosts\":[\"app.example.com\"]},\"exclude\":[{\"scope\":\"person\",\"property\":\"staff\",\"operator\":\"in\",\"values\":[true]}]}\n```\n\n- `app.kind` is `native`, `web` or `both`, with `app.hosts` and `app.pathPrefixes` saying where a web product lives. This is what the screen above writes.\n- `app.filters` replaces the whole definition with your own rule; all filters must match. Choosing it shows as **Custom rules** on the screen, and the rule is subtracted from web visitors the same way.\n- `exclude` removes matching traffic from every metric.\n- `registration.filters` narrows the registration event, for example `result = success`.\n- `registration.accounts: false` says the app has no accounts (a local-first app, or one that identifies by device). Registrations then read as not applicable instead of a missing event, and the weekly email stops listing them as unread.\n- `registration.identity` defaults to `person_id`. A configured property must be present and should never change, because it counts accounts.\n- Each rule takes `scope: event|person`, `operator: in|not_in` and string, number or boolean `values`. A missing property fails `in` and passes `not_in`. Property names are literal keys, dots included.\n\nThe same object is `posthogConfig` in REST `PATCH /api/app-profiles/:id`, MCP `upsert_app_profile` and the App Profile JSON used by `sprid init`, where `registration.event` and `registration.since` also live. No credentials or SQL belong in it. After saving, fetch Insights and check its registration notes and totals against your database before trusting a funnel.\n\n## Review suspected automated traffic\n\nRead [Check whether a traffic spike is real](https://sprid.studio/docs/traffic) (`sprid docs traffic`) before saving a rule: a spike or a shared fingerprint alone does not prove bots.\n\nOpen **Web visitors → Review traffic exclusions**, or **Settings → Apps → your app → PostHog → Review traffic**. Pick a UTC date range and a traffic group, preview the effect, then save. Agents use MCP `review_traffic` or `POST /api/app-profiles/:ref/traffic-review?workspaceId=…` with `{\"start\":\"2026-09-18\",\"end\":\"2026-09-19\"}`: end dates exclusive, UTC, at most 31 days. An optional `exclusion` previews a rule against this period and the one before. Save approved rules in `posthogConfig.trafficExclusions` through `upsert_app_profile` or the profile PATCH route, keeping the rest of the configuration. Sprid records who edited it and when.\n\nHow exclusions apply:\n\n- App-specific and date-bounded. Properties inside a rule are AND; enabled rules are OR.\n- Applied to website totals, charts, breakdowns, social attribution and weekly website counts, comparison period included. Remaining visitors are recounted as distinct people, never subtracted.\n- Native app activity and registrations are unchanged, and PostHog’s data is never changed or deleted. **Restore this traffic** disables a rule.\n- Raw connected queries and the marketing-review event inventory stay unfiltered as evidence; Insights shows the corrected figures.\n- Only PostHog is supported today.\n\n## Social traffic and clip comparisons\n\nSave the app’s website URL on its App Profile as well as connecting PostHog. Insights then compares completed-day website sessions with recorded social view gains. Shared bio traffic stays at channel level; only a dedicated tagged link identifies one publish. Older clips with metric activity stay candidates.\n\nCopy the stable bio link and dedicated clip links from Insights. Keep `utm_source`, `utm_medium`, `sprid_account` and, on dedicated links only, `sprid_publish` through any redirects. Never repoint the shared bio link to the newest publish. The optional bio-page HTML export records selections separately and keeps a direct app link; host it on the saved hostname with your existing PostHog setup.\n\nTo capture store-link clicks and website outcomes, add after your PostHog initialization:\n\n```html\n<script src=\"https://sprid.studio/sprid-attribution.js\" defer></script>\n```\n\nIt uses your PostHog client and consent state and sends nothing to Sprid. Call `window.spridAttribution?.track('signup')` or `.track('activation')` only after that action succeeds. `posthogEvents.signup` and `posthogEvents.activation` mappings also count when the event shares the arriving website session. It does not track a native app or join visits to purchases, and a store click is not an install. Missing outcomes stay unmeasured. Redirect-only flows need a beacon before navigating; redirect events are counted apart from sessions. `get_insights` and `sprid insights` return the same evidence as the dashboard.\n\n## If it fails\n\n- **Access denied:** check the key is active, grants your project and has both Read permissions. Reconnect with a corrected key file.\n- **Project not found:** check project number and host together. A US project needs the US host.\n- **Connected but no events:** check the dates and project, then ask your agent to check your app’s tracking is sending events.\n\n## Investigate with this connection\n\nReads `query` (HogQL), `events` and `properties` for the pinned project. Check instrumentation before interpreting events. Event-definition discovery also needs `event_definition:read`, property-definition discovery `property_definition:read`, both restricted to the same project.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source posthog --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources and verification\n\nSetup and live reads checked 2026-09-09.\n\n- [PostHog personal API keys](https://posthog.com/docs/api/personal-api-keys)\n- [Project identity endpoint](https://posthog.com/docs/api/projects)\n- [HogQL query endpoint](https://posthog.com/docs/api/query)\n- [SDK and framework guides](https://posthog.com/docs/libraries)\n- [React Native screen tracking](https://posthog.com/docs/libraries/react-native)\n- [Identifying users](https://posthog.com/docs/product-analytics/identify)\n- [Funnels](https://posthog.com/docs/product-analytics/funnels)\n",
395
- "revision": "04a73687811719e3"
394
+ "markdown": "# PostHog\n\n**What Sprid does with this:** See how people use your app and where they stop.\n\n## You need\n\nAccess to your app’s PostHog project and permission to create a **personal API key**. Your app must already send events to PostHog; connecting Sprid adds no tracking. The public key inside your app does not work here.\n\n## Click path (us.posthog.com or eu.posthog.com)\n\n1. **Settings → Account → Personal API keys → Create personal API key**, named `Sprid <app name>`.\n2. Under access, select **Projects** and choose your app’s project. Leave **All access** off.\n3. Select **Query → Read** and **Project → Read**. Leave write access off.\n4. Create the key and keep the dialog open: PostHog shows it once. Use a separate key per app.\n5. Copy the key and leave it on your clipboard.\n\n## Project id and host\n\n- **Project id:** the number after `/project/` in your PostHog address, such as `12345`.\n- **Host:** `eu` for `eu.posthog.com`, `us` for `us.posthog.com`, or your full self-hosted address, whichever you open the dashboard on.\n\n## Then run\n\n```sh\nsprid connect posthog --app myapp --key-from-clipboard --project 12345 --host eu\n```\n\nUse your own app slug, project id and host. Add `--workspace <slug>` if needed. Keep the key out of chat.\n\nSprid reads the key from your clipboard, saves it and clears the clipboard, so it never shows on screen or lands in a file. Working with an agent? Tell it the key is copied and it runs this for you. Typing it yourself, copy the key last: paste the command into your terminal first, then copy the key, then press Enter. If the clipboard still holds the command, Sprid refuses it; copy the key and run it again. Without clipboard access (a remote shell), save the key to a file and pass `--key <file>` instead.\n\n## How to check it worked\n\nAsk your agent: “Check that Sprid can read recent PostHog events for this app, and confirm the project is correct.” The check reports recent activity or explains why there is none; a saved connection alone does not prove events arrive.\n\n## Metric definitions\n\nIn **Settings → Apps → your app → PostHog**, set the account-created event and the first UTC date from which tracking is complete.\n\n| Metric | What counts |\n|---|---|\n| Signups | First `sprid_signup` event per PostHog person, across all surfaces |\n| New product users | First product activity, including anonymous people |\n| Active product users | Distinct people with product activity in the period |\n| Web visitors | Distinct pageview visitors on the saved website hostname, minus your product |\n\n**Signups.** Send `sprid_signup` from your server after the account is created, with the account id as `distinct_id`, and identify that id in your clients. Never fire it on login, page load or install. An existing `posthogEvents.signup` mapping also works; `posthogConfig.registration.event` wins over it. With no tracking start date, no matching event history reads as unavailable; with one, it reads as zero signups. A period that starts before the tracking date is counted from that date and says so. Comparisons crossing it, or with no confirmed start, are withheld. Backfill with the original creation timestamps.\n\nWhen an app reports signups, they are its user count on Home and in Insights. First product activity counts anonymous devices, so it moves into that card's detail.\n\n### What counts as your product\n\nSet this under **What counts as your product** on the same screen. It decides who is an active person, and it is the single setting most likely to make the people card wrong.\n\n| Your product is | Choose | Also give |\n|---|---|---|\n| An iOS or Android app | **A phone app** | Nothing. This is the default |\n| A web app on its own subdomain | **A website or web app** | The hostnames, such as `app.example.com` |\n| A web app under a path on your marketing domain | **A website or web app** | The paths, such as `/app`, `/dashboard` |\n| A phone app with a web client | **Both** | The web hostnames or paths |\n\n**A web product must say where it lives.** Your marketing pages and your product are the same PostHog events; without a hostname or a path there is nothing to tell them apart, and every visitor would be counted as someone using the product. Sprid refuses that configuration rather than reporting it.\n\nWhatever you name here is **subtracted from your website visitor numbers**, so a page inside the product is never also counted as a visit.\n\n**A phone app** needs `posthog-react-native` on iOS, iPadOS or Android and rejects explicit web surfaces: set `app_surface: 'web'` on Expo web and `'native'` on devices. Web counts need a browser and exclude reported bots and crawler user agents. Every metric excludes events or people with `is_internal`, `is_test` or `sprid_test = true`. What remains is observed identities, not guaranteed humans.\n\n### Keep your own testing out\n\nOn a new app, your own phone, simulators and store reviewers can outnumber real people. Sprid already drops Google Play's pre-launch test devices, events reporting `$is_emulator`, reported bots, and anything flagged `is_internal`, `is_test` or `sprid_test`. Two things are yours to add:\n\n- **Flag your own accounts.** After sign-in, register `is_internal: true` when the account's email is on your own domain, store-review accounts included, and set it on the person if you identify. A web client that never identifies has to put it on every event, before the first pageview. Do the same on the server event that records a signup.\n- **List your own devices** under `exclude`, for what happens before sign-in, such as a fresh install. Open your app, then read the newest events in PostHog for the `$device_name` values:\n\n```json\n{\"exclude\":[{\"scope\":\"event\",\"property\":\"$device_name\",\"operator\":\"in\",\"values\":[\"Pixel 8\",\"Simulator iOS\",\"sdk_gphone64_arm64\"]}]}\n```\n\nApple's review devices have reported `$device_name` `iPhone99,7`, with locale `en-US` and time zone `US/Pacific`, in every app we checked (September 2026). Add it if it shows up in yours.\n\n### Custom properties\n\nAdvanced rules go under **Custom properties**:\n\n```json\n{\"registration\":{\"identity\":{\"scope\":\"event\",\"property\":\"account_id\"}},\"app\":{\"kind\":\"web\",\"hosts\":[\"app.example.com\"]},\"exclude\":[{\"scope\":\"person\",\"property\":\"staff\",\"operator\":\"in\",\"values\":[true]}]}\n```\n\n- `app.kind` is `native`, `web` or `both`, with `app.hosts` and `app.pathPrefixes` saying where a web product lives. This is what the screen above writes.\n- `app.filters` replaces the whole definition with your own rule; all filters must match. Choosing it shows as **Custom rules** on the screen, and the rule is subtracted from web visitors the same way.\n- `exclude` removes matching traffic from every metric.\n- `registration.filters` narrows the registration event, for example `result = success`.\n- `registration.accounts: false` says the app has no accounts (a local-first app, or one that identifies by device). Registrations then read as not applicable instead of a missing event, and the weekly email stops listing them as unread.\n- `registration.identity` defaults to `person_id`. A configured property must be present and should never change, because it counts accounts.\n- Each rule takes `scope: event|person`, `operator: in|not_in` and string, number or boolean `values`. A missing property fails `in` and passes `not_in`. Property names are literal keys, dots included.\n\nThe same object is `posthogConfig` in REST `PATCH /api/app-profiles/:id`, MCP `upsert_app_profile` and the App Profile JSON used by `sprid init`, where `registration.event` and `registration.since` also live. No credentials or SQL belong in it. After saving, fetch Insights and check its registration notes and totals against your database before trusting a funnel.\n\n## Review suspected automated traffic\n\nRead [Check whether a traffic spike is real](https://sprid.studio/docs/traffic) (`sprid docs traffic`) before saving a rule: a spike or a shared fingerprint alone does not prove bots.\n\nOpen **Web visitors → Review traffic exclusions**, or **Settings → Apps → your app → PostHog → Review traffic**. Pick a UTC date range and a traffic group, preview the effect, then save. Agents use MCP `review_traffic` or `POST /api/app-profiles/:ref/traffic-review?workspaceId=…` with `{\"start\":\"2026-09-18\",\"end\":\"2026-09-19\"}`: end dates exclusive, UTC, at most 31 days. An optional `exclusion` previews a rule against this period and the one before. Save approved rules in `posthogConfig.trafficExclusions` through `upsert_app_profile` or the profile PATCH route, keeping the rest of the configuration. Sprid records who edited it and when.\n\nHow exclusions apply:\n\n- App-specific and date-bounded. Properties inside a rule are AND; enabled rules are OR.\n- Applied to website totals, charts, breakdowns, social attribution and weekly website counts, comparison period included. Remaining visitors are recounted as distinct people, never subtracted.\n- Native app activity and registrations are unchanged, and PostHog’s data is never changed or deleted. **Restore this traffic** disables a rule.\n- Raw connected queries and the marketing-review event inventory stay unfiltered as evidence; Insights shows the corrected figures.\n- Only PostHog is supported today.\n\n## Social traffic and clip comparisons\n\nSave the app’s website URL on its App Profile as well as connecting PostHog. Insights then compares completed-day website sessions with recorded social view gains. Shared bio traffic stays at channel level; only a dedicated tagged link identifies one publish. Older clips with metric activity stay candidates.\n\nCopy the stable bio link and dedicated clip links from Insights. Keep `utm_source`, `utm_medium`, `sprid_account` and, on dedicated links only, `sprid_publish` through any redirects. Never repoint the shared bio link to the newest publish. The optional bio-page HTML export records selections separately and keeps a direct app link; host it on the saved hostname with your existing PostHog setup.\n\nTo capture store-link clicks and website outcomes, add after your PostHog initialization:\n\n```html\n<script src=\"https://sprid.studio/sprid-attribution.js\" defer></script>\n```\n\nIt uses your PostHog client and consent state and sends nothing to Sprid. Call `window.spridAttribution?.track('signup')` or `.track('activation')` only after that action succeeds. `posthogEvents.signup` and `posthogEvents.activation` mappings also count when the event shares the arriving website session. It does not track a native app or join visits to purchases, and a store click is not an install. Missing outcomes stay unmeasured. Redirect-only flows need a beacon before navigating; redirect events are counted apart from sessions. `get_insights` and `sprid insights` return the same evidence as the dashboard.\n\n## If it fails\n\n- **Access denied:** check the key is active, grants your project and has both Read permissions. Reconnect with a corrected key file.\n- **Project not found:** check project number and host together. A US project needs the US host.\n- **Connected but no events:** check the dates and project, then ask your agent to check your app’s tracking is sending events.\n\n## Investigate with this connection\n\nReads `query` (HogQL), `events` and `properties` for the pinned project. Check instrumentation before interpreting events. Event-definition discovery also needs `event_definition:read`, property-definition discovery `property_definition:read`, both restricted to the same project.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source posthog --json\n```\n\nSee [connected queries](https://sprid.studio/docs/queries).\n\n## Sources and verification\n\nSetup and live reads checked 2026-09-09.\n\n- [PostHog personal API keys](https://posthog.com/docs/api/personal-api-keys)\n- [Project identity endpoint](https://posthog.com/docs/api/projects)\n- [HogQL query endpoint](https://posthog.com/docs/api/query)\n- [SDK and framework guides](https://posthog.com/docs/libraries)\n- [React Native screen tracking](https://posthog.com/docs/libraries/react-native)\n- [Identifying users](https://posthog.com/docs/product-analytics/identify)\n- [Funnels](https://posthog.com/docs/product-analytics/funnels)\n",
395
+ "revision": "65f2ff7ea54b5e57"
396
396
  },
397
397
  {
398
398
  "id": "google-analytics",
@@ -1614,8 +1614,8 @@ export const CONTENT_GUIDES = [
1614
1614
  "title": "Review marketing from connected evidence",
1615
1615
  "summary": "Investigate acquisition, activation, retention and revenue through the services connected to Sprid, and say plainly what evidence is missing.",
1616
1616
  "url": "https://sprid.studio/docs/marketing-review",
1617
- "markdown": "# Review marketing from connected evidence\n\n**What this guide does:** Investigate acquisition, activation, retention and revenue through the services connected to Sprid, and say plainly what evidence is missing.\n\n## Start with the question\n\nName the app, the date range and the decision the review should inform.\n\n1. `list_apps` and `list_app_profiles` to find the app.\n2. `get_marketing_review_context` for shared definitions, required investigations, earlier decisions and corrections.\n3. `get_marketing_review` for the app over two comparable periods. The summary links to full evidence per source (`view: \"full\", sources: [<source>]`); omit `sources` to include social history, store reviews and milestones.\n4. Re-read an exact saved packet with `get_marketing_review_evidence` and its `evidence.id` (CLI: `sprid marketing-review evidence --id <id> --app <slug>`). It still works after a connection changes.\n\n`refresh: true` asks for live reads; `cachedOnly: true` reads only stored evidence. Keep returned errors, coverage, population definitions and sample counts in the report.\n\nIn a browser chat there is no repository. For a question about a shipped change, ask for a release summary or public changelog and label it as supplied. Never claim to have read a repository or private database. Local users can add repo diffs and database reads through their own tools.\n\n## Follow the evidence\n\n`list_marketing_queries` lists supported reads and their schemas; `query_marketing_source` runs them with Sprid’s saved credentials. Follow pagination and keep truncation visible. `get_documentation` with `metrics` covers definitions and refused calculations; with `queries`, source-specific reads.\n\n- Keep acquisition apart from activation, and people apart from events. A post impression is not an install; a download is not an activated user.\n- Pinterest Pins resurface for months: compare them at equal ages (30, 60 and 90 days). Saves are interest on Pinterest and outbound clicks are people leaving it; neither is a site session or an install. The `pinterest` source has `posts` only, with no comments. See [Pinterest content and publishing](pinterest.md).\n- Compare equal windows and comparable populations.\n- No onboarding events means recommending instrumentation, not guessing a funnel.\n- Keep revenue sources separate until the overlap check says they can be added.\n\nFor each finding, test another explanation: tracking changes, traffic exclusions, attribution window, seasonality, a different audience mix. A before/after comparison doesn’t prove cause. If a difference is within normal variation or the sample is small, say so and don’t rank it.\n\n## Report and save\n\nLead with the decision, then the evidence, the limits and what would change the answer. Keep failed reads apart from reads that returned nothing. `next_actions` gives the follow-up. Check [remaining setup](../../../references/setup-continuation.md#keep-setup-current), including the saved app icon, voice and relevant connections. Recommend the next useful integration step without repeating completed or explicitly declined setup. A missing credential goes to the secure App Profile form or a CLI key-file command; no provider MCP is needed for a supported query.\n\n- **Shared context:** when authorized, save non-secret definitions, investigations, decisions, corrections and release references with `save_marketing_review_context`, sending `baseRevision` from the latest read. A conflict returns both versions; reconcile before retrying. Saved context is a claim with references, not verification. CLI: `sprid marketing-review context --app <slug>` reads, `--file <context.json>` imports `{baseRevision,context}`. Private notes stay local.\n- **Changes:** log confirmed product or marketing changes with `add_event`, naming the areas they affect and why.\n- **The review itself:** save it as an app-scoped `kind: \"note\"` event with the date, conclusion, evidence references, coverage and next hypothesis in `meta`. Other clients read it with `list_events`.\n\nNever store secrets or signed attachment URLs in any of these.\n\n## Collection and coverage\n\n- Sprid refreshes two rolling 30-day windows daily for configured services on active plans, and re-reads late provider exports. No model calls, publishing or paid social reads. Other windows are read on demand.\n- Pause with the App Profile’s `metricsCollectionEnabled: false` or `sprid marketing-review collection --enabled false`. Stored evidence stays readable.\n- Snapshots are private to the app, tied to its configuration and kept 90 days. Save local evidence exports for a lasting archive.\n- Missing days stay unknown. Incomplete store or Search Console windows get no percentage change. Never sum daily unique people into a monthly count.\n- `configuration_only` means identifiers and a key are saved, not that access works. Errors return a safe code, the HTTP status when known, whether to retry and the next step. Retry transient errors before recommending a reconnect. Unsupported metrics and provider privacy thresholds are stated as limits.\n",
1618
- "revision": "f4339d5f9f97297b"
1617
+ "markdown": "# Review marketing from connected evidence\n\n**What this guide does:** Investigate acquisition, activation, retention and revenue through the services connected to Sprid, and say plainly what evidence is missing.\n\n## Start with the question\n\nName the app, the date range and the decision the review should inform.\n\n1. `list_apps` and `list_app_profiles` to find the app.\n2. `get_marketing_review_context` for shared definitions, required investigations, earlier decisions and corrections.\n3. `get_marketing_review` for the app over two comparable periods. The summary links to full evidence per source (`view: \"full\", sources: [<source>]`); omit `sources` to include social history, store reviews and milestones.\n4. Re-read an exact saved packet with `get_marketing_review_evidence` and its `evidence.id` (CLI: `sprid marketing-review evidence --id <id> --app <slug>`). It still works after a connection changes.\n\n`refresh: true` asks for live reads; `cachedOnly: true` reads only stored evidence. Keep returned errors, coverage, population definitions and sample counts in the report.\n\nIn a browser chat there is no repository. For a question about a shipped change, ask for a release summary or public changelog and label it as supplied. Never claim to have read a repository or private database. Local users can add repo diffs and database reads through their own tools.\n\n## Follow the evidence\n\n`list_marketing_queries` lists supported reads and their schemas; `query_marketing_source` runs them with Sprid’s saved credentials. Follow pagination and keep truncation visible. `get_documentation` with `metrics` covers definitions and refused calculations; with `queries`, source-specific reads.\n\n- Keep acquisition apart from activation, and people apart from events. A post impression is not an install; a download is not an activated user.\n- Pinterest Pins resurface for months: compare them at equal ages (30, 60 and 90 days). Saves are interest on Pinterest and outbound clicks are people leaving it; neither is a site session or an install. The `pinterest` source has `posts` only, with no comments. See [Pinterest content and publishing](pinterest.md).\n- Compare equal windows and comparable populations.\n- No onboarding events means recommending instrumentation, not guessing a funnel.\n- Keep revenue sources separate until the overlap check says they can be added.\n\nFor each finding, test another explanation: tracking changes, traffic exclusions, attribution window, seasonality, a different audience mix. A before/after comparison doesn’t prove cause. If a difference is within normal variation or the sample is small, say so and don’t rank it.\n\n## Report and save\n\nLead with the decision, then the evidence, the limits and what would change the answer. Keep failed reads apart from reads that returned nothing. `next_actions` gives the follow-up. Check [remaining setup](../../../references/setup-continuation.md#keep-setup-current), including the saved app icon, voice and relevant connections. Recommend the next useful integration step without repeating completed or explicitly declined setup. A missing credential goes to the secure App Profile form or a CLI key-file command; no provider MCP is needed for a supported query.\n\n- **Shared context:** when authorized, save non-secret definitions, investigations, decisions, corrections and release references with `save_marketing_review_context`, sending `baseRevision` from the latest read. A conflict returns both versions; reconcile before retrying. Saved context is a claim with references, not verification. CLI: `sprid marketing-review context --app <slug>` reads, `--file <context.json>` imports `{baseRevision,context}`. Private notes stay local.\n- **Changes:** log confirmed product or marketing changes with `add_event`, naming the areas they affect and why.\n- **The review itself:** end every review with `save_marketing_review {profile, verdict, agenda: [{title, why}], period: {start, end}, surface: \"chat\"}`: the commercial verdict in a sentence or two and the ranked agenda, most consequential first. It appears on the app’s Reviews page in Sprid and is where the next review starts; `list_marketing_reviews` reads past ones. Send the verdict and agenda only, never the full report or raw customer rows. A second save the same day replaces that day’s review.\n\nNever store secrets or signed attachment URLs in any of these.\n\n## Collection and coverage\n\n- Sprid refreshes two rolling 30-day windows daily for configured services on active plans, and re-reads late provider exports. No model calls, publishing or paid social reads. Other windows are read on demand.\n- Pause with the App Profile’s `metricsCollectionEnabled: false` or `sprid marketing-review collection --enabled false`. Stored evidence stays readable.\n- Snapshots are private to the app, tied to its configuration and kept 90 days. Save local evidence exports for a lasting archive.\n- Missing days stay unknown. Incomplete store or Search Console windows get no percentage change. Never sum daily unique people into a monthly count.\n- `configuration_only` means identifiers and a key are saved, not that access works. Errors return a safe code, the HTTP status when known, whether to retry and the next step. Retry transient errors before recommending a reconnect. Unsupported metrics and provider privacy thresholds are stated as limits.\n",
1618
+ "revision": "997462410e74e207"
1619
1619
  },
1620
1620
  {
1621
1621
  "id": "traffic",