Impressions and clicks
The SDK renders inside your page, so it is the only party that knows whether a creative was ever actually on screen. It therefore owns the count: every request tells the server not to count that delivery server-side, and the browser reports what a visitor really saw.
An impression fires once the block has actually been seen
Section titled “An impression fires once the block has actually been seen”Two conditions, both required:
- The element has been at least 50% visible for 300 milliseconds.
- The copy has settled: the final text for this page view is on screen.
Then exactly one impression is sent, per block, per page view. Not per render, not per state change, and not twice under React strict mode.
Waiting for settle means the report describes what the visitor read, including whether adapted copy was on screen, rather than a half-finished guess.
Attach the ref and impressions count themselves
Section titled “Attach the ref and impressions count themselves”Visibility is measured on the element you attach ref to:
const { state, ref } = useIsland({ campaignId, island: "hero", fallback });return <section ref={ref}>…</section>;Hero and Cta do this for you. A custom component that forgets it renders
perfectly and reports zero traffic. Because the SDK has already told the
server it owns the count, nothing else counts it either. There is no warning.
Clicks are reported by Cta
Section titled “Clicks are reported by Cta”Cta sends a click event and then runs your onClick and the navigation, in
that order and without waiting. Build your own button and you send it yourself:
import { track } from "@sparkletree/core";
track({ apiBase, organizationId, campaignId, variantId }, "click", { island: "cta" });What each event carries
Section titled “What each event carries”| Sent | Notes |
|---|---|
| Organization, campaign, variant, surface | Whatever applies to the block. |
| Event type | impression, click, conversion or custom. |
| Per-tab session id | Random, stored in sessionStorage. Not a cross-site identifier. |
| Where the copy came from | Generated for this context, served from cache, or your own fallback. |
| Whether adapted copy was on screen | So what visitors actually saw is measurable rather than assumed. |
| Degraded reason | Present when the server served a fixed version instead of generating one. |
Events are fire-and-forget with keepalive, so an impression recorded as the
visitor navigates away still gets out. A failed request is a lost data point and
never an exception in your render tree.
Send your own conversion events
Section titled “Send your own conversion events”import { track } from "@sparkletree/core";
track( { apiBase, organizationId, campaignId }, "conversion", { plan: "pro", value: 49 },);track never throws and never blocks (the await is optional and changes nothing).
Blocks addressed by surface still report
Section titled “Blocks addressed by surface still report”A block that names a surfaceId instead of a campaignId does not know its
campaign up front. The server names the campaign it served in the final message,
and impressions and clicks are attributed to that.
The gap: a request that fails before the campaign is resolved records nothing, because there is nothing to attribute it to.
The cards are counted by the server
Section titled “The cards are counted by the server”CopyCard and ProductCard do not open a request of their own, so they do not
suppress server-side counting and do not need a ref. They are counted exactly
as any non-SDK consumer of the same content is.
Fragments send no impression beacons
Section titled “Fragments send no impression beacons”Fragments are served from a shared cached read keyed by context, not per visitor, and they fire no impression or click beacons at all. What you see in the dashboard for fragments is mint and serving activity, not visitor-level analytics. Islands are the instrumented surface; if a fragment needs click tracking, it is your own button and your own analytics call.