Skip to content
DocsTroubleshooting

Workspace

Troubleshooting

The snippet shows no sessions, customers aren't matched, MRR differs from Stripe, the attributed share is low, a sync is failing: causes and fixes.

Each section below starts from what you see, lists the usual causes, then how to check and how to fix it. Most problems come down to one of three things: the snippet isn't sending, the user IDs don't line up across your app and your billing source, or two tools are using different definitions. Start with the browser's developer tools and the Connections screen; between them they answer most questions.

The snippet shows no sessions

The Tracking snippet page says Waiting for the first event next to Last session, and the Connections screen says Not installed.

Likely causes

  • Consent was never granted. This is by design: without joinsight("consent", true), a stored earlier consent, or data-consent="granted" on the tag, the snippet sends nothing.
  • The test started on a page reached through a link on your own site, or consent was given on such a page. Only a landing page starts a session: one opened by typing the address or from another site.
  • The script tag has no data-site attribute, or the site key is wrong.
  • An ad blocker or privacy extension blocked j.js or the request it sends.
  • Your Content Security Policy blocks the script (script-src) or its request (connect-src).
  • The script can't find its own tag. j.js reads data-site from document.currentScript, which is null in a module script (type="module") and isn't your tag if the code is bundled into another file or evaluated by a loader. In those cases it stops silently.
  • The code was pasted inline instead of loaded with src. Without data-api, the snippet works out where to send events from its own src, so inline code fails.

Check

  1. Open a new private window, open the developer tools, type your site's address, and accept your cookie banner on that first page.
  2. In the Network tab, look for j.js, then for a POST to /api/ingest. A working setup returns 204.
  3. Read the result:
    • No j.js request, or a blocked one: the tag isn't on the page, or an extension or CSP blocked it. CSP violations are reported in the Console.
    • j.js loads but no /api/ingest request: no consent, a missing data-site, or document.currentScript was null. If a tag manager or script loader injects the tag, inspect the final script element in the Elements panel and confirm data-site is still on it.
    • 404: the site key isn't one Joinsight knows.
    • 429: more than 120 events a minute from one network address. Behind your own proxy, that budget is shared by all your visitors.

The snippet sends one event per browser session, so reloading the same tab after the first event sends nothing more. Use a new tab or a new private window for each test. The Last session time also updates at most once every five minutes.

Fix

  • Copy the tag again from the Tracking snippet page, or the site key from Settings, Keys. Keep defer, src and data-site as given.
  • Call joinsight("consent", true) from your banner's accept handler, with the queue line before the tag so early calls aren't lost. See the snippet guide.
  • Add the Joinsight origin to script-src and connect-src, and allow the inline queue line with a nonce or hash. Or serve the snippet from your own domain and set data-api: requests then go to the data-api value followed by /api/ingest and /api/identify, which your proxy forwards to Joinsight. Serving it first-party also helps with ad blockers, which drop third-party analytics.
  • Load j.js as a classic script with its own tag, not as a module or part of your bundle.

Once events arrive, the badge shows when the last one came in. After 24 hours without an event it switches to a warning, and the data health alert fires: "The snippet has sent no events for N h."

Sessions arrive but identify() never does

Last session shows a recent time, but Last identify() still says None yet.

Likely causes

  • identify() isn't called, or runs before the user ID exists. Without a user ID it does nothing.
  • It's called before j.js has loaded and the queue line is missing, so joinsight is undefined and the call throws.
  • The signup page doesn't include the snippet. Apps on a separate subdomain often miss it.
  • An ad blocker drops the /api/identify request.

Check

Sign up with a test account and watch the Network tab for a POST to /api/identify. It should return 204. Look in the Console for joinsight is not defined.

Fix

Call joinsight("identify", user.id, user.email) right after signup succeeds, on a page that loads the snippet, with the queue line in place. An invalid email is dropped and the call still counts. If client-side calls are unreliable, call /api/v1/identify from your server with your secret key and forward the visitor's js_aid value as anonymousId. Server calls fail with "Missing secret key" or "Invalid secret key" when the Authorization header is wrong, and with 429 "Too many requests" past 30 calls a minute per secret key. identify() has both forms.

Paying customers aren't matched to a visit

Customers show Unattributed in the customer list, or the setup page says "identify() works (N signups). Waiting for a payment that matches."

Likely causes

  • Different user IDs. Joinsight matches on your user ID first: the ID passed to identify() has to equal metadata.user_id on the Stripe customer or subscription (or payment_intent_data.metadata for one-time payments), or the RevenueCat app user ID. Anonymous RevenueCat IDs ($RCAnonymousID:…) are ignored.
  • No ID and no email match. Without a user ID, Joinsight falls back to the email, but only if both sides have one: the address passed to identify() and the email on the billing customer.
  • They paid before the snippet was live. Customers imported from your billing history have no recorded visit. They count in MRR and stay unattributed. A visit linked later, when they log in and identify() runs, doesn't count either: only a visit on or before the day of the first payment can be credited.
  • Signup on another origin. The anonymous ID lives in localStorage, which browsers keep separate for each origin. A visit recorded on www.example.com isn't visible to a snippet on app.example.com, so the signup there starts fresh. It can show up as a Referral from your own marketing site.

Check

Open a customer from the Revenue screen and compare their user ID with the one your app passes to identify() for the same person. Check the Stripe customer's metadata in the Stripe dashboard.

Fix

Set metadata.user_id on the Stripe customer and subscription at Checkout, using the same ID as identify(), and use that ID as the RevenueCat app user ID. The Stripe card shows the code, and Stripe Checkout metadata explains it. For customers already in Stripe, add metadata.user_id to the Stripe customer: the next sync picks it up, and the recompute links the customer to that user, merging it with any record matched on email. Until then they match by email, so pass emails to identify(). For a separate app subdomain, read js_aid on the marketing site and pass it through signup to a server-side identify call, or run signup on the same origin as the marketing pages. The snippet guide covers subdomains.

Matching runs again a couple of minutes after each identify() call and after each sync that changes billing data, so a test purchase takes a Stripe sync (every 15 minutes) to appear.

MRR doesn't match the Stripe dashboard

Joinsight's MRR is not meant to equal every other tool's number. It follows one written definition, shown in the tooltips and on the definitions page. Compare on these points:

RuleJoinsight
TaxExcluded
DiscountsApplied: MRR is what the customer pays
TrialsExcluded until they convert
Annual and weekly plansAnnual ÷ 12, weekly × 52 ÷ 12
Past-due subscriptionsStill counted until they cancel, and also shown separately as at risk
Statuses countedStripe active and past_due only
One-time paymentsNever in MRR; shown separately as one-time revenue
FeesGross, before Stripe or store fees
CurrencyConverted with the ECB reference rate of the day each billing period started

Check

  • The Stripe card says Test mode when the key is a test key. Test data won't match your live dashboard.
  • The Stripe card shows when it last synced. Changes from the last 15 minutes may not be in yet.
  • If you bill in several currencies, the conversion rule alone can explain a gap: a tool that converts everything at today's rate moves every day, while Joinsight's figures only change at each renewal. The reporting currency is set in Settings, General.

Fix

There's usually nothing to fix, only a definition to line up. Pick two or three customers and compare them one by one; a single mismatched subscription usually shows which rule is the cause. How to calculate MRR explains the reasoning behind each choice.

Combined MRR isn't Stripe plus RevenueCat

A subscription that appears in both Stripe and RevenueCat counts once, from Stripe. Joinsight matches them on the Stripe subscription ID that RevenueCat stores. A person paying on web and mobile is one customer when their user IDs match, with both subscriptions' MRR summed.

Check

The Sources reconcile panel on the Revenue screen lays it out: Stripe, "RevenueCat, excluding Stripe-billed subscriptions", the overlap left out, and Combined MRR. Below it, Joinsight quotes RevenueCat's own dashboard figure, which includes the overlap and is in RevenueCat's currency before conversion. It won't equal the RevenueCat line above it.

Fix

The overlap is only detected when both Stripe and RevenueCat are connected to the workspace. With only RevenueCat connected, the subscriptions it tracks from Stripe count from RevenueCat. Combined MRR for Stripe and RevenueCat covers the approach.

The attributed share is low

The attributed share is attributed new MRR divided by all new MRR in the period. The Overview card also says how many new customers were linked to a first visit. It never reaches 100%, and shows the gap rather than hiding it.

Likely causes

  • Visitors refused consent and signed up on a later page, not the one they landed on.
  • Ad blockers dropped j.js or its requests.
  • Customers who signed up before the snippet was live paid in this period.
  • People who installed your app straight from the store have no web visit to credit.
  • User IDs don't match between identify() and billing (see above).

Device switching mostly skews the share rather than lowering it: someone who reads on their phone and signs up on their laptop is credited with the laptop's first visit, which may be a direct visit. It lowers the share only when the laptop recorded no visit.

Fix

Give it time: the share rises as new customers arrive with the snippet in place, while early periods include customers who signed up before it. Then serve the snippet from your own domain with data-api to reduce blocking, and fix any user ID mismatches. How attribution works explains what can and can't be measured, and first-party analytics and ad blockers covers proxying.

Keyword revenue looks off

Keyword revenue is modeled, and labeled as such. Google never says which query a particular visitor typed, so Joinsight takes each page's organic Google revenue and splits it across that page's queries by their share of its Search Console clicks, month by month.

What to expect

  • Hidden queries. Google leaves anonymized queries out of its query data. Their share of a page's clicks stays in a separate Hidden queries figure, not spread over the visible queries. On a small site this can be most of the total. That's Google's limit, not a sync problem. See Search Console hidden queries.
  • Without search data. Revenue on pages that had no Search Console clicks in the month of the customer's first visit. Check that the Search Console property you picked covers those pages.
  • Small numbers. With a few clicks per page, one query can take a whole customer's MRR. Treat keyword figures as a guide for what to write next, not as an exact amount.

Only customers whose first touch is organic search from Google are included. Page revenue itself is measured, so check the Pages view first if the keyword split surprises you.

Search Console numbers are behind

Search Console data runs 2 to 3 days behind, and the Overview says so. Joinsight syncs Google every 6 hours, but it can't import days Google hasn't published yet. The alert rules allow for this: the money-page rule compares weeks that end 3 days ago.

Don't read the last two or three days as a drop. If data is more than a few days old, check the Google card for an error and see Search Console API limits.

App Store numbers are missing

Likely causes

  • App not picked, or reports not requested yet. After you connect a key, an owner has to pick the app with Use this app. If the app has no report requests yet, the card's Reports line reads "Not requested yet" and it asks for an Admin key, once, under Request the reports with an Admin key, once. Until both are done the card shows Setup not finished and nothing syncs.
  • Reports not generated yet. Apple builds the reports after the request. The card shows "No data yet. App Store data arrives in 1 to 2 days." The one-time history snapshot is best effort and sometimes empty.
  • Normal delay. Each day's report lands a day or two after the day it describes. Joinsight downloads App Store reports every 6 hours.
  • Campaigns not shown yet. Apple adds a campaign to the detailed reports after 24 hours and at least 5 first-time downloads.
  • Small numbers withheld. Apple applies privacy thresholds, so small sources and campaigns can show nothing on some days. Read weeks, not days.
  • The request was stopped. Apple stops a report request when its reports go undownloaded for a while. The card shows "Apple stopped the report request".

Fix

To request reports for the first time, or after Apple stopped the request, enter an Admin key on the card and choose Request reports or Re-request reports. That key is used once and isn't stored. If the error says "This API key can no longer read analytics reports", replace the key with a Sales and Reports, Finance or Admin key. App Store Connect source types explains the delays and thresholds.

A sync is failing

The source's card shows its state: Waiting to sync, Setup not finished, Syncing, Synced with a time, Sync failing or Access revoked. When something went wrong, the error appears in red under the card's title.

How to read it

  • Setup not finished isn't an error. A Google connection has no Search Console property saved yet, or an App Store app has no report requests. Nothing syncs, and Sync now is refused, until you finish on the card.
  • Sync failing is a temporary error. Joinsight retries up to five times, waiting longer each time, and keeps trying at the next scheduled sync. When a source rate limits Joinsight, the retry waits as long as the source asks and doesn't count as an attempt. The Sync activity panel on the source's page lists recent runs and their errors.
  • Access revoked means the source rejected the credentials. Retrying won't help, so Joinsight stops until you reconnect.
  • If the error reads "Syncing failed because of a problem on Joinsight's side. It retries automatically.", the problem is on Joinsight's side, not with your account. Every other message comes from the source or names something you can fix.

Common messages

MessageFix
The restricted key is missing the Customers Read permission. (or another permission)In Stripe, set that permission to Read on the restricted key, or create a new key with the link on the card, then paste the key on the card
Stripe rejected the API key. It may have been deleted or rolled.Create a new restricted key and paste it on the Stripe card
RevenueCat rejected the API key. It may have been revoked.Create a new secret API key (version 2) with the Read permissions the card lists
Google access was revoked or expired. Reconnect Google to resume syncing.Connect Google again from its card
Google denied access: … Reconnect Google with an account that can read this property.Reconnect with a Google account that has access to the property
Plausible's 600 requests per hour limit was reached.Nothing; the sync resumes after the limit resets
The Instagram token expired. Reconnect Instagram on the Connections screen.Reconnect Instagram

After fixing the cause, owners can press Sync now on the source's page rather than wait for the next scheduled sync. A connection with revoked access has to be reconnected first. The data health alert fires within the hour for revoked access, and after 24 hours of continuous failure otherwise.

The Monday brief or alerts don't arrive

Check

  • Settings, Notifications. "Email the Monday brief to every member at 08:00" controls the brief. "Email alerts and new-customer pings to every member" controls alert and ping emails. Only owners can change them.
  • Access. When the trial or subscription has ended, alerts, pings and the brief are paused until a plan is active. The workspace screens redirect to Billing, which says so.
  • Time zone. The brief is sent on Monday from 08:00 in the time zone in Settings, General, which was taken from your browser when the workspace was created.
  • Nothing to report. The brief is skipped while MRR is zero and nothing changed, for example before a revenue source is connected. Preview this week's brief on the Notifications page shows what it would contain.
  • Thresholds. Alerts only fire when a change is large enough to matter. Churn has to be more than twice the weekly average of the previous four weeks, at least 50 in your reporting currency and across at least two customers. Signups have to fall by 30% or more from a baseline of at least 10 a week. Past-due MRR has to be more than twice its four-week average and at least 50. Each rule sends at most one email a day. The full rules are in alerts and the brief.
  • Deliverability. Emails come from hello@joinsight.app. Look in spam, and add the address to your contacts or your mail server's allowlist. Alert subjects start with the workspace name.

The demo workspace never sends the brief and doesn't email alerts.

An AI assistant can't connect over MCP

Check

  • The server URL. Copy it from Settings, AI assistants, and follow the steps there for Claude, ChatGPT or Cursor.
  • The consent screen. Connecting opens Joinsight in your browser. Sign in, choose the workspace and press Allow read access. If you have no workspace yet, the screen says so.
  • The wrong workspace. Each connection reads one workspace. If answers come from the wrong one, or from the Demo workspace, disconnect the app and connect again, choosing the right one.
  • "This app can't connect". The request came from an app Joinsight couldn't verify, for example "The redirect URI doesn't match any URI registered for this app." Start the connection again from the app itself.
  • It worked, then stopped. The connection was disconnected in Settings, AI assistants, went unused for 30 days, or the person who connected it left the workspace. If a client presents an old refresh token again, Joinsight revokes the connection as a precaution. In each case, connect again.
  • Rate limit. Each connection can make 60 tool calls a minute. Beyond that the tool replies with how many seconds to wait.
  • Access paused. When the trial or subscription has ended, every tool call answers that access is paused and where the owner can choose a plan. The connection stays in place and answers again once a plan is active.

The MCP guide has the setup steps for each client.

Getting help

If none of this explains what you see, email hello@joinsight.app. Include the workspace name, the source or screen involved, the exact message on the card or in the Sync activity panel, and roughly when it started. Never send an API key, a secret key or a sign-in link; nobody at Joinsight needs one to investigate.