Funnel splits & JSON import

A split turns a funnel into a table with one row per value of a field, so you can see which channel, campaign, country or plan brings people who make it through. This guide covers the three split sources, how to read the table, and how to import funnels (with their splits) from JSON. Funnels themselves are introduced in Behavioral analytics & funnels.

Three split sources

Pick the split with the Split by picker, in the funnel builder or above the table on the funnel view. No split turns it off.

SourceFieldsCounts
Visit fields (visit)channel, utm_source, utm_medium, utm_campaign, utm_term, utm_content, referrer_host, landing_path, country, device_type, browser, osVisits. Only anonymous events sent with a visit id enter.
Person properties (person)Any profile property. The first_* first-touch properties are listed first.People. Only events sent with a distinctId enter.
Event property (event)A property name, read from the step 1 event.Entries, the same keys as the unsplit funnel.
  • A visit splitreads the fields from the visit's first browser event: the earliest event of the visit that carries visit fields. That is the same rule the Traffic page uses, so an event sent without visit fields cannot move a visit to another channel, and the numbers match Traffic for the same days.
  • A person split groups people by the current value of the property. For first_* properties this makes no difference, since they never change. For anything else (a plan, a status) the table says so.
  • An event split is the older funnel breakdown (breakdownProperty).

Why visits and people are never joined

A funnel is either about visits or about people, never both. Vevee never links an anonymous visit to a person: only the visit's source (channel, campaign, country and so on) is copied into the person's first touch. That is what lets you run without a cookie banner. A funnel that started at a visit and ended at a signed-up person would break that rule, and it would mix units (visits at step 1, people at step 2), so the step rate would mean nothing.

So attribution takes two separate funnels:

  • Visit to signup, split by a visit field: of the visits from each channel, how many reached the signup page and how many signed up.
  • Signup to paid, split by first_channel: of the people who signed up, grouped by the channel of their first touch, how many paid.

The visit funnel

Every step must be an anonymous event sent with the visit id from the browser helper: for example $pageview, then login_page_viewed, then signup_completed. The last step is the one apps usually miss: at signup, send signup_completed as a separate anonymous capture from your server, with the visit fields and the visit id and no distinctId, next to identify(). See step 5 of the attribution guide.

An anonymous visit only converts within the same app day, because the visit hash rotates at local midnight in the app's time zone. A signup the next morning does not count for the visit that started the evening before.

The people funnel

Use identified events: your signup event, then your payment event. People need a stored first touch, which Vevee saves when you call identify() with isSignup: true. Then split by first_channel or any other first_* property.

Reading the split table

  • Columns.The value; the step 1 count; one cell per later step with the count and the rate together, such as “12 · 41%” (reached over the previous required step); and the overall conversion, such as “41% (12 of 29)”. The header names the unit: visits, people or entries.
  • Top 10 plus Other. The table shows the 10 values with the highest step 1 count, plus an Other row that sums the rest. Show all expands it.
  • Missing values get their own row, never folded into Other or hidden: “(none)” on visit and event splits, “(no first touch)” on a first_*split, “(not set)” on any other person property. People who signed up before you deployed first touch land in “(no first touch)”, so that row starts large.
  • Greyed rows. Rows with fewer than 20 entries at step 1 are greyed but keep their numbers. When you sort by a rate they sort below every row that is not greyed.
  • Visits without visit data.A visit whose events all lack visit fields cannot be placed in any row. It is left out (never put into direct) and the table says how many, for example “3 visits without visit data not shown.”
  • App-local days.Split funnels count by the app's local days (the time zone in app settings), which is why a visit split matches the Traffic page. Around midnight the same funnel with and without a split can differ slightly.
  • Open in Traffic. The link in the table header opens the Traffic page with the same date range and, when a row is selected, the matching filter. It only navigates; nothing is joined. Fields with no Traffic dimension (such as utm_content) have no link.
  • Click a row to draw its funnel above the table; click it again, or use Clear, to go back to all rows. Every column header sorts.
  • A visit split combined with a person filter gives an empty table, because anonymous visits have no person properties.

Visit-scoped $pageview events are pruned after 90 days, so a visit split that uses $pageview as a step has no data beyond 90 days (the longest date preset is 3 months).

i
Example data. A signup-to-paid funnel split by first_channel might read ai: 7 paid of 24 signups, paid: 4 of 25. With counts this small both rows would be greyed: read them as a direction, not a result.

Shareable URLs

The funnel view keeps the split, selected row and sort in the URL:

  • split=visit:channel, split=person:first_channel, split=event:plan, or split=none. Overrides the saved split without saving it.
  • row=<value> selects a row. $none is the missing row and $other is Other; a real value starting with $ gets one more $ in front.
  • sort=<col>:<asc|desc>, where col is value, s0, s1... (count at a step), r1, r2... (rate into a step) or overall.
  • all=1 shows every value instead of the top 10 plus Other.

Importing funnels from JSON

The funnel builder's Import JSON dialog accepts one funnel (an object) or many (an array). Keep your funnels in a file in your repo and paste it in.

[
  {
    "name": "Visit to signup",
    "window": "24h",
    "range": "last_30_days",
    "split": { "source": "visit", "field": "channel" },
    "steps": ["$pageview", "login_page_viewed", "signup_started", "signup_completed"]
  },
  {
    "name": "Pricing to signup",
    "window": "24h",
    "split": "channel",
    "steps": [
      { "event": "$pageview", "where": { "path": "/pricing" } },
      "login_page_viewed",
      "signup_started",
      "signup_completed"
    ]
  },
  {
    "name": "Signup to paid by channel",
    "window": "14d",
    "split": { "source": "person", "field": "first_channel" },
    "steps": ["signed_up", "generate_succeeded", "checkout_completed"]
  }
]

Fields

FieldMeaning
nameRequired in an array; unique within the file (case-insensitive) and among your saved funnels.
descriptionAny length. Accepted and validated, but not stored yet.
windowConversion window: a number of hours, or "24h" / "3d". 1 to 720 hours.
rangelast_24_hours, last_7_days, last_30_days, last_90_days, or { "from": ..., "to": ... }.
splitSee below.
breakdownTop level: the legacy whole-funnel event-property breakdown, not a split.
stepsRequired. Each step is a bare event name, { "event", "where"?, "optional"?, "label"?, "breakdown"? }, { "feature", ... }, or { "any": [...] } for an OR of several events.
  • where on a step filters that step's event by property: { "path": "/pricing" } means equals; use { "tier": { "neq": "trial" } } for the other operators (eq, neq, contains, gt, lt).
  • breakdown on a step buckets people by an event property at that step, with an optional breakdownLimit (1 to 50, default 10).
  • Unknown keys are errors in this shorthand shape, at every level, and the message names the path. A saved funnel definition exported from Vevee (with a definition wrapper) imports as well.
  • Arrays are checked first. If any entry fails, nothing is imported and the error names the entry, for example [1] ("Checkout").steps[2].breakdown. Entries are then saved one by one; if a save fails midway, earlier entries stay saved and the message says how many.

split

{ "split": "channel" }
{ "split": { "source": "visit", "field": "utm_campaign" } }
{ "split": { "source": "person", "field": "first_channel" } }
{ "split": { "source": "event", "field": "plan" } }
  • A string is shorthand for a visit split: "channel" means { "source": "visit", "field": "channel" }. It must be one of the 12 visit fields.
  • person and event splits take an object; field must match /^[a-z_][a-z0-9_]{0,63}$/i.
  • An invalid split is an error, never silently dropped.

Where to next