@sprid/cli 1.2.5 → 1.2.7
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 +13 -2
- package/README-post.md +22 -61
- package/README.md +4 -7
- package/package.json +1 -1
- package/src/docs/commands.mjs +2 -2
- package/src/docs/guides.generated.mjs +39 -31
- package/src/post/cli.mjs +2 -4
- package/src/post/screen/input.mjs +7 -28
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
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
|
+
## 1.2.7 - 2026-10-19
|
|
7
|
+
|
|
8
|
+
- Marketing review guidance reuses saved goals and supports app, shop and service outcomes, with concrete actions tied to the available evidence.
|
|
9
|
+
- Update content, advertising and analytics guides to follow the customer's evidence and voice. Clarify background data refresh and local media requirements.
|
|
10
|
+
|
|
11
|
+
## 1.2.6 - 2026-10-14
|
|
12
|
+
|
|
13
|
+
- Conversion campaign guidance covers preflight checks, attribution, placement assets and app approval. Meta connection guidance covers sharing an ad account across apps, shared spending limits and removing access.
|
|
14
|
+
|
|
15
|
+
- The chat guide provides an app fallback when a connector does not expose account creation.
|
|
16
|
+
|
|
6
17
|
## 1.2.5 - 2026-10-12
|
|
7
18
|
|
|
8
19
|
- `sprid docs chat` carries the updated chat guide: an agent creates a missing content account itself with `create_content_account`, and asks whether the app has users before offering analytics and revenue connections.
|
|
@@ -263,8 +274,8 @@ shape or its exit codes is a major release.
|
|
|
263
274
|
|
|
264
275
|
## 0.1.3 - 2026-09-23
|
|
265
276
|
|
|
266
|
-
- Posts and reviews have refs: `
|
|
267
|
-
`
|
|
277
|
+
- Posts and reviews have refs: `APP-78` is post 78 of the account keyed APP,
|
|
278
|
+
`APP-R4` is review 4 of the app keyed APP. Every command that takes a post id
|
|
268
279
|
takes its ref, and `sprid reviews draft|reply` take a review ref. `sprid
|
|
269
280
|
status` and the review mail print refs, which older versions refuse.
|
|
270
281
|
- `sprid reviews reply` reads the one review it replies to, so a review
|
package/README-post.md
CHANGED
|
@@ -156,61 +156,25 @@ the speed, the background - none of it needs the app driven again. `cut.json`
|
|
|
156
156
|
next to the recording holds what a re-cut needs: the timeline, the second
|
|
157
157
|
clapper's wall time and the device's pixel size.
|
|
158
158
|
|
|
159
|
-
###
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
elapsed time; the probes now run through `baguette screenshot --scale 4`, a
|
|
179
|
-
twentieth of the pixels for ffmpeg to decode. A recording that solves below
|
|
180
|
-
**0.9x wall is treated as spoiled and fails**: the solve keeps the captions in
|
|
181
|
-
sync but the missing time cannot be put back, so the app's own motion plays fast
|
|
182
|
-
on top of whatever `speed` asked for.
|
|
183
|
-
|
|
184
|
-
**Scratch goes outside your repo** (`$TMPDIR/sprid/<repo>/<slug>`) unless a
|
|
185
|
-
lane sets `tmp`. Inside it, Metro's watcher sees every probe screenshot, sends a
|
|
186
|
-
Fast Refresh, and RN drops its blue "Refreshing…" banner over the frame.
|
|
187
|
-
|
|
188
|
-
**Input goes through `baguette`, which does not touch the machine.** It injects
|
|
189
|
-
through SimulatorKit's private symbols with the iOS-26 calling conventions: the
|
|
190
|
-
pointer never moves, the Simulator never comes to the front, and you can keep
|
|
191
|
-
working through a run. Its `input` command streams newline-delimited JSON
|
|
192
|
-
gestures, which is the part that matters - **we keep our own velocity curve**,
|
|
193
|
-
where a canned `swipe --duration` would be the linear interpolation that makes a
|
|
194
|
-
scripted scroll read as scripted.
|
|
195
|
-
|
|
196
|
-
Two details inside that, both of which cost a take:
|
|
197
|
-
|
|
198
|
-
- **Swipes stream, taps do not.** The streaming path is `IOHIDDigitizerDispatch`,
|
|
199
|
-
built for continuous gestures. React Native surfaces can accept a
|
|
200
|
-
streamed tap and the one UIKit surface, the tab bar, silently did not.
|
|
201
|
-
`baguette tap` lands on it every time, and a tap has no timing worth owning.
|
|
202
|
-
- **A tap must not move.** An earlier version jittered the touch a pixel or two
|
|
203
|
-
during the hold, on the theory that a finger is never still. Nothing can see it
|
|
204
|
-
- a screen recording has no cursor in it - and UIKit reads those moves as the
|
|
205
|
-
start of a drag and cancels the tap.
|
|
206
|
-
|
|
207
|
-
Without baguette it falls back to posting CGEvents through a compiled Swift
|
|
208
|
-
helper (`src/screen/simkit.swift`), and **that path owns the pointer for the
|
|
209
|
-
length of the run**. It is a fallback, not a preference: the Simulator ignores
|
|
210
|
-
events posted to its process, tested both plain and with the window-number
|
|
211
|
-
routing trick, so the global HID tap is the only other way in. simkit stays
|
|
212
|
-
either way, because it also rasterises the caption cards through CoreText -
|
|
213
|
-
Homebrew's ffmpeg has neither `drawtext` nor `subtitles`.
|
|
159
|
+
### Recording behavior
|
|
160
|
+
|
|
161
|
+
`settle` compares the screen with the frame before the preceding gesture. This
|
|
162
|
+
helps avoid tapping the old screen while a transition has not started yet.
|
|
163
|
+
|
|
164
|
+
The recording uses markers before and after the script to align captions with
|
|
165
|
+
the video's clock. Low-resolution probes reduce recording overhead. Recordings
|
|
166
|
+
below the timing check's accepted rate fail rather than producing an accelerated
|
|
167
|
+
capture. Inspect the reported failure before repeating a run.
|
|
168
|
+
|
|
169
|
+
Scratch files go under `$TMPDIR/sprid/<repo>/<slug>` unless a lane sets `tmp`.
|
|
170
|
+
Keeping them outside the repo prevents development file watchers from refreshing
|
|
171
|
+
the app during capture.
|
|
172
|
+
|
|
173
|
+
With `baguette`, input goes to the selected simulator without moving the desktop
|
|
174
|
+
pointer. Swipes use timed gesture samples; taps use its dedicated tap command.
|
|
175
|
+
Without it, the Swift CGEvent fallback requires Accessibility permission and
|
|
176
|
+
uses the desktop pointer for the duration of the run. Avoid using the pointer
|
|
177
|
+
while that fallback is recording.
|
|
214
178
|
|
|
215
179
|
### What it needs
|
|
216
180
|
|
|
@@ -220,12 +184,9 @@ still runs, on the CGEvent fallback, which needs the terminal to hold macOS
|
|
|
220
184
|
Accessibility permission and **takes over the pointer for the length of the
|
|
221
185
|
run**.
|
|
222
186
|
|
|
223
|
-
The status bar
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
away anyway - a row logged during the recording carries the wall clock, so a
|
|
227
|
-
9:41 status bar above it is a tell. `statusBarTime` and `battery` in the spec
|
|
228
|
-
override both.
|
|
187
|
+
The default status bar uses the host clock and a partly charged battery.
|
|
188
|
+
Set `statusBarTime` and `battery` in the spec to match the product and the intended
|
|
189
|
+
presentation. Check that visible app content and status bar values are consistent.
|
|
229
190
|
|
|
230
191
|
## Two kinds of post
|
|
231
192
|
|
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
The Sprid command line. Log in once, connect the stores and the channels, and see
|
|
4
4
|
what needs you. Node 18+. Screenshot composition and releases also need Bun.
|
|
5
5
|
|
|
6
|
-
Sprid
|
|
7
|
-
|
|
6
|
+
Sprid stores marketing plans and results, schedules approved content and gives
|
|
7
|
+
your agent tools to carry out the work you request. Connected workflows share
|
|
8
8
|
server records across REST, Sprid MCP and the dashboard. The CLI adds terminal access
|
|
9
9
|
and local media production. Credentials stay out of conversations: CLI key-file
|
|
10
10
|
commands and secure Sprid forms handle them.
|
|
@@ -80,10 +80,7 @@ $ sprid status
|
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
Connections are grouped one line per account (channels) and one per App Profile
|
|
83
|
-
(sensors): `✓` ok, `~` expiring, `✗` revoked or the last pull failed, `–` key missing.
|
|
84
|
-
PLAN line ends with `· BYOK anthropic ✓` or `· BYOK –` once the server reports the
|
|
85
|
-
workspace's model key. When the token sees
|
|
86
|
-
several workspaces, the first line names the one you are reading.
|
|
83
|
+
(sensors): `✓` ok, `~` expiring, `✗` revoked or the last pull failed, `–` key missing. When the token sees several workspaces, the first line names the one you are reading.
|
|
87
84
|
|
|
88
85
|
Every line of `status` is one field of the server's `GET /api/status`. The web Home
|
|
89
86
|
card, the Monday mail and the plugin's closing message print the same list.
|
|
@@ -197,7 +194,7 @@ claude mcp add sprid -- sprid mcp
|
|
|
197
194
|
```
|
|
198
195
|
|
|
199
196
|
It uses the `sprid login` credentials (or `SPRID_PAT`). `--surface all` lists the full
|
|
200
|
-
catalog instead of the
|
|
197
|
+
catalog instead of the core tools.
|
|
201
198
|
|
|
202
199
|
## What it touches
|
|
203
200
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sprid/cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
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",
|
package/src/docs/commands.mjs
CHANGED
|
@@ -29,7 +29,7 @@ export const COMMAND_GROUPS = [
|
|
|
29
29
|
['media', 'sprid media job <UUID>', 'Read saved upload results or creation progress without repeating the operation.'],
|
|
30
30
|
['media', 'sprid media init|build|register|check|push|preview …', 'Run the existing local media pipeline. Historical sprid post invocations remain supported.'],
|
|
31
31
|
['post', 'sprid post create --file <draft.json>', 'Create a shared draft from ordered asset references and a stable requestId. With "status": "idea" it saves an idea instead: notes, a sourceUrl and any gathered assets, before layout. Never publishes.'],
|
|
32
|
-
['post', 'sprid post get <postId>', 'Read the shared post and its slides. Every <postId> here takes the UUID or the post\'s ref, such as
|
|
32
|
+
['post', 'sprid post get <postId>', 'Read the shared post and its slides. Every <postId> here takes the UUID or the post\'s ref, such as APP-142.'],
|
|
33
33
|
['post', 'sprid post update <postId> --file <changes.json>', 'Apply the existing post-update contract to a shared draft.'],
|
|
34
34
|
['post', 'sprid post preview --id <postId>', 'Preview a shared post. Without --id, historical local preview arguments are unchanged.'],
|
|
35
35
|
['post', 'sprid post deliveries <postId>', 'Read scheduled, failed and delivered publications with platform receipts.'],
|
|
@@ -95,7 +95,7 @@ export const COMMAND_GROUPS = [
|
|
|
95
95
|
['status', 'sprid status', 'Show what needs you, your connections and the publishing queue.'],
|
|
96
96
|
['next', 'sprid next [--limit N]', 'List the next actions and how to complete them.'],
|
|
97
97
|
['marketing-review', 'sprid marketing-review [--app <slug>] [--days N|--since YYYY-MM-DD] [--end YYYY-MM-DD] [--check]', 'Read connected analytics and a versioned metric dataset for two equal periods. End is exclusive. Validated results retain blocked reasons; complete packets are saved immutably under marketing-reports/evidence. --check reads setup only.'],
|
|
98
|
-
['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
|
|
98
|
+
['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 queues background collection; inspect progress and freshness. Cached-only makes no provider calls. Daily collection refreshes connected sources automatically for active plans.'],
|
|
99
99
|
['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.'],
|
|
100
100
|
['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.'],
|
|
101
101
|
['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.'],
|
|
@@ -360,7 +360,7 @@ export const GUIDES = [
|
|
|
360
360
|
"id": "metric-definitions",
|
|
361
361
|
"title": "Metric definitions",
|
|
362
362
|
"kind": "detail",
|
|
363
|
-
"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\n**Every signup surface has to send it.** An event your app sends after sign-up misses accounts made on your website, and the other way round: one app's in-app event missed 17% of its signups, which happened on its website. A server event covers every surface at once. Without one, list each surface's account-created event in `registration.events`; the first of any of them per identity counts, so an existing account signing in elsewhere is not a new signup. Never add a sign-in event that existing accounts also send. The marketing review flags a signup-like event on a surface where your registration event never fires.\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\
|
|
363
|
+
"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\n**Every signup surface has to send it.** An event your app sends after sign-up misses accounts made on your website, and the other way round: one app's in-app event missed 17% of its signups, which happened on its website. A server event covers every surface at once. Without one, list each surface's account-created event in `registration.events`; the first of any of them per identity counts, so an existing account signing in elsewhere is not a new signup. Never add a sign-in event that existing accounts also send. The marketing review flags a signup-like event on a surface where your registration event never fires.\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\nKeep known staff and test activity separate from customer metrics. Sprid applies\nits built-in exclusions and flags including `is_internal`, `is_test` and\n`sprid_test`; inspect the reading's filter notes to see what was excluded.\n\n- Flag verified staff or test identities on the events and person records that\n your queries use. Include anonymous test sessions when they can be identified\n safely, and apply the same convention to server events.\n- Use custom exclusions only for properties that reliably identify testing in\n this product. A common phone model, country or language also describes customers.\n\nFor example, a dedicated person property can identify verified staff accounts:\n\n```json\n{\"exclude\":[{\"scope\":\"person\",\"property\":\"staff\",\"operator\":\"in\",\"values\":[true]}]}\n```\n\nPossible store-review activity needs corroboration from release timing or known\ntest records. A measurement warning is a reason to investigate, not permission\nto exclude everyone sharing that device name, locale or geography.\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.events` adds account-created events from other surfaces, counted with `registration.event` (see Signups above).\n- `entryUrls` lists the pages a plan, campaign or bio link sends people to, such as a web funnel's first step. The review flags one with almost no pageviews in 14 days, which usually means a link stopped pointing at it.\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### Paywall and purchase events\n\nA paywall event that says only that a paywall appeared cannot tell a price change from a paywall that failed to load: both read as fewer sales. One app's Android paywall had no purchasable product for five months and its revenue view read as zero demand. Send at least these, and map the events under `posthogEvents` as `paywallShown` and `purchase`:\n\n| Event | Properties |\n|---|---|\n| Paywall shown | `price`, `currency`, `product_id`, `offering_id`, `offering_loaded` (true once the store returned a product), `has_free_phase`, `store_country` |\n| Purchase or trial started, after the store confirms it | `price`, `currency`, `product_id`, `has_free_phase` |\n| Purchase failed | `product_id`, the store's error code |\n\nReport a billing error to your error tracker too, never only to the console. The marketing review flags a platform that shows the paywall to many people and never sells, and paywall events that carry none of these properties."
|
|
364
364
|
},
|
|
365
365
|
{
|
|
366
366
|
"id": "countries-sources-and-entry-pages",
|
|
@@ -399,8 +399,8 @@ export const GUIDES = [
|
|
|
399
399
|
"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)"
|
|
400
400
|
}
|
|
401
401
|
],
|
|
402
|
-
"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\n**Every signup surface has to send it.** An event your app sends after sign-up misses accounts made on your website, and the other way round: one app's in-app event missed 17% of its signups, which happened on its website. A server event covers every surface at once. Without one, list each surface's account-created event in `registration.events`; the first of any of them per identity counts, so an existing account signing in elsewhere is not a new signup. Never add a sign-in event that existing accounts also send. The marketing review flags a signup-like event on a surface where your registration event never fires.\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\nTwo more Apple populations pass every default rule, because they are real iPhones: the device that checks each uploaded build (a fresh US person per build, often Cupertino, locale a bare `en` where real phones send `en-US`) and App Review (US, locale `zh-Hans`, about one a day). Both are active for one day and never reach a paywall: one app counted 14 of them as installs in two months. The marketing review flags them with their counts. Exclude a locale only after checking none of your real users send it.\n\nGoogle Play's pre-launch devices are covered by default: in the app we checked, every `OnePlus8Pro` event carried the fleet's screen width and every `sdk_gphone` event carried `$is_emulator` (2026-10-03). If yours sends neither property, list the device names under `exclude`.\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.events` adds account-created events from other surfaces, counted with `registration.event` (see Signups above).\n- `entryUrls` lists the pages a plan, campaign or bio link sends people to, such as a web funnel's first step. The review flags one with almost no pageviews in 14 days, which usually means a link stopped pointing at it.\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### Paywall and purchase events\n\nA paywall event that says only that a paywall appeared cannot tell a price change from a paywall that failed to load: both read as fewer sales. One app's Android paywall had no purchasable product for five months and its revenue view read as zero demand. Send at least these, and map the events under `posthogEvents` as `paywallShown` and `purchase`:\n\n| Event | Properties |\n|---|---|\n| Paywall shown | `price`, `currency`, `product_id`, `offering_id`, `offering_loaded` (true once the store returned a product), `has_free_phase`, `store_country` |\n| Purchase or trial started, after the store confirms it | `price`, `currency`, `product_id`, `has_free_phase` |\n| Purchase failed | `product_id`, the store's error code |\n\nReport a billing error to your error tracker too, never only to the console. The marketing review flags a platform that shows the paywall to many people and never sells, and paywall events that carry none of these properties.\n\n## Countries, sources and entry pages\n\nEach website breakdown is one PostHog property on each `$pageview`. posthog-js fills them all itself, so a site that loads it with default settings needs nothing here. They go missing when something else sends the events, and the breakdown then reads **Unknown**. Insights and `get_insights` (`traffic.gaps`) name which one is missing, why, and the fix.\n\n| Breakdown | Property Sprid reads | posthog-js sets it from |\n|---|---|---|\n| Country, region, city | `$geoip_country_code`, `$geoip_subdivision_1_name`, `$geoip_city_name` | PostHog’s GeoIP lookup of the visitor’s IP |\n| Source | `$referring_domain` | The referrer’s hostname, or `$direct` when there is none, on every page of the visit |\n| Entry and exit page, sessions, bounce, time on site | `$session_id` | One id per visit |\n| Browser, OS, device | `$browser`, `$os`, `$device_type` | The user agent |\n| Pages, hostname | `$pathname`, `$host` | The page address |\n\nWebsite events also need `$lib: \"web\"`; Sprid tells your site apart from your app by it.\n\nThree setups lose the country, and each has its own fix:\n\n- **Cookieless mode** (`cookieless_mode: \"always\"`): PostHog does not geolocate cookieless events, so every country is Unknown. Register the country yourself before the first pageview, from a value your host already has. On Cloudflare that is `request.cf.country`, the `CF-IPCountry` header, or `loc=` in `/cdn-cgi/trace` on your own domain: `posthog.register({ $geoip_country_code: \"SE\" })`. Or turn cookieless mode off if your privacy policy allows it.\n- **Your own relay or server** (a Worker, an API route or a proxy that calls PostHog’s capture endpoint): PostHog sees the server’s IP, or none if you set `$geoip_disable: true`. Set `$geoip_country_code` in the relay. A property called `country` does not count: nothing in PostHog or Sprid reads it.\n- **\"Discard client IP data\"** under Project settings, or the GeoIP transformation switched off under Data pipelines: no event in the project gets a location. Turn it back on, or supply the country as above.\n\nA relay also has to send what posthog-js would have: `$referring_domain` on every pageview (`$direct` when the visit had no referrer, and the landing page’s referrer on every later page of the same visit), and a `$session_id` that stays the same for the whole visit. For example, in a Cloudflare Worker:\n\n```js\nproperties: {\n ...props,\n $lib: \"web\",\n $host: \"example.com\",\n $pathname: path,\n $session_id: visitId,\n $referring_domain: visitReferrer || \"$direct\",\n $geoip_disable: true, // PostHog never sees the IP\n $geoip_country_code: request.cf?.country ?? null,\n}\n```\n\nA visit whose source is `localhost` is someone on a development build clicking through to your live site. Stop capturing on development hosts, or exclude it as below.\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\nNot every crawler arrives as a spike. One site's September was 84% people on desktop, with no referrer, from one country, most of them for one pageview, with a normal Chrome user agent that no bot filter catches. The marketing review flags a desktop, direct, single-country group above half of website people; confirm it here before excluding anything.\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",
|
|
403
|
-
"revision": "
|
|
402
|
+
"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\n**Every signup surface has to send it.** An event your app sends after sign-up misses accounts made on your website, and the other way round: one app's in-app event missed 17% of its signups, which happened on its website. A server event covers every surface at once. Without one, list each surface's account-created event in `registration.events`; the first of any of them per identity counts, so an existing account signing in elsewhere is not a new signup. Never add a sign-in event that existing accounts also send. The marketing review flags a signup-like event on a surface where your registration event never fires.\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\nKeep known staff and test activity separate from customer metrics. Sprid applies\nits built-in exclusions and flags including `is_internal`, `is_test` and\n`sprid_test`; inspect the reading's filter notes to see what was excluded.\n\n- Flag verified staff or test identities on the events and person records that\n your queries use. Include anonymous test sessions when they can be identified\n safely, and apply the same convention to server events.\n- Use custom exclusions only for properties that reliably identify testing in\n this product. A common phone model, country or language also describes customers.\n\nFor example, a dedicated person property can identify verified staff accounts:\n\n```json\n{\"exclude\":[{\"scope\":\"person\",\"property\":\"staff\",\"operator\":\"in\",\"values\":[true]}]}\n```\n\nPossible store-review activity needs corroboration from release timing or known\ntest records. A measurement warning is a reason to investigate, not permission\nto exclude everyone sharing that device name, locale or geography.\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.events` adds account-created events from other surfaces, counted with `registration.event` (see Signups above).\n- `entryUrls` lists the pages a plan, campaign or bio link sends people to, such as a web funnel's first step. The review flags one with almost no pageviews in 14 days, which usually means a link stopped pointing at it.\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### Paywall and purchase events\n\nA paywall event that says only that a paywall appeared cannot tell a price change from a paywall that failed to load: both read as fewer sales. One app's Android paywall had no purchasable product for five months and its revenue view read as zero demand. Send at least these, and map the events under `posthogEvents` as `paywallShown` and `purchase`:\n\n| Event | Properties |\n|---|---|\n| Paywall shown | `price`, `currency`, `product_id`, `offering_id`, `offering_loaded` (true once the store returned a product), `has_free_phase`, `store_country` |\n| Purchase or trial started, after the store confirms it | `price`, `currency`, `product_id`, `has_free_phase` |\n| Purchase failed | `product_id`, the store's error code |\n\nReport a billing error to your error tracker too, never only to the console. The marketing review flags a platform that shows the paywall to many people and never sells, and paywall events that carry none of these properties.\n\n## Countries, sources and entry pages\n\nEach website breakdown is one PostHog property on each `$pageview`. posthog-js fills them all itself, so a site that loads it with default settings needs nothing here. They go missing when something else sends the events, and the breakdown then reads **Unknown**. Insights and `get_insights` (`traffic.gaps`) name which one is missing, why, and the fix.\n\n| Breakdown | Property Sprid reads | posthog-js sets it from |\n|---|---|---|\n| Country, region, city | `$geoip_country_code`, `$geoip_subdivision_1_name`, `$geoip_city_name` | PostHog’s GeoIP lookup of the visitor’s IP |\n| Source | `$referring_domain` | The referrer’s hostname, or `$direct` when there is none, on every page of the visit |\n| Entry and exit page, sessions, bounce, time on site | `$session_id` | One id per visit |\n| Browser, OS, device | `$browser`, `$os`, `$device_type` | The user agent |\n| Pages, hostname | `$pathname`, `$host` | The page address |\n\nWebsite events also need `$lib: \"web\"`; Sprid tells your site apart from your app by it.\n\nThree setups lose the country, and each has its own fix:\n\n- **Cookieless mode** (`cookieless_mode: \"always\"`): PostHog does not geolocate cookieless events, so every country is Unknown. Register the country yourself before the first pageview, from a value your host already has. On Cloudflare that is `request.cf.country`, the `CF-IPCountry` header, or `loc=` in `/cdn-cgi/trace` on your own domain: `posthog.register({ $geoip_country_code: \"SE\" })`. Or turn cookieless mode off if your privacy policy allows it.\n- **Your own relay or server** (a Worker, an API route or a proxy that calls PostHog’s capture endpoint): PostHog sees the server’s IP, or none if you set `$geoip_disable: true`. Set `$geoip_country_code` in the relay. A property called `country` does not count: nothing in PostHog or Sprid reads it.\n- **\"Discard client IP data\"** under Project settings, or the GeoIP transformation switched off under Data pipelines: no event in the project gets a location. Turn it back on, or supply the country as above.\n\nA relay also has to send what posthog-js would have: `$referring_domain` on every pageview (`$direct` when the visit had no referrer, and the landing page’s referrer on every later page of the same visit), and a `$session_id` that stays the same for the whole visit. For example, in a Cloudflare Worker:\n\n```js\nproperties: {\n ...props,\n $lib: \"web\",\n $host: \"example.com\",\n $pathname: path,\n $session_id: visitId,\n $referring_domain: visitReferrer || \"$direct\",\n $geoip_disable: true, // PostHog never sees the IP\n $geoip_country_code: request.cf?.country ?? null,\n}\n```\n\nA visit whose source is `localhost` is someone on a development build clicking through to your live site. Stop capturing on development hosts, or exclude it as below.\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\nNot every crawler arrives as a spike. One site's September was 84% people on desktop, with no referrer, from one country, most of them for one pageview, with a normal Chrome user agent that no bot filter catches. The marketing review flags a desktop, direct, single-country group above half of website people; confirm it here before excluding anything.\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",
|
|
403
|
+
"revision": "8ae7cbb023b9c925"
|
|
404
404
|
},
|
|
405
405
|
{
|
|
406
406
|
"id": "google-analytics",
|
|
@@ -620,7 +620,7 @@ export const GUIDES = [
|
|
|
620
620
|
"id": "see-who-buys",
|
|
621
621
|
"title": "See who buys",
|
|
622
622
|
"kind": "detail",
|
|
623
|
-
"markdown": "This key tells Sprid how much you sold, split by country, store, product and, when the app sends attribution to RevenueCat, by campaign. It cannot tell which visitor or user became a buyer. More permissions on this key would not change that: RevenueCat’s API reads customers one at a time.\n\nTo see who buys, send RevenueCat’s purchase events to the tool that already knows your users, under the same user id:\n\n1. In the app, give RevenueCat the id your analytics tool uses. Either log the user in with it (`Purchases.logIn`), or set the attribute the integration’s own RevenueCat page names.\n2. In RevenueCat, open **Integrations** and turn on the one you use: Amplitude, Mixpanel, PostHog, Segment, mParticle, Firebase (which reaches Google Analytics), or an attribution provider such as AppsFlyer or Adjust. For anything else, use a webhook or a scheduled export to your warehouse.\n3. Ask the question in that tool: which sources, pages or onboarding steps come before a purchase.\n\n**
|
|
623
|
+
"markdown": "This key tells Sprid how much you sold, split by country, store, product and, when the app sends attribution to RevenueCat, by campaign. It cannot tell which visitor or user became a buyer. More permissions on this key would not change that: RevenueCat’s API reads customers one at a time.\n\nTo see who buys, send RevenueCat’s purchase events to the tool that already knows your users, under the same user id:\n\n1. In the app, give RevenueCat the id your analytics tool uses. Either log the user in with it (`Purchases.logIn`), or set the attribute the integration’s own RevenueCat page names.\n2. In RevenueCat, open **Integrations** and turn on the one you use: Amplitude, Mixpanel, PostHog, Segment, mParticle, Firebase (which reaches Google Analytics), or an attribution provider such as AppsFlyer or Adjust. For anything else, use a webhook or a scheduled export to your warehouse.\n3. Ask the question in that tool: which sources, pages or onboarding steps come before a purchase.\n\n**Verify geography on server-sent purchase events.** GeoIP enrichment can use\nthe integration server's location rather than the buyer's. Check event and person\nproperties before reporting buyer country. Use a verified store-country breakdown\nor an app-supplied purchase property where available. If server events overwrite\nperson geography, inspect the analytics pipeline's enrichment settings before\nchanging them. Sprid's review flags suspected cases for investigation.\n\nCookieless analytics such as Plausible count visits without knowing who made them, so there is no user to match. Their UTM sources still line up with RevenueCat’s campaign breakdown as totals."
|
|
624
624
|
},
|
|
625
625
|
{
|
|
626
626
|
"id": "if-it-fails",
|
|
@@ -641,8 +641,8 @@ export const GUIDES = [
|
|
|
641
641
|
"markdown": "Setup and live reads checked 2026-09-09.\n\n- [RevenueCat API keys](https://www.revenuecat.com/docs/projects/authentication)\n- [API V2 reference](https://www.revenuecat.com/docs/api-v2)"
|
|
642
642
|
}
|
|
643
643
|
],
|
|
644
|
-
"markdown": "# RevenueCat\n\n**What Sprid does with this:** See subscription revenue and how it changes over time.\n\n## You need\n\n**Admin** access to the RevenueCat project, to create a dedicated **V2 secret API key**. The app’s public key and V1 keys do not work.\n\n## Click path (app.revenuecat.com)\n\n1. Select your project → **API keys → Secret API keys → New secret API key**.\n2. Name it `Sprid analytics` and select **V2**.\n3. **Charts metrics permissions**: set **Overview Configuration Access Level** and **Charts Configuration Access Level** to **Read only**.\n4. **Project configuration permissions**: set **Apps Configuration Access Level** to **Read only**. Leave everything else at **No access**.\n5. Click **Generate**. Use a separate key per app.\n6. Copy the key and leave it on your clipboard.\n\n## Project id\n\n**Project settings → General → Project ID**, such as `proj1ab2c3d4`. Not the shorter id in the dashboard address.\n\n## Then run\n\n```sh\nsprid connect revenuecat --app myapp --key-from-clipboard --project proj1ab2c3d4\n```\n\nUse your own app slug and project id. 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 this app’s RevenueCat overview and daily revenue history. Report any missing dates.” A saved key alone does not confirm access. The overview can work while history does not; missing dates are not zero sales.\n\n## If you also sell on the web\n\nConnect the services that record the other sales. Sprid withholds a combined total when RevenueCat may already include the same Stripe or Paddle sales, or when currencies or definitions differ. RevenueCat Web Billing and your own Stripe are separate merchant accounts, so they never overlap.\n\n## See who buys\n\nThis key tells Sprid how much you sold, split by country, store, product and, when the app sends attribution to RevenueCat, by campaign. It cannot tell which visitor or user became a buyer. More permissions on this key would not change that: RevenueCat’s API reads customers one at a time.\n\nTo see who buys, send RevenueCat’s purchase events to the tool that already knows your users, under the same user id:\n\n1. In the app, give RevenueCat the id your analytics tool uses. Either log the user in with it (`Purchases.logIn`), or set the attribute the integration’s own RevenueCat page names.\n2. In RevenueCat, open **Integrations** and turn on the one you use: Amplitude, Mixpanel, PostHog, Segment, mParticle, Firebase (which reaches Google Analytics), or an attribution provider such as AppsFlyer or Adjust. For anything else, use a webhook or a scheduled export to your warehouse.\n3. Ask the question in that tool: which sources, pages or onboarding steps come before a purchase.\n\n**
|
|
645
|
-
"revision": "
|
|
644
|
+
"markdown": "# RevenueCat\n\n**What Sprid does with this:** See subscription revenue and how it changes over time.\n\n## You need\n\n**Admin** access to the RevenueCat project, to create a dedicated **V2 secret API key**. The app’s public key and V1 keys do not work.\n\n## Click path (app.revenuecat.com)\n\n1. Select your project → **API keys → Secret API keys → New secret API key**.\n2. Name it `Sprid analytics` and select **V2**.\n3. **Charts metrics permissions**: set **Overview Configuration Access Level** and **Charts Configuration Access Level** to **Read only**.\n4. **Project configuration permissions**: set **Apps Configuration Access Level** to **Read only**. Leave everything else at **No access**.\n5. Click **Generate**. Use a separate key per app.\n6. Copy the key and leave it on your clipboard.\n\n## Project id\n\n**Project settings → General → Project ID**, such as `proj1ab2c3d4`. Not the shorter id in the dashboard address.\n\n## Then run\n\n```sh\nsprid connect revenuecat --app myapp --key-from-clipboard --project proj1ab2c3d4\n```\n\nUse your own app slug and project id. 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 this app’s RevenueCat overview and daily revenue history. Report any missing dates.” A saved key alone does not confirm access. The overview can work while history does not; missing dates are not zero sales.\n\n## If you also sell on the web\n\nConnect the services that record the other sales. Sprid withholds a combined total when RevenueCat may already include the same Stripe or Paddle sales, or when currencies or definitions differ. RevenueCat Web Billing and your own Stripe are separate merchant accounts, so they never overlap.\n\n## See who buys\n\nThis key tells Sprid how much you sold, split by country, store, product and, when the app sends attribution to RevenueCat, by campaign. It cannot tell which visitor or user became a buyer. More permissions on this key would not change that: RevenueCat’s API reads customers one at a time.\n\nTo see who buys, send RevenueCat’s purchase events to the tool that already knows your users, under the same user id:\n\n1. In the app, give RevenueCat the id your analytics tool uses. Either log the user in with it (`Purchases.logIn`), or set the attribute the integration’s own RevenueCat page names.\n2. In RevenueCat, open **Integrations** and turn on the one you use: Amplitude, Mixpanel, PostHog, Segment, mParticle, Firebase (which reaches Google Analytics), or an attribution provider such as AppsFlyer or Adjust. For anything else, use a webhook or a scheduled export to your warehouse.\n3. Ask the question in that tool: which sources, pages or onboarding steps come before a purchase.\n\n**Verify geography on server-sent purchase events.** GeoIP enrichment can use\nthe integration server's location rather than the buyer's. Check event and person\nproperties before reporting buyer country. Use a verified store-country breakdown\nor an app-supplied purchase property where available. If server events overwrite\nperson geography, inspect the analytics pipeline's enrichment settings before\nchanging them. Sprid's review flags suspected cases for investigation.\n\nCookieless analytics such as Plausible count visits without knowing who made them, so there is no user to match. Their UTM sources still line up with RevenueCat’s campaign breakdown as totals.\n\n## If it fails\n\n- **Key rejected:** check it is an active V2 secret key for this project.\n- **Access denied or history missing:** open the key’s **More → Edit**, check all three Read only settings, then **Submit**. No new key needed.\n- **Project not found:** copy the full Project ID from **Project settings → General**. Key and id must belong to the same project.\n- **Too many requests:** retry later. Another key will not help.\n\n## Investigate with this connection\n\nReads `chart_options`, `chart` and `subscriptions` for the pinned project. Read `chart_options` before picking dimensions, filters or resolution, and keep the returned units. `subscriptions` also needs `customer_information:subscriptions:read` and defaults to production; ask for sandbox to see test purchases.\n\n```sh\nsprid marketing-review capabilities --app <slug> --source revenuecat --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- [RevenueCat API keys](https://www.revenuecat.com/docs/projects/authentication)\n- [API V2 reference](https://www.revenuecat.com/docs/api-v2)\n",
|
|
645
|
+
"revision": "6357f75a855e8df3"
|
|
646
646
|
},
|
|
647
647
|
{
|
|
648
648
|
"id": "stripe",
|
|
@@ -1451,13 +1451,13 @@ export const GUIDES = [
|
|
|
1451
1451
|
"id": "you-need",
|
|
1452
1452
|
"title": "You need",
|
|
1453
1453
|
"kind": "requirements",
|
|
1454
|
-
"markdown": "- A Facebook profile with two-factor authentication turned on. Meta will not let you add ad assets without it.\n- A **business portfolio** at [business.facebook.com](https://business.facebook.com). One per company is enough, even with several apps or brands. See **One portfolio or several** below.\n- A **Facebook Page** for the brand. Ads always run from a Page, including the ones shown on Instagram. To create one, see [Create your social accounts](https://sprid.studio/docs/connect/social-accounts).\n- To promote **Instagram** posts: the Instagram account **linked to that Page**. Meta runs an Instagram ad through the Page the profile is linked to, so without the link only Facebook posts can be promoted.\n- An **ad account** with a payment method
|
|
1454
|
+
"markdown": "- A Facebook profile with two-factor authentication turned on. Meta will not let you add ad assets without it.\n- A **business portfolio** at [business.facebook.com](https://business.facebook.com). One per company is enough, even with several apps or brands. See **One portfolio or several** below.\n- A **Facebook Page** for the brand. Ads always run from a Page, including the ones shown on Instagram. To create one, see [Create your social accounts](https://sprid.studio/docs/connect/social-accounts).\n- To promote **Instagram** posts: the Instagram account **linked to that Page**. Meta runs an Instagram ad through the Page the profile is linked to, so without the link only Facebook posts can be promoted.\n- An **ad account** with a payment method. Several apps in the same Sprid workspace can share it.\n- Optional: a **dataset** (formerly Meta Pixel) if you want campaigns that optimise for signups or purchases.\n\nYou do **not** create a Meta developer app or copy a token. You only sign in with Facebook and pick the Page and ad account."
|
|
1455
1455
|
},
|
|
1456
1456
|
{
|
|
1457
1457
|
"id": "set-up-meta-once-before-you-connect",
|
|
1458
1458
|
"title": "Set up Meta (once, before you connect)",
|
|
1459
1459
|
"kind": "steps",
|
|
1460
|
-
"markdown": "Skip any step you have already done.\n\n1. **Business portfolio.** Go to [business.facebook.com](https://business.facebook.com) and create a business portfolio under your company’s legal name.\n2. **Page.** In the portfolio, open **Settings → Accounts → Pages** and add the brand’s Page. Give yourself access that includes **Ads**.\n3. **Instagram (to promote Instagram posts).** On the Page, open **Settings → Linked accounts → Instagram** and connect the brand’s Instagram account. It has to be the same account you publish to from Sprid. Sprid reads the link from Meta each time you promote, so you can do this later without reconnecting.\n4. **Ad account.**
|
|
1460
|
+
"markdown": "Skip any step you have already done.\n\n1. **Business portfolio.** Go to [business.facebook.com](https://business.facebook.com) and create a business portfolio under your company’s legal name.\n2. **Page.** In the portfolio, open **Settings → Accounts → Pages** and add the brand’s Page. Give yourself access that includes **Ads**.\n3. **Instagram (to promote Instagram posts).** On the Page, open **Settings → Linked accounts → Instagram** and connect the brand’s Instagram account. It has to be the same account you publish to from Sprid. Sprid reads the link from Meta each time you promote, so you can do this later without reconnecting.\n4. **Ad account.** Use an existing ad account, or open **Settings → Accounts → Ad accounts → Add → Create a new ad account**. Pick the **currency** and **time zone** carefully. Changing them creates a new ad account and closes the old one, stopping its ads. Choose the currency of the market you advertise in. Give yourself **Manage campaigns** or full control.\n5. **Payment method.** Open **Billing & payments** for that ad account and add a card. Sprid can connect an account without one, but no campaign can start until it has one.\n6. **Dataset (optional).** Open [Events Manager](https://business.facebook.com/events_manager2) → **Connect data sources → Web**, name it after your domain and copy its **dataset id**. Sprid only needs the id. The events themselves reach it from your site or app, through the Meta Pixel or the Conversions API, and without events a conversion campaign has nothing to optimise for.\n\nFor campaigns that optimise for signups or purchases, three more:\n\n7. **Conversions API token** (if your server sends the events). Open the dataset → **Settings → Conversions API** and go through Meta's setup guide to the end. The events and fields you tick there only shape its instructions, not what your server may send. Generate the access token there and store it on your server. Sprid never needs it.\n8. **Dataset settings.** Review Meta’s settings and any restrictions shown for this dataset. A server-only Conversions API integration can send events without browser pixel code.\n9. **Advertising settings.** Review any optional AI tests that change your destination or its presentation. Creative opt-outs cannot verify account-level tests. Define engaged audiences and existing customers before using audience-segment reports."
|
|
1461
1461
|
},
|
|
1462
1462
|
{
|
|
1463
1463
|
"id": "then-run",
|
|
@@ -1469,35 +1469,35 @@ export const GUIDES = [
|
|
|
1469
1469
|
"id": "click-path-the-connect",
|
|
1470
1470
|
"title": "Click path (the connect)",
|
|
1471
1471
|
"kind": "steps",
|
|
1472
|
-
"markdown": "1. Sign in with the Facebook profile that manages the Page and the ad account.\n2. Leave every requested permission switched on and click **Continue**.\n3. Back in Sprid, pick the **Page** your ads should appear from, then the **ad account**. Each list only appears when there is more than one to choose from.\n4. From the CLI, a profile with several Pages or ad accounts gets a message listing their ids. Run the command again with them:\n\n```sh\nsprid connect meta-ads --account <slug> --page <page id> --ad-account <ad account id> --dataset <dataset id>\n```\n\nWith several
|
|
1472
|
+
"markdown": "1. Sign in with the Facebook profile that manages the Page and the ad account.\n2. Leave every requested permission switched on and click **Continue**.\n3. Back in Sprid, pick the **Page** your ads should appear from, then the **ad account**. Each list only appears when there is more than one to choose from.\n4. From the CLI, a profile with several Pages or ad accounts gets a message listing their ids. Run the command again with them:\n\n```sh\nsprid connect meta-ads --account <slug> --page <page id> --ad-account <ad account id> --dataset <dataset id>\n```\n\nWith several apps, connect each Sprid account separately. Pick that app’s Page and dataset each time; you can select the same ad account for apps in the same workspace. Their campaigns and conversion lists stay separate."
|
|
1473
1473
|
},
|
|
1474
1474
|
{
|
|
1475
1475
|
"id": "check-the-connection",
|
|
1476
1476
|
"title": "Check the connection",
|
|
1477
1477
|
"kind": "verify",
|
|
1478
|
-
"markdown": "Run `sprid status` and check that Meta Ads shows the intended Page name, followed by “(Ads)”.\n\nA campaign Sprid prepares starts **paused**. Nothing is spent until you confirm the start."
|
|
1478
|
+
"markdown": "Run `sprid status` and check that Meta Ads shows the intended Page name, followed by “(Ads)”.\n\nFor conversion delivery, complete the intended action and confirm its exact event name in Events Manager. An API response such as `{\"events_received\":1}` only acknowledges the request; it does not prove the chosen event arrived. Then ask your agent to run `dataset_health` for that event. A dataset ID alone cannot pass the conversion launch check. Follow [Run your first Meta conversion campaign](https://sprid.studio/docs/meta-conversions#prove-the-conversion-signal) for testing and custom conversions.\n\nA campaign Sprid prepares starts **paused**. Nothing is spent until you confirm the start."
|
|
1479
1479
|
},
|
|
1480
1480
|
{
|
|
1481
1481
|
"id": "one-portfolio-or-several",
|
|
1482
1482
|
"title": "One portfolio or several",
|
|
1483
1483
|
"kind": "detail",
|
|
1484
|
-
"markdown": "Keep one business portfolio for your company and
|
|
1484
|
+
"markdown": "Keep one business portfolio for your company. Use a Page and dataset for each app. You can share one ad account across apps in the same Sprid workspace, or use separate ad accounts if Meta allows them and you want separate billing.\n\nA shared ad account uses one billing currency. Its payment and account status affect every app advertising through it. Check the currency before choosing budgets.\n\nIn Sprid, open **Ad settings → Shared Meta limits** on any connected account. A workspace owner or admin can set the combined daily budget and monthly spend-and-commitment limit. These limits cover campaigns managed through Sprid. Meta can vary actual daily spend, and changes made directly in Ads Manager can exceed these limits. Lowering a limit blocks new commitments; it does not pause running ads.\n\n**Disconnect** removes only the selected Sprid connection. **Remove access** first lists every connection using the same Meta authorization, then asks you to confirm in Sprid. Removing access stops Sprid from managing those connections; running ads remain on Meta. Reconnect older connections if Sprid cannot identify who authorized them."
|
|
1485
1485
|
},
|
|
1486
1486
|
{
|
|
1487
1487
|
"id": "if-it-fails",
|
|
1488
1488
|
"title": "If it fails",
|
|
1489
1489
|
"kind": "troubleshooting",
|
|
1490
|
-
"markdown": "- **“No Facebook Pages found”:** the profile you signed in with has no Page access. Check the Page in the portfolio, give yourself access and reconnect.\n- **No ad account to choose, or the wrong ones:** only active ad accounts are listed. Check that the account is not disabled or closed in Ads Manager and that your profile is assigned to it.\n- **“Re-run the connect with --page / --ad-account”:** the profile manages several. Copy the right ids from the message and run the command again with them.\n- **“Already connected to another account”:**
|
|
1490
|
+
"markdown": "- **“No Facebook Pages found”:** the profile you signed in with has no Page access. Check the Page in the portfolio, give yourself access and reconnect.\n- **No ad account to choose, or the wrong ones:** only active ad accounts are listed. Check that the account is not disabled or closed in Ads Manager and that your profile is assigned to it.\n- **“Re-run the connect with --page / --ad-account”:** the profile manages several. Copy the right ids from the message and run the command again with them.\n- **“Already connected to another account”:** check the workspace. Sharing a Meta ad account is supported within one workspace. Social publishing channels still belong to one Sprid account at a time.\n- **Campaign refuses to start with a conversion goal:** inspect the readiness reason. Connect the correct dataset, send a real event, or confirm a recent fire of the selected custom conversion. [The conversion guide](https://sprid.studio/docs/meta-conversions#if-it-fails) covers each repair.\n- **Campaign refuses to start at all:** add a payment method to the ad account (step 5).\n- **Instagram shows “Not linked” on the Ads page or in Promote:** the Page has no Instagram account linked. Link it (step 3), then press **I’ve linked it**. If the Page is linked to a different Instagram account than the one Sprid publishes to, Meta will only promote that other account’s posts: change the link, or connect the Page that belongs to your Instagram account.\n- **“Invalid Scopes”, app unavailable or “URL blocked”:** contact [Sprid support](mailto:hello@sprid.studio). Only Sprid can fix these.\n- **Expiring connection in `sprid status`:** Meta’s sign-in lasts about 60 days. Run the connect again."
|
|
1491
1491
|
},
|
|
1492
1492
|
{
|
|
1493
1493
|
"id": "sources",
|
|
1494
1494
|
"title": "Sources",
|
|
1495
1495
|
"kind": "sources",
|
|
1496
|
-
"markdown": "- [Connect a professional Instagram account and a Facebook Page](https://www.facebook.com/business/help/connect-instagram-to-page)\n- [Create a business portfolio](https://www.facebook.com/business/help/1710077379203657)\n- [Add an ad account to your business portfolio](https://www.facebook.com/business/help/915885887059947)\n- [Ad account limits](https://www.facebook.com/business/help/1026272311098874)\n- [Add a payment method to an ad account](https://www.facebook.com/business/help/132073386867900)\n- [Assign business assets to people](https://www.facebook.com/business/help/325571851329683)\n- [Create a dataset in Events Manager](https://www.facebook.com/business/help/5818684664831465)\n- [About the Conversions API](https://www.facebook.com/business/help/AboutConversionsAPI)"
|
|
1496
|
+
"markdown": "- [Connect a professional Instagram account and a Facebook Page](https://www.facebook.com/business/help/connect-instagram-to-page)\n- [Create a business portfolio](https://www.facebook.com/business/help/1710077379203657)\n- [Add an ad account to your business portfolio](https://www.facebook.com/business/help/915885887059947)\n- [Ad account limits](https://www.facebook.com/business/help/1026272311098874)\n- [Add a payment method to an ad account](https://www.facebook.com/business/help/132073386867900)\n- [Assign business assets to people](https://www.facebook.com/business/help/325571851329683)\n- [Create a dataset in Events Manager](https://www.facebook.com/business/help/5818684664831465)\n- [About the Conversions API](https://www.facebook.com/business/help/AboutConversionsAPI)\n\n- [Ad account currency and time zone changes](https://www.facebook.com/business/help/291404291014138)"
|
|
1497
1497
|
}
|
|
1498
1498
|
],
|
|
1499
|
-
"markdown": "# Meta Ads\n\n**What Sprid does with this:** Prepare campaigns in your own Meta ad account, start them only when you confirm, and report what they spent and returned.\n\n## You need\n\n- A Facebook profile with two-factor authentication turned on. Meta will not let you add ad assets without it.\n- A **business portfolio** at [business.facebook.com](https://business.facebook.com). One per company is enough, even with several apps or brands. See **One portfolio or several** below.\n- A **Facebook Page** for the brand. Ads always run from a Page, including the ones shown on Instagram. To create one, see [Create your social accounts](https://sprid.studio/docs/connect/social-accounts).\n- To promote **Instagram** posts: the Instagram account **linked to that Page**. Meta runs an Instagram ad through the Page the profile is linked to, so without the link only Facebook posts can be promoted.\n- An **ad account** with a payment method
|
|
1500
|
-
"revision": "
|
|
1499
|
+
"markdown": "# Meta Ads\n\n**What Sprid does with this:** Prepare campaigns in your own Meta ad account, start them only when you confirm, and report what they spent and returned.\n\n## You need\n\n- A Facebook profile with two-factor authentication turned on. Meta will not let you add ad assets without it.\n- A **business portfolio** at [business.facebook.com](https://business.facebook.com). One per company is enough, even with several apps or brands. See **One portfolio or several** below.\n- A **Facebook Page** for the brand. Ads always run from a Page, including the ones shown on Instagram. To create one, see [Create your social accounts](https://sprid.studio/docs/connect/social-accounts).\n- To promote **Instagram** posts: the Instagram account **linked to that Page**. Meta runs an Instagram ad through the Page the profile is linked to, so without the link only Facebook posts can be promoted.\n- An **ad account** with a payment method. Several apps in the same Sprid workspace can share it.\n- Optional: a **dataset** (formerly Meta Pixel) if you want campaigns that optimise for signups or purchases.\n\nYou do **not** create a Meta developer app or copy a token. You only sign in with Facebook and pick the Page and ad account.\n\n## Set up Meta (once, before you connect)\n\nSkip any step you have already done.\n\n1. **Business portfolio.** Go to [business.facebook.com](https://business.facebook.com) and create a business portfolio under your company’s legal name.\n2. **Page.** In the portfolio, open **Settings → Accounts → Pages** and add the brand’s Page. Give yourself access that includes **Ads**.\n3. **Instagram (to promote Instagram posts).** On the Page, open **Settings → Linked accounts → Instagram** and connect the brand’s Instagram account. It has to be the same account you publish to from Sprid. Sprid reads the link from Meta each time you promote, so you can do this later without reconnecting.\n4. **Ad account.** Use an existing ad account, or open **Settings → Accounts → Ad accounts → Add → Create a new ad account**. Pick the **currency** and **time zone** carefully. Changing them creates a new ad account and closes the old one, stopping its ads. Choose the currency of the market you advertise in. Give yourself **Manage campaigns** or full control.\n5. **Payment method.** Open **Billing & payments** for that ad account and add a card. Sprid can connect an account without one, but no campaign can start until it has one.\n6. **Dataset (optional).** Open [Events Manager](https://business.facebook.com/events_manager2) → **Connect data sources → Web**, name it after your domain and copy its **dataset id**. Sprid only needs the id. The events themselves reach it from your site or app, through the Meta Pixel or the Conversions API, and without events a conversion campaign has nothing to optimise for.\n\nFor campaigns that optimise for signups or purchases, three more:\n\n7. **Conversions API token** (if your server sends the events). Open the dataset → **Settings → Conversions API** and go through Meta's setup guide to the end. The events and fields you tick there only shape its instructions, not what your server may send. Generate the access token there and store it on your server. Sprid never needs it.\n8. **Dataset settings.** Review Meta’s settings and any restrictions shown for this dataset. A server-only Conversions API integration can send events without browser pixel code.\n9. **Advertising settings.** Review any optional AI tests that change your destination or its presentation. Creative opt-outs cannot verify account-level tests. Define engaged audiences and existing customers before using audience-segment reports.\n\n## Then run\n\n```sh\nsprid connect meta-ads --account <slug>\n```\n\nReplace `<slug>` with your Sprid account slug. Add `--dataset <id>` if you created one in step 6. You can also use **Settings → Accounts → [account] → Meta Ads** in Sprid.\n\n## Click path (the connect)\n\n1. Sign in with the Facebook profile that manages the Page and the ad account.\n2. Leave every requested permission switched on and click **Continue**.\n3. Back in Sprid, pick the **Page** your ads should appear from, then the **ad account**. Each list only appears when there is more than one to choose from.\n4. From the CLI, a profile with several Pages or ad accounts gets a message listing their ids. Run the command again with them:\n\n```sh\nsprid connect meta-ads --account <slug> --page <page id> --ad-account <ad account id> --dataset <dataset id>\n```\n\nWith several apps, connect each Sprid account separately. Pick that app’s Page and dataset each time; you can select the same ad account for apps in the same workspace. Their campaigns and conversion lists stay separate.\n\n## Check the connection\n\nRun `sprid status` and check that Meta Ads shows the intended Page name, followed by “(Ads)”.\n\nFor conversion delivery, complete the intended action and confirm its exact event name in Events Manager. An API response such as `{\"events_received\":1}` only acknowledges the request; it does not prove the chosen event arrived. Then ask your agent to run `dataset_health` for that event. A dataset ID alone cannot pass the conversion launch check. Follow [Run your first Meta conversion campaign](https://sprid.studio/docs/meta-conversions#prove-the-conversion-signal) for testing and custom conversions.\n\nA campaign Sprid prepares starts **paused**. Nothing is spent until you confirm the start.\n\n## One portfolio or several\n\nKeep one business portfolio for your company. Use a Page and dataset for each app. You can share one ad account across apps in the same Sprid workspace, or use separate ad accounts if Meta allows them and you want separate billing.\n\nA shared ad account uses one billing currency. Its payment and account status affect every app advertising through it. Check the currency before choosing budgets.\n\nIn Sprid, open **Ad settings → Shared Meta limits** on any connected account. A workspace owner or admin can set the combined daily budget and monthly spend-and-commitment limit. These limits cover campaigns managed through Sprid. Meta can vary actual daily spend, and changes made directly in Ads Manager can exceed these limits. Lowering a limit blocks new commitments; it does not pause running ads.\n\n**Disconnect** removes only the selected Sprid connection. **Remove access** first lists every connection using the same Meta authorization, then asks you to confirm in Sprid. Removing access stops Sprid from managing those connections; running ads remain on Meta. Reconnect older connections if Sprid cannot identify who authorized them.\n\n## If it fails\n\n- **“No Facebook Pages found”:** the profile you signed in with has no Page access. Check the Page in the portfolio, give yourself access and reconnect.\n- **No ad account to choose, or the wrong ones:** only active ad accounts are listed. Check that the account is not disabled or closed in Ads Manager and that your profile is assigned to it.\n- **“Re-run the connect with --page / --ad-account”:** the profile manages several. Copy the right ids from the message and run the command again with them.\n- **“Already connected to another account”:** check the workspace. Sharing a Meta ad account is supported within one workspace. Social publishing channels still belong to one Sprid account at a time.\n- **Campaign refuses to start with a conversion goal:** inspect the readiness reason. Connect the correct dataset, send a real event, or confirm a recent fire of the selected custom conversion. [The conversion guide](https://sprid.studio/docs/meta-conversions#if-it-fails) covers each repair.\n- **Campaign refuses to start at all:** add a payment method to the ad account (step 5).\n- **Instagram shows “Not linked” on the Ads page or in Promote:** the Page has no Instagram account linked. Link it (step 3), then press **I’ve linked it**. If the Page is linked to a different Instagram account than the one Sprid publishes to, Meta will only promote that other account’s posts: change the link, or connect the Page that belongs to your Instagram account.\n- **“Invalid Scopes”, app unavailable or “URL blocked”:** contact [Sprid support](mailto:hello@sprid.studio). Only Sprid can fix these.\n- **Expiring connection in `sprid status`:** Meta’s sign-in lasts about 60 days. Run the connect again.\n\n## Sources\n\n- [Connect a professional Instagram account and a Facebook Page](https://www.facebook.com/business/help/connect-instagram-to-page)\n- [Create a business portfolio](https://www.facebook.com/business/help/1710077379203657)\n- [Add an ad account to your business portfolio](https://www.facebook.com/business/help/915885887059947)\n- [Ad account limits](https://www.facebook.com/business/help/1026272311098874)\n- [Add a payment method to an ad account](https://www.facebook.com/business/help/132073386867900)\n- [Assign business assets to people](https://www.facebook.com/business/help/325571851329683)\n- [Create a dataset in Events Manager](https://www.facebook.com/business/help/5818684664831465)\n- [About the Conversions API](https://www.facebook.com/business/help/AboutConversionsAPI)\n\n- [Ad account currency and time zone changes](https://www.facebook.com/business/help/291404291014138)\n",
|
|
1500
|
+
"revision": "a6934ad7446d8f53"
|
|
1501
1501
|
},
|
|
1502
1502
|
{
|
|
1503
1503
|
"id": "google-ads",
|
|
@@ -1665,8 +1665,8 @@ export const CONTENT_GUIDES = [
|
|
|
1665
1665
|
"title": "Use Sprid in chat",
|
|
1666
1666
|
"summary": "Take an app from setup to a reviewed post and confirmed delivery using only your chat app and a browser. No CLI or installed skills needed.",
|
|
1667
1667
|
"url": "https://sprid.studio/docs/chat",
|
|
1668
|
-
"markdown": "# Use Sprid in chat\n\n**What this guide does:** Take an app from setup to a reviewed post and confirmed delivery using only your chat app and a browser. No CLI or installed skills needed.\n\nYou tell the agent what you want; it handles the tool calls below. The tool names are here so the agent knows the order. Setup should only ask for the connection the current task needs.\n\n## Connect Sprid\n\nChoose your chat at `https://sprid.studio/start#chat` and follow its steps. Your chat may call this an app, connector or integration. The address is `https://api.sprid.studio/api/mcp?surface=core`. Sign in to Sprid and authorize the right workspace.\n\nAgent: once the app exists, start with `get_marketing_plan` and reuse its prepared work and next step rather than starting over. Discovery can start from a website or store URL, with no social account.\n\n## Pick the app and account\n\nAgent: call `get_capabilities`, then `list_apps` and `list_accounts`, and reuse what exists. If the app is missing, get its description or website/store URL, confirm it and create it with `upsert_app_profile`. A content account owns voice and media; a channel is one exact social destination. Never pick a handle just because it is first in a list. If the app has no account, ask what to call it
|
|
1669
|
-
"revision": "
|
|
1668
|
+
"markdown": "# Use Sprid in chat\n\n**What this guide does:** Take an app from setup to a reviewed post and confirmed delivery using only your chat app and a browser. No CLI or installed skills needed.\n\nYou tell the agent what you want; it handles the tool calls below. The tool names are here so the agent knows the order. Setup should only ask for the connection the current task needs.\n\n## Connect Sprid\n\nChoose your chat at `https://sprid.studio/start#chat` and follow its steps. Your chat may call this an app, connector or integration. The address is `https://api.sprid.studio/api/mcp?surface=core`. Sign in to Sprid and authorize the right workspace.\n\nAgent: once the app exists, start with `get_marketing_plan` and reuse its prepared work and next step rather than starting over. Discovery can start from a website or store URL, with no social account.\n\n## Pick the app and account\n\nAgent: call `get_capabilities`, then `list_apps` and `list_accounts`, and reuse what exists. If the app is missing, get its description or website/store URL, confirm it and create it with `upsert_app_profile`. A content account owns voice and media; a channel is one exact social destination. Never pick a handle just because it is first in a list. If the app has no account, ask what to call it. Use `create_content_account` when your connector exposes it. If that tool is unavailable, open `https://app.sprid.studio/settings/accounts/new?workspaceId=<workspace-id>&app=<app-id>` with the selected app’s workspace and app IDs filled in. Ask the person to confirm that app and create the account, then read `list_accounts` again.\n\nThen ask whether the app has users yet. If it does, offer the analytics and revenue connections `next_actions` lists first: each link opens Sprid’s secure form, and the first recommendation then reads real numbers. If it has none yet, go on to the first post; connecting stays optional.\n\n## Connect a destination or data source\n\n- **Social channel:** `get_documentation` with the platform name, then `connect_channel`. The person opens the returned link and approves in the browser; `channel_status` confirms the saved result.\n- **Store or analytics:** the person enters identifiers and the key in the secure form at `https://app.sprid.studio/settings/app-profiles`. Credentials never go in chat, tool arguments or reports. A saved-key indicator only proves configuration; `get_marketing_review` reports source status and freshness; inspect those before treating data as current.\n\n## Bring images or a finished video\n\nDraft copy in the chat, in the account’s voice. No extra model key needed.\n\nIf the chat exposes the attached files, pass them to `import_assets`: each file has `download_url` and `file_id`, optionally `mime_type` and `file_name`. Public HTTPS URLs and existing `{kind, id}` library references also work. Limits: PNG, JPEG, WebP or finished MP4, up to 32 MiB each. Each position reports on its own; retry the missing ones before building the post. Files are copied to the account and deduplicated by bytes; temporary download URLs are not kept.\n\nA `sandbox:` URL or local path can’t be fetched. For an image the chat generated, use the existing file if the host exposes it (not verified for every host); otherwise download it and use the browser upload. Never ask the agent to recreate an image or transcribe binary data.\n\n**Fallback: browser upload.** Call `create_upload_session` with the account and a new UUID `requestId`. The person opens the link and picks the files. Then read `get_studio_job` with the returned ID; its result keys are zero-based positions. The same session and asset IDs work from any other chat. No image-generation charge.\n\n## Create and review the post\n\nCall `create_post` with a new UUID `requestId`, the account, ordered `{kind, id}` assets, captions and aspect ratio. Reuse the same ID and inputs if a response is lost.\n\n- `presentation: \"finished\"` keeps artwork as-is: no text overlay, no added music. Images fit inside the canvas, which can add margins.\n- `presentation: \"editable\"` allows `text` and `subtitle` on image slides.\n- One finished video becomes one reel. Never mix a video with image slides.\n\nEdit with `update_post` (captions) and `update_slides` (slides). Call `preview_post` and check every slide or the video: captions, order, crop, legibility. It returns a small image of each slide for the agent to look at and an embedded preview for the person; a chat without embedded apps still shows the images. A preview is not approval to publish.\n\nAgent: show the person the preview, and link every post you mention with the `links` the tools return, as a Markdown link on its ref or title: `links.review` when the ask is to approve or schedule, `links.editor` when it is to change something. Never draw the slides in your own widget or artifact: those sandboxes block Sprid's media host and the images show broken.\n\n## Make a reel from photos or text\n\nCheck `get_capabilities` for hosted creation first. `create_reel` takes ordered scenes (`imageId`, `text`, `durationMs`), up to 120 seconds, and makes a silent MP4. It costs 3 Sprid credits per started render minute, or uses the plan allowance: check `spend_status`, tell the person, and get their go-ahead before sending `confirmed: true`. Save the job ID and read `get_studio_job`. Retrying with the same ID never starts a second render; a stopped job reports `needs_attention` instead of charging again.\n\nTo turn an existing post's slides into a video instead, use its own video: `get_video_capabilities`, then `get_post_video` and `edit_post_video` for transitions and motion, and `update_post {outputs}` for which destinations get it. Rendering it is metered the same way: `quote_post_video_render`, the person's go-ahead, then `render_post_video` with `confirmed: true`. Schedule a video destination with the render's `videoRenderId`.\n\nApp captures, simulator recordings and your own build scripts run outside remote MCP. Upload their finished files.\n\n## Approve and confirm delivery\n\nPublishing and scheduling require the person's approval of the preview and intended destinations. The browser review link remains available for picking channels, time and platform options.\n\nTikTok requires an explicit `postMode`; nothing defaults to a direct post or a draft:\n\n- `MEDIA_UPLOAD` sends the media to the creator's TikTok inbox. They finish the post in TikTok; report it as a draft.\n- `DIRECT_POST` requires `privacyLevel`, `disableComment`, `disableDuet`, `disableStitch`, `yourBrandToggle`, `brandedContentToggle` and `confirmed: true` in `options.tiktok`. Fetch fresh choices with `list_connections {includeTikTokCreatorInfo: true}` and show the creator identity, media and caption. The person chooses every setting for this post. Photo posts use `disableDuet: true` and `disableStitch: true`; comments respect the creator's restriction. Branded content cannot use `SELF_ONLY`.\n\nBefore sending `confirmed: true`, get the person's approval of this exact post and their agreement to TikTok's [Music Usage Confirmation](https://www.tiktok.com/legal/page/global/music-usage-confirmation/en), plus the [Branded Content Policy](https://www.tiktok.com/legal/page/global/bc-policy/en) for branded content. That flag is a caller's attestation, not proof of a click in TikTok's prescribed UI. Never invent choices or carry usual preferences into a new post. If choices are missing, open the returned `approvalUrl` for the person to answer in Sprid.\n\nAn approval covers the version the person saw. If you edit a scheduled direct post afterwards, the edit's result carries `tiktokApproval`: the scheduled post will not publish as it is. Show the person the change and the settings in its `rebook` call, ask whether to post this version, and only after they agree call `rebook` with `options.tiktok.confirmed: true` added. `post_queue` returns the same call while the approval is outstanding.\n\nChat approval does not establish that TikTok's required posting controls have been completed. Use the Sprid review link for those controls and consent before confirming delivery; do not describe chat-only approval as platform-verified.\n\nAfter approval, read `post_queue`. Keep draft, scheduled, failed and delivered apart, and report delivery only with the platform receipt. Read a failure before retrying. Never repeat a publish call just because a response was lost.\n\nFor results, read `get_documentation` for `marketing-review`. Keep the same app, account and post IDs across clients. `next_actions` gives the next step; an empty `verb` means follow its browser link.\n\n## Limits and troubleshooting\n\n- **Attachment can’t be read:** use the browser upload, not the CLI.\n- **Missing account or service:** set it up in the dashboard. Never paste credentials into chat.\n- **Hosted creation:** check `get_capabilities` before promising it. Store screenshots and store releases are local unless it reports otherwise.\n- **File transfer:** whether a host exposes uploaded or generated files has to be checked per host. A supported schema doesn’t prove it.\n",
|
|
1669
|
+
"revision": "d37b92a0d99f8a24"
|
|
1670
1670
|
},
|
|
1671
1671
|
{
|
|
1672
1672
|
"id": "local",
|
|
@@ -1679,18 +1679,18 @@ export const CONTENT_GUIDES = [
|
|
|
1679
1679
|
{
|
|
1680
1680
|
"id": "marketing-review",
|
|
1681
1681
|
"title": "Review marketing from connected evidence",
|
|
1682
|
-
"summary": "
|
|
1682
|
+
"summary": "Review marketing and business outcomes for an app, shop or service through connected evidence, using its actual customer path and stating what remains unknown.",
|
|
1683
1683
|
"url": "https://sprid.studio/docs/marketing-review",
|
|
1684
|
-
"markdown": "# Review marketing from connected evidence\n\n**What this guide does:**
|
|
1685
|
-
"revision": "
|
|
1684
|
+
"markdown": "# Review marketing from connected evidence\n\n**What this guide does:** Review marketing and business outcomes for an app, shop or service through connected evidence, using its actual customer path and stating what remains unknown.\n\n## Start with the question\n\nName the app, the date range and the decision the review should inform.\n\nThe App Profile selects the business; it does not require a mobile app. Reuse the saved goal, audience and customer path; investigate only missing or changed parts. For an app, use its meaningful-use and purchase definitions; for a shop, the product-page → checkout → paid-order path and relevant fulfillment/returns; for a service, qualified enquiry → paid delivery. These are examples, not compulsory funnels. Mark irrelevant metrics not applicable and relevant missing evidence unknown. Keep orders, items and people separate. Full method: [business context and outcomes](../../../references/review-to-action.md#1-follow-the-money-then-split-volume-from-rate).\n\n1. `list_apps` and `list_app_profiles` to find the app.\n2. Reuse complete context already returned by `get_marketing_plan`, `get_marketing_review_session` or a full review packet for this app. Otherwise call `get_marketing_review_context` for definitions, investigations, decisions and corrections. A compact summary is not complete context; keep the loaded revision until a reported change or conflict.\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` queues background collection; inspect progress and freshness before treating the result as updated; `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. Use available public pages and campaign/CMS history; ask for a change summary only when it would affect the decision 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- Read the PostHog source's measurement `checks` before any rate. Each one (a platform whose paywall never sells, purchase events with suspect geography, concentrated traffic that needs investigation, possible store-review activity, a signup surface the registration event misses, a dead funnel entry) says a number cannot be read at face value. A platform at zero revenue is not zero demand until its paywall is known to sell.\n- Store impressions by source are listing exposure; downloads by source are acquisition. Do not quote one as the other, and treat every store referrer as a last-click floor.\n- Many email opens with no recorded clicks warrants checking both tracking and actual engagement; it does not establish either explanation.\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- Missing events on the relevant path require a concrete measurement check or proposal; they do not supply an inferred funnel. App onboarding is relevant only when it exists.\n- Keep revenue sources separate until the overlap check says they can be added.\n- Payment totals do not supply a shop's order states or contribution costs, or a service's qualified leads. Use supported records or scoped exports where available and label unsupported connections. Do not invent profile fields or assume a commerce integration.\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\nChoose a concrete activity from the business evidence. Prepare further material only within the user's authorization. Match the deliverable to actual access: ready-to-review content, a supported CMS draft, a tested repo patch or a change brief with exact destination, proposed copy/behavior and verification. A needed checkout or product repair can outrank more content. Creating an external issue is a handoff; verify the actual delivery before following its effect.\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. Read `next_actions` once in scope, reusing a current result. Check [remaining setup](../../../references/setup-continuation.md#keep-setup-current) for gaps that affect the review or requested unfinished setup; do not restart completed identity/icon/voice work or repeat declined connections. 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 selected non-secret goals, definitions, investigations, decisions, corrections and release references with `save_marketing_review_context`, merging with the context at `baseRevision`. If context is empty, derive the proposed summary from existing material and verify the saved result. Saving a review verdict does not save these fields. Accepted goals need no independent research; factual claims and definitions need rechecking when relevant changes, contradictory evidence or a recorded revisit condition call for it. Keep required investigations and due audits. A conflict returns both versions; reconcile before retrying. 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",
|
|
1685
|
+
"revision": "aedb94bf4d89b143"
|
|
1686
1686
|
},
|
|
1687
1687
|
{
|
|
1688
1688
|
"id": "traffic",
|
|
1689
1689
|
"title": "Check whether a traffic spike is real",
|
|
1690
|
-
"summary": "
|
|
1690
|
+
"summary": "Helps investigate a jump in website visitors and prepare a narrow, reviewable exclusion when evidence supports it.",
|
|
1691
1691
|
"url": "https://sprid.studio/docs/traffic",
|
|
1692
|
-
"markdown": "# Check whether a traffic spike is real\n\n**What this guide does:**
|
|
1693
|
-
"revision": "
|
|
1692
|
+
"markdown": "# Check whether a traffic spike is real\n\n**What this guide does:** Helps investigate a jump in website visitors and prepare\na narrow, reviewable exclusion when evidence supports it.\n\nA spike can be a successful referral, automated traffic or a mixture. Sprid's\n**Review that day** action is a prompt to investigate, not a bot verdict. Keep raw\ncounts beside any filtered comparison and explain what remains uncertain.\n\n## Inspect the traffic\n\n1. **Locate the change.** Split the period by day and hour. Compare the spike with\n campaigns, referrals and ordinary traffic. A concentrated burst alone does not\n establish that the visitors are automated.\n2. **Read behavior.** Compare pages per visitor, navigation and meaningful events.\n A crawler can send several pageviews; a real reader may need only one page.\n Neither ratio proves who visited.\n3. **Inspect combined groups.** `review_traffic` groups browser, screen, timezone,\n referrer and other dimensions together. Counts from separate dimensions can\n describe the same visitors and must not be added.\n4. **Read the pages and sources.** Broad repeated requests across a catalogue may\n suggest automation. Check whether a real campaign, shared device or monitoring\n service explains the same pattern before excluding it.\n\n## Treat fingerprints as clues\n\nCompare combinations of browser properties, request timing, destinations and\nreferrers. A shared viewport, missing referrer or timezone mismatch is insufficient\non its own: ordinary people can share those properties or use a VPN.\n\nDo not infer a bot from country, language or a device model. Seek independent\nsupport such as verified crawler identification or matching server-log behavior\nwhen those sources are available. Preserve unresolved traffic as uncertain.\n\n## Check both comparison periods\n\nApply the same investigation to the baseline. Removing a spike only from the\ncurrent period can distort the reported change when similar traffic exists in\nthe earlier one. Query suspected patterns across both windows before comparing.\n\nUse matching metric definitions. Sprid's website counts already apply their\nconfigured exclusions; a raw provider query can include traffic that never\nentered the displayed total. Read the returned coverage and filter notes before\ntrying to reconcile or exclude it again.\n\n## Prepare a narrow exclusion\n\nChoose conditions supported by the customer's evidence and limit the dates to\nthe verified interval. Preview the affected visitors and the remaining traffic\nin both periods. Check that known customer activity is not caught by the rule.\n\nThe following is a fictional schema example, not a suggested device blacklist.\nReplace its dates, conditions and reason with verified evidence before saving:\n\n```json\n{\n \"id\": \"verified-crawl-example\",\n \"reason\": \"Example only: record the corroborating evidence, date range and affected counts here.\",\n \"start\": \"2026-01-10\",\n \"end\": \"2026-01-11\",\n \"enabled\": false,\n \"conditions\": [\n { \"field\": \"browser\", \"value\": \"Example crawler\" },\n { \"field\": \"screenWidth\", \"value\": \"800\" }\n ]\n}\n```\n\n- End dates are exclusive and UTC.\n- Conditions in a rule are AND; separate rules are OR.\n- Fields: `browser`, `browserVersion`, `os`, `osVersion`, `screenWidth`,\n `screenHeight`, `timezone`, `referrer`, `country`. Values are exact strings;\n a missing property never matches.\n- Write the evidence and affected counts in `reason`. Keep unrelated exclusions\n and profile settings intact.\n\nWhen authorized, save through `upsert_app_profile` or `PATCH /api/app-profiles/:id`\nunder `posthogConfig.trafficExclusions`. Read back the configuration and affected\nmetrics, then log the measurement change with its areas and reason so a later\nreview can explain the difference.\n\n## What changes\n\nAn exclusion filters Sprid's website readings; it does not delete provider data.\nApp activity, registrations and revenue use their own definitions. Raw connected\nqueries and the marketing-review event inventory remain available as evidence.\nDisabling the rule restores that traffic to the filtered readings.\n\nKeep an unexplained group visible. If the available evidence cannot separate\ncustomers from automation, report that limitation instead of saving an exclusion.\n",
|
|
1693
|
+
"revision": "e29d458977ec10d2"
|
|
1694
1694
|
},
|
|
1695
1695
|
{
|
|
1696
1696
|
"id": "pinterest-content",
|
|
@@ -1703,26 +1703,34 @@ export const CONTENT_GUIDES = [
|
|
|
1703
1703
|
{
|
|
1704
1704
|
"id": "distribution",
|
|
1705
1705
|
"title": "Why a post travels",
|
|
1706
|
-
"summary": "
|
|
1706
|
+
"summary": "Helps choose where and how to publish, then evaluate whether distribution serves the customer's business goal.",
|
|
1707
1707
|
"url": "https://sprid.studio/docs/distribution",
|
|
1708
|
-
"markdown": "# Why a post travels\n\n**What this guide does:**
|
|
1709
|
-
"revision": "
|
|
1708
|
+
"markdown": "# Why a post travels\n\n**What this guide does:** Helps choose where and how to publish, then evaluate\nwhether distribution serves the customer's business goal.\n\nUse the account's audience, available formats and measured results. Treat claims\nabout algorithm weights, ideal slide counts or guaranteed reach as unverified\nunless a current source supports them. A platform average is context for a test;\nit does not establish what will work for this account.\n\n## Choose the destination\n\nMatch the content to a real audience and a useful next step. An explainer might\nanswer a search question, demonstrate a product or help someone recognize a\nproblem. Choose its channel from evidence about that audience and the customer's\nexisting distribution, rather than making every business start with social posts.\n\nFor a connected channel, inspect publishing status and account restrictions\nbefore preparing a schedule. A failed publish is different from a published post\nwith low reach. Respect the destination's current format requirements and the\ncustomer's approved publishing scope.\n\n## Build for the actual format\n\nFor an Instagram or TikTok carousel, make the opening understandable and let\neach following slide advance the idea. Preview the full deck at its intended\nsize. Use enough slides to deliver the promise; a fixed slide count is not a\nquality check or a guarantee of distribution.\n\nFor video, inspect the opening, pacing and delivered result using\n[first seconds](../../reels/guides/first-seconds.md). A question, demonstration or\nstory can each work when it matches the purpose and the customer's voice.\n\nFor Pinterest, use the [Pinterest guide](pinterest.md). Prepare a standalone Pin\nwith a relevant destination, board and search wording. Evaluate it with its own\nmetrics and observation window rather than importing a reel's retention target.\n\n## Decide what to measure\n\nRecord the intended outcome before publishing. Depending on the task, it can be\nqualified visits, an order, an account signup or a useful audience response.\nState how the content connects to that outcome and where attribution stops.\n\nRead only metrics the connected tools actually provide. Keep views, reach,\ncomments, saves and outbound clicks distinct. Platform engagement does not by\nitself establish visits or sales. A missing metric is unknown, not zero.\n\nCompare content at similar ages and separate paid from organic distribution.\nInclude the sample size and spread, since one unusual post can dominate an average.\nAvoid comparing different metric definitions as though they measured the same thing.\n\n## Choose a workable cadence\n\nUse the customer's objectives, production process and measured delivery to set\na schedule. The saved posting schedule is a preference to follow, not evidence of\nan algorithmic advantage. Test timing when there is enough comparable activity\nto learn from it.\n\nPrepare a repeatable content process and keep the queue visible. Requested volume\nand quality both matter; do not reduce the user's ambition to a fixed weekly\nquota. Reply to relevant comments within the authorization the user has given.\nA research or publishing request does not authorize unrelated likes or outreach.\n\n## Learn from a result\n\nWhen a post meets its intended outcome, identify what could explain it and test\nthat explanation with another example. Reuse a format when the evidence supports\nit, while writing for the new topic and the account's voice.\n\nKeep each experiment's hypothesis, publication references and results with the\ncustomer's plan. If several variables change, attribute the outcome to the whole\nvariant until further evidence separates them. Preserve unsuccessful examples\nas well as successful ones so later reviews can compare them.\n\n## Recheck external claims when they matter\n\nFor a platform requirement or a claimed ranking mechanism, consult current\nfirst-party documentation and retain the source and date. Separate a platform's\npublished explanation from third-party speculation and from this customer's\nmeasured results. If the distinction could change the recommendation and the\nclaim cannot be verified, leave it out of the decision.\n",
|
|
1709
|
+
"revision": "c29132f09797a901"
|
|
1710
|
+
},
|
|
1711
|
+
{
|
|
1712
|
+
"id": "meta-conversions",
|
|
1713
|
+
"title": "Run your first Meta conversion campaign.",
|
|
1714
|
+
"summary": "Takes you from Meta account setup to a conversion campaign prepared in Sprid, with a real event checked before you approve spending.",
|
|
1715
|
+
"url": "https://sprid.studio/docs/meta-conversions",
|
|
1716
|
+
"markdown": "# Run your first Meta conversion campaign.\n\n**What this guide does:** Takes you from Meta account setup to a conversion campaign prepared in Sprid, with a real event checked before you approve spending.\n\nA conversion campaign asks Meta to find people likely to complete an action in your app. Choose the action you want, prove that Meta receives it, then build the campaign. Your agent prepares the ads. You review the campaign and press Launch in Sprid.\n\n## You need.\n\n- Admin access to the Meta business portfolio and ad account that will pay for this app’s ads.\n- A Facebook Page for the app, linked to its professional Instagram account if you want Instagram delivery.\n- Access to your app’s server and domain DNS. A developer needs to send your conversion events to Meta.\n- A Sprid account connected to Meta Ads, and a person signed in to Sprid to approve the reviewed budget.\n\n## Set up Meta once.\n\n1. **Create or choose the business portfolio.** Keep the app’s Page, ad account and dataset under the business that owns them. Your personal Facebook login administers those assets.\n2. **Create the Page and link Instagram.** The Page supplies the identity for the ad, including ads that run only on Instagram. Check the linked handle before connecting Sprid.\n3. **Choose or create the ad account and add billing.** Choose its currency and time zone carefully. Changing them creates a new ad account and closes the old one, stopping its ads. Sprid’s budget amounts use the connected ad account’s currency.\n4. **Create the dataset in Events Manager.** This is where Meta receives conversion events. Keep its ID. A server-only Conversions API integration can use the dataset without installing browser pixel code.\n5. **Verify the app’s domain.** In Meta’s business settings, follow domain verification and add the supplied DNS TXT record with your DNS provider. Verify the domain where your conversions happen.\n6. **Create the Conversions API token.** Use the dataset’s direct integration setup in Events Manager. Save the token in your server’s secret settings. This token sends your app’s events; Sprid’s Meta Ads connection manages campaigns separately.\n7. **Review account-level advertising settings.** Check any optional AI tests that change the destination or its presentation. Your agent’s creative settings cannot prove what an account-level test will do. If you use Meta’s audience-segment reports, define the engaged audience and existing customers there before interpreting those reports.\n8. **Connect Meta Ads to Sprid.** Follow [Connect Meta Ads](https://sprid.studio/docs/connect/meta-ads), selecting this app’s Page, ad account and dataset. Set the daily, campaign and monthly spending caps in Sprid’s account settings. If several apps share the ad account, a workspace owner or admin also sets **Shared Meta limits** there. Each app keeps its own Page and dataset; the shared limits cover campaigns managed through Sprid.\n\n## Prove the conversion signal.\n\n1. In Events Manager, select the dataset and open **Test events**. Add its test code to your server’s Conversions API request as `test_event_code`.\n2. Complete the chosen action through the app. Check that Events Manager receives the exact event name and reports a server connection. A successful HTTP request alone does not prove the expected event arrived.\n3. Inspect the event details. Website events need `event_source_url` and the visitor’s `client_user_agent`. Send a stable `external_id` for matching. When a real Meta click supplies `fbclid`, preserve it through redirects and send the correctly formatted `fbc` value. Keep the click ID’s case unchanged.\n4. Check `Purchase` with the actual paid amount and currency if the app sells something. Store the visitor’s user agent and click ID when they arrive, so a later payment webhook can use the original values.\n5. Remove `test_event_code` for production. Complete another real action and check its arrival, then ask your agent to run `dataset_health` for the event you will optimize on.\n\nEmail and phone identifiers need Meta’s normalization and hashing. Hashing `external_id` is recommended; keep its representation consistent across integrations. `fbc` and `client_user_agent` must remain unhashed. See [customer information parameters](https://developers.facebook.com/documentation/ads-commerce/conversions-api/parameters/customer-information-parameters) when implementing the server call.\n\nTest codes identify test payloads. Meta says those events still flow into Events Manager and can be used for targeting and measurement. Test only with actions you actually perform, then remove the code.\n\n`dataset_health` checks recent event counts and, when Meta provides it, event match quality. Its count check covers seven days and Meta’s top 100 events per hour. An event absent from that response is unobserved. A match-quality score does not prove that a recent conversion happened.\n\n## Choose an event that predicts payment.\n\nUse `Purchase` when purchases happen often enough to give the campaign a useful signal. If purchases are sparse, choose a completed action in the same visit that customers take before paying. Check the connection to payment in your own analytics.\n\nMeta says ad sets usually leave learning after about 50 results in the week following the last significant edit. This is guidance about stable delivery. Results below that volume can still tell you about a broken checkout or whether anyone paid.\n\nStandard events use their exact names, such as `Purchase`, `Lead` or `CompleteRegistration`. For your own event, such as `RecipePlanCompleted`, ask your agent to:\n\n1. Run `dataset_health` without a conversion selection and find the exact name in `datasetHealth.events`. Check that the returned `datasetId` is the connected dataset you intend to use. The readiness result may still ask you to select a conversion.\n2. Run `list_custom_conversions` to check for an existing matching rule.\n3. If needed, call `create_custom_conversion` with a name and the exact `eventName`. Keep the returned ID.\n4. Complete the action again after creating the rule. The custom conversion itself must have a recent fire before launch; an earlier underlying event cannot prove a new rule worked.\n\nA custom conversion is a named rule on events in your dataset. Sprid creates a case-sensitive event-name rule. The ad set selects that rule by `customConversionId`.\n\n## Build the campaign through Sprid.\n\nGive your agent the intended countries, total budget, run length, destination and hypothesis. State the regulated category before the campaign is created. Housing, employment, financial products and services have targeting restrictions; declare `NONE` only when no category applies. [Meta’s category guide](https://developers.facebook.com/docs/marketing-api/audiences/special-ad-category/) explains the scope.\n\nAsk the agent to follow this order:\n\n1. **Check readiness:** `ads_overview`, then `dataset_health` with the chosen `conversionEvent`. Fix missing connections or an unobserved event before creating ads.\n2. **Create the paused campaign:** `create_ad_campaign` with an objective such as `OUTCOME_SALES` and explicit `specialAdCategories`. Political campaigns also need explicit `countries`. For housing, credit and employment, Sprid derives the campaign countries from the ad sets.\n3. **Create the paused ad set:** `create_ad_set` with `OFFSITE_CONVERSIONS`, the chosen event, countries and reviewed budget. Use explicit Facebook and Instagram Feed, Stories and Reels placements for paired creatives. Set `targetCostPerEventCents` as the cost you will judge results against. This is a reporting target, not a bid. Use `startAt` if you want a later start.\n4. **Prepare the creative:** `render_ad_creative` builds from a named template, or `create_ad_creative` accepts your own material. Ask for feed and vertical assets. An external pair must identify whether its assets are images or videos. Keep `destinationUrl` free of hand-written `utm_*` parameters; Sprid adds tracking.\n5. **Attach and review:** use `create_ads` to attach the creatives, review each one with `review_ad_copy`, then run `ads_preflight`. Check previews for Feed, Stories and Reels. A passing comparison confirms the stored fields checked against Sprid’s intent; it does not guarantee Meta will approve delivery.\n6. **Approve in Sprid:** `launch_ads` returns the reviewed commitment and an `approvalUrl`. Open that link, check the amount and end date, then press Launch. The agent cannot start spending or raise the budget.\n\nThe following is a fictional ad-set input for Pantry Note on a USD ad account. The IDs are examples. Choose your own budget and target cost before preparing a campaign.\n\n```json\n{\n \"account\": \"pantrynote\",\n \"campaignId\": \"00000000-0000-4000-8000-000000000001\",\n \"name\": \"Completed recipe plan\",\n \"optimizationGoal\": \"OFFSITE_CONVERSIONS\",\n \"conversionEvent\": { \"customConversionId\": \"123456789\" },\n \"dailyBudgetCents\": 1000,\n \"durationDays\": 7,\n \"countries\": [\"US\"],\n \"targeting\": {\n \"publisher_platforms\": [\"facebook\", \"instagram\"],\n \"facebook_positions\": [\"feed\", \"story\", \"facebook_reels\"],\n \"instagram_positions\": [\"stream\", \"story\", \"reels\"]\n },\n \"targetCostPerEventCents\": 500,\n \"hypothesis\": \"Showing the completed meal plan brings customers who purchase.\"\n}\n```\n\nFor a standard event, use `\"conversionEvent\": {\"standard\": \"Purchase\"}`.\n\nSprid requests opt-outs for the documented creative enhancements it supports and checks Meta’s stored settings before launch. It supplies feed and vertical assets when paired material is available. Conversion ad sets store explicit attribution settings: seven-day click, one-day view and one-day engaged video view. A mismatch found by preflight blocks launch.\n\n## Read where people stop.\n\nUse `ad_status` to check delivery and rejection messages. Ask for `ad_council` to compare cost per optimized event with your target. Then compare Meta’s clicks with your app’s actual outcomes.\n\nThe stage-by-stage diagnosis below uses Meta reports and your own analytics. Verify each stage against its source and keep attribution gaps explicit.\n\n| Stage | Compare | Investigate when that stage loses people |\n|---|---|---|\n| Ad | Link clicks per impression | The opening, image or promise |\n| Page loads | Landing-page views per link click | Load errors, slow pages or redirects |\n| App starts | Started actions per landing-page view | Message match and the first action |\n| App completes | Completed actions per started action | Product failures or confusing steps |\n| Customer pays | Paid customers per completed action | The offer, price presentation or checkout |\n\nCompare a consistent cohort and period. Repeat actions by one person are different from independent potential buyers. Keep campaign totals separate from their ad-set totals, since they overlap.\n\nWith zero purchases, the table gives approximate one-sided 95% upper bounds on the purchase rate. It assumes independent comparable people, complete measurement and enough follow-up for them to purchase. Use it to decide what to investigate; a sample alone cannot prove an offer will fail.\n\n| Completed actions from different people, with zero purchases | Approximate upper bound |\n|---|---|\n| 30 | 10% |\n| 60 | 5% |\n| 100 | 3% |\n\nCompare that bound with the purchase rate your margins require. Confirm checkout and purchase tracking work before blaming the offer. [The statistical rule](https://jhanley.biostat.mcgill.ca/c607/ch08/zero_numerator.pdf) explains the approximation.\n\nChanges to targeting, creative or the optimization event can restart learning. Budget changes depend on their size. Record each campaign change and its reason. Fix product and checkout bugs when you find them; changing page content alone is different from editing the ad’s destination or creative.\n\n## If it fails.\n\n- **No recent event observed:** check the dataset ID and server response, perform the action, then recheck Events Manager. Remove the test code before the production check.\n- **Custom conversion has no recent fire:** confirm its dataset and exact event-name rule. Perform the action after the rule exists. Do not substitute an unrelated standard event to pass the check.\n- **Match quality unavailable:** Meta may need dataset permission or an opt-in. Read the score in Events Manager while you resolve access. Missing quality does not turn an observed event into zero.\n- **Preflight names a mismatch:** correct the named setting, or recreate the paused ad set or creative through Sprid when it cannot be edited. Run preflight again. Avoid changing tracked fields independently in Ads Manager.\n- **Budget exceeds a cap:** review the daily and total commitment in Sprid’s account settings. A person must approve the spending limit or budget change in the app.\n- **No purchases yet:** verify measurement, then read the completed-action count and follow-up period. A small cohort is inconclusive.\n\n## Sources.\n\nMeta references checked 14 October 2026. Account-specific acceptance still needs a paused create and readback on your connected account.\n\n- [Meta Ads connection](https://sprid.studio/docs/connect/meta-ads) and [ad-account currency changes](https://www.facebook.com/business/help/291404291014138).\n- [Conversions API: sending and testing events](https://developers.facebook.com/documentation/ads-commerce/conversions-api/using-the-api), [server event parameters](https://developers.facebook.com/documentation/ads-commerce/conversions-api/parameters/server-event), and [click ID handling](https://developers.facebook.com/documentation/ads-commerce/conversions-api/parameters/fbp-and-fbc).\n- [Custom conversions](https://developers.facebook.com/docs/marketing-api/reference/custom-conversion/) and [dataset quality](https://developers.facebook.com/documentation/ads-commerce/conversions-api/dataset-quality-api).\n- [Learning phase](https://www.facebook.com/business/help/112167992830700) and [significant edits](https://www.facebook.com/business/help/316478108955072).\n",
|
|
1717
|
+
"revision": "5b9c3c6ecc3864d7"
|
|
1710
1718
|
},
|
|
1711
1719
|
{
|
|
1712
1720
|
"id": "ads",
|
|
1713
1721
|
"title": "Ads: pay to show more people what already worked",
|
|
1714
1722
|
"summary": "Explains how Sprid thinks about paid ads on Instagram, Facebook, YouTube and TikTok: when to spend, how a promotion runs from draft to result, and which numbers to believe along the way.",
|
|
1715
1723
|
"url": "https://sprid.studio/docs/ads",
|
|
1716
|
-
"markdown": "# Ads: pay to show more people what already worked\n\n**What this guide does:** Explains how Sprid thinks about paid ads on\nInstagram, Facebook, YouTube and TikTok: when to spend, how a promotion runs\nfrom draft to result, and which numbers to believe along the way.\n\
|
|
1717
|
-
"revision": "
|
|
1724
|
+
"markdown": "# Ads: pay to show more people what already worked\n\n**What this guide does:** Explains how Sprid thinks about paid ads on\nInstagram, Facebook, YouTube and TikTok: when to spend, how a promotion runs\nfrom draft to result, and which numbers to believe along the way.\n\nChoose a paid campaign from the customer's audience, objective and acquisition\neconomics. Organic results can help select a post to promote, but a paid creative\ncan also be tested directly with an approved budget and a measurable outcome.\nNeither social engagement nor a competitor's activity proves profitable demand.\n\n## Preparing a campaign\n\n**Nothing spends without you.** Everything Sprid or your agent prepares is\npaused. Spending starts only when you press Launch or Confirm in the Sprid app,\non a screen that shows what will run, what it costs a day and for how long. An\nagent can prepare a campaign and send you that link. It can't start one or\nraise a budget itself.\n\n**Give each test a question.** A visual test can hold the copy constant; a\nmessage test can hold the format constant. Record what changes and what result\nwould support the hypothesis. When several elements change together, treat the\nresult as evidence about the complete variant.\n\n**Check how much evidence you have.** Read delivery, conversions and the\nfollow-up period before judging a campaign. A platform's learning status and\nSprid's review guidance answer different questions. A small sample may reveal a\nbroken destination without establishing which creative performs better.\n\n**Judge by what happens in your product.** Use the outcome the campaign is meant to produce, such as a qualified lead,\npaid account or completed order. Check attribution and reconcile platform\nconversions with the business records before estimating acquisition value.\n\n## Two ways to start\n\n**Promote a post.** Pick a post already live, set a daily budget and an end\ndate, and confirm. The ad runs only where that post lives, through the network\nbehind it:\n\n| Post live on | Runs as | Paid by |\n|---|---|---|\n| Instagram | a Meta ad on Instagram only | your Meta ad account |\n| Facebook | a Meta ad on Facebook only | your Meta ad account |\n| YouTube | a Google Ads video campaign (YouTube, Shorts, Discover, Gmail) | your Google Ads account |\n| TikTok | a Spark Ad on TikTok | your TikTok ad account |\n\nInstagram needs one more thing: the Instagram account has to be linked to the\nFacebook Page your Meta ad account runs as, because that is how Meta runs an\nInstagram ad. Until it is, the Ads page shows Instagram as **Not linked**, and\ntapping it says what to do.\n\nSprid checks the ad account can pay before you choose a budget, the network\nreviews the ad, and you get an email when it goes live, is rejected (with the\nnetwork's reason) or ends (with what the money bought). The budget and end date\ncan change while it runs, from Sprid. A TikTok post also needs the creator's\npermission for that one post: a code the TikTok app makes under the post's Ad\nsettings, pasted into Sprid.\n\n**Run a test.** For finding out what to say, not just saying it louder. Tests\nrun on Meta. Every\ncreative is built from a named shape, such as the screen that shows the app's\nverdict about the reader, two columns where the reader does the arithmetic, or\none sourced number. The shape is what makes a result transferable: \"showing the\nverdict beat listing features\" is something you can build again, \"ad 7 beat\nad 3\" is not.\n\nFor a campaign that optimizes for a signup or purchase, follow\n[Run your first Meta conversion campaign](https://sprid.studio/docs/meta-conversions).\nIt covers server events, custom conversions and the fresh checks before launch.\n\n## The checks before money moves\n\n- **The copy check.** Banned words, your own never-say list, invented urgency,\n and openings that name an identity (\"for anxious people\") instead of a moment\n (\"you reread the message four times\"). Warnings pass, errors block the launch.\n- **Regulated categories.** Housing, credit, employment and political ads must\n be declared. An undeclared one isn't quietly rejected, it's pulled with the\n ad account attached. Anything about where people live, what they can borrow or\n who gets hired counts, even when the product doesn't look like it.\n- **The payment method**, before a budget is chosen rather than after Meta has\n accepted the campaign and refused the ad.\n\n## Reading the result\n\nAsk for an ad set's verdict once a week and it answers three questions: did it\nwork, why, and what next.\nWhen a test ends, it writes down what the hypothesis was, what happened and\nwhich shape carried it, so the next test starts from that instead of from zero.\nA losing set is data: close it and say what it ruled out.\n\nTwo rules for the numbers. A campaign's totals and its ad sets' totals are\nseparate readings of the same money, never added together. And a period that\nends today and one that ended last week are two different readings, so every\nnumber says which window it covers.\n\n## What competitors can and can't tell you\n\nMeta's Ad Library shows every active ad: the creative, the copy and the date it\nstarted. It shows no spend, no clicks and no results. Days live means somebody\nkeeps paying, not that it works, so a competitor's ad is \"longest-running\",\nnever \"best-performing\". Sprid's scout research reads the library for your own\nmarket. Use those observations to propose tests, with their sources and limits.\n\n## You need\n\n- An ad account with a card on it, connected to Sprid, for the network behind\n the post: [Meta Ads](https://sprid.studio/docs/connect/meta-ads) for\n Instagram and Facebook, [Google Ads](https://sprid.studio/docs/connect/google-ads)\n for YouTube, [TikTok Ads](https://sprid.studio/docs/connect/tiktok-ads) for\n TikTok.\n- A published post to promote, or, on Meta, an image or video to build a\n creative from.\n- You, signed in to the Sprid app, to press Launch. No agent or token can.\n",
|
|
1725
|
+
"revision": "788bf5758834e4b6"
|
|
1718
1726
|
},
|
|
1719
1727
|
{
|
|
1720
1728
|
"id": "reel-first-seconds",
|
|
1721
1729
|
"title": "The first seconds of a reel",
|
|
1722
|
-
"summary": "
|
|
1730
|
+
"summary": "Helps write and inspect the opening of a vertical video, then test changes against the customer's own results.",
|
|
1723
1731
|
"url": "https://sprid.studio/docs/reel-first-seconds",
|
|
1724
|
-
"markdown": "# The first seconds of a reel\n\n**What this guide does:**
|
|
1725
|
-
"revision": "
|
|
1732
|
+
"markdown": "# The first seconds of a reel\n\n**What this guide does:** Helps write and inspect the opening of a vertical video,\nthen test changes against the customer's own results.\n\nStart from the audience, the purpose of the post and the account's accepted voice.\nA product demonstration, an explainer and a personal story can need different\nopenings. No fixed duration or hook formula is a performance guarantee.\n\n## Make the opening understandable\n\nInspect the rendered first frame and the opening at normal playback speed on a\nphone-sized preview. Check that the subject and any essential text are visible,\nincluding when playback starts without sound. An empty fade or an unreadable\ncaption can delay the point of the video.\n\nChoose the cover separately where the destination supports it. Do not assume\nthat every platform or placement uses the same frame as its cover.\n\n## Match timing to the content\n\nRead the opening aloud and watch it without pausing. Keep enough time to read\nand understand it, then move when the next useful piece of information arrives.\nShorten excess wording before speeding up the cards.\n\nFor text-led videos, inspect each card's reading load, contrast and placement.\nFor demonstrations, allow the viewer to see the action and its result. Captions\nmust follow speech without covering the thing being explained. Validate each\nlanguage separately because translated text and narration have different lengths.\n\nTreat renderer timing defaults as starting points. Keep deliberate pauses when\nthey help comprehension; neither a cut before a fixed second nor a shorter total\nruntime is evidence that the video will perform better.\n\n## Choose an opening worth testing\n\nUse customer language, a recognizable situation, a useful question, a product\naction or a sourced fact. Deliver what the opening promises. A how-to or a question\nis appropriate when it fits the viewer's intent and the content answers it.\n\nWrite alternatives for the same idea before choosing. Keep the account's voice,\naccepted examples and previous corrections with the brief. Do not invent a\nquotation or present a drafted hook as something a customer said.\n\nChoose the next beat so the explanation progresses. End with the useful result\nand a call to action when it serves the purpose of the post.\n\n## Diagnose with available evidence\n\nRead the connected platform's returned metrics and coverage before selecting a\ncomparison. Use retention or opening-view metrics when actually available through\nthat connection or the user's native analytics. Do not promise an API metric that\nthe current tools do not expose.\n\nAverage watch time divided by duration describes average watch proportion. It\ndoes not establish how many viewers stayed through the opening. Views, likes and\nsaves also cannot reconstruct that curve. State a missing metric as a gap.\n\nCompare posts of similar age, audience, placement and distribution. Show the\nsample size and spread. A small or unevenly distributed sample may support a\nnew experiment without establishing a winning format.\n\n## Test one explanation at a time\n\nRecord the hypothesis, what changes and what outcome would support it. For an\nopening test, keep the body comparable. If length, visuals and wording all change,\nreport the result as a comparison of complete variants rather than attributing\nit to one change.\n\nChoose a review point from the customer's posting cadence and observed delivery.\nDo not impose a universal retention threshold, post count or account age before\na decision. A visible defect can be corrected before a performance test finishes.\nConnect creative results to qualified visits, purchases or the declared business\noutcome where measurement supports it.\n\n## A reel as a Pinterest video Pin\n\nPrepare its board, destination, title and description using the\n[Pinterest guide](../../post/guides/pinterest.md). Compare Pins at matching ages\nand keep platform metrics separate from site visits and product outcomes.\nDo not import an Instagram or TikTok retention benchmark into a Pin review.\n\n## Reuse footage across languages\n\nKeep captions and other editable text separate from footage where practical.\nBind overlays to the relevant actions so translated narration can be retimed.\nLocalize product screens when their visible text matters to the explanation.\nPreview each export; a reusable composition still needs language-specific review.\n",
|
|
1733
|
+
"revision": "0d7c80fbc629126f"
|
|
1726
1734
|
},
|
|
1727
1735
|
{
|
|
1728
1736
|
"id": "store-listing",
|
package/src/post/cli.mjs
CHANGED
|
@@ -2030,10 +2030,8 @@ async function cmdSync(cfg, argv) {
|
|
|
2030
2030
|
for (const m of all) {
|
|
2031
2031
|
const cp = captionPath(cfg, m.slug);
|
|
2032
2032
|
if (!existsSync(cp)) continue;
|
|
2033
|
-
//
|
|
2034
|
-
//
|
|
2035
|
-
// title drift" - the same unverified-negative that cost the whole day. One
|
|
2036
|
-
// retry for a restarting dev server, then it counts as drift.
|
|
2033
|
+
// Retry once, then keep an unreadable remote title in the drift report.
|
|
2034
|
+
// A failed read cannot establish that local and remote titles agree.
|
|
2037
2035
|
let post = await api(cfg, "GET", `/posts/${m.sprid.postId}`).catch(() => null);
|
|
2038
2036
|
if (!post) post = await api(cfg, "GET", `/posts/${m.sprid.postId}`).catch(() => null);
|
|
2039
2037
|
remoteTitle.set(m.slug, post ? (post.title ?? "") : UNREADABLE);
|
|
@@ -1,28 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// `baguette` (Homebrew, Apple Silicon, Xcode 26) injects touches through
|
|
6
|
-
// SimulatorKit's private symbols with the iOS-26 calling conventions. Nothing
|
|
7
|
-
// is focused, the pointer never moves, and the Simulator does not come to the
|
|
8
|
-
// front - so a run costs the machine nothing and you can keep working through
|
|
9
|
-
// it. `baguette input` reads newline-delimited JSON gestures from stdin, which
|
|
10
|
-
// is the part that matters: **we keep our own velocity curve.** A canned
|
|
11
|
-
// `swipe --duration` would be a linear interpolation, and a linear
|
|
12
|
-
// interpolation is the single thing that makes a scripted scroll read as
|
|
13
|
-
// scripted.
|
|
14
|
-
//
|
|
15
|
-
// The CGEvent backend below is the fallback, and it is a real fallback rather
|
|
16
|
-
// than a preference: posting to the global HID tap is the ONLY way to reach the
|
|
17
|
-
// Simulator without baguette, and the global tap owns the pointer for the
|
|
18
|
-
// length of the run. Measured 2026-09-06: `CGEventPostToPid`, plain and with
|
|
19
|
-
// fields 91/92 set to the Simulator's window number, delivers without moving
|
|
20
|
-
// the cursor and is ignored. Do not spend another hour on it.
|
|
21
|
-
//
|
|
22
|
-
// Coordinates are normalised 0..1 in both backends. baguette scales x/y by the
|
|
23
|
-
// width/height you pass, so the device's pixel size goes straight in and there
|
|
24
|
-
// is no window geometry to calibrate at all - which also means resizing the
|
|
25
|
-
// Simulator window cannot break a run.
|
|
1
|
+
// Simulator input backends. baguette injects into the selected simulator without
|
|
2
|
+
// taking the desktop pointer. The CGEvent fallback uses the global pointer and
|
|
3
|
+
// requires Accessibility permission. Both take normalized coordinates (0..1).
|
|
4
|
+
// Stream swipe samples at the chosen timing; use dedicated commands for taps.
|
|
26
5
|
|
|
27
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
28
7
|
|
|
@@ -84,7 +63,7 @@ function baguetteInput(udid, px) {
|
|
|
84
63
|
|
|
85
64
|
// **Paced by the clock, not by the acknowledgements.** Awaiting each ack
|
|
86
65
|
// would make the sample rate a function of pipe latency, and the whole point
|
|
87
|
-
// of streaming rather than calling `baguette swipe` is
|
|
66
|
+
// of streaming rather than calling `baguette swipe` is to preserve the
|
|
88
67
|
// timing. Errors are read off stdout in the background and raised at the end
|
|
89
68
|
// of the gesture instead.
|
|
90
69
|
const send = (type, nx, ny) => {
|
|
@@ -100,8 +79,8 @@ function baguetteInput(udid, px) {
|
|
|
100
79
|
* **A tap goes through baguette's one-shot path, not the stream.**
|
|
101
80
|
*
|
|
102
81
|
* The streaming `touch1-*` path is `IOHIDDigitizerDispatch`, built for
|
|
103
|
-
* continuous gestures - swipes, edge pulls, app-switcher drags - and
|
|
104
|
-
* the
|
|
82
|
+
* continuous gestures - swipes, edge pulls, app-switcher drags - and
|
|
83
|
+
* preserves the gesture velocity curve. It is not equally good at
|
|
105
84
|
* a discrete tap: UIKit tab bars can ignore streamed taps that React Native
|
|
106
85
|
* views accept. Use the dedicated tap command for discrete input.
|
|
107
86
|
*
|