Declared Navigation Source
Definition
This document explains how to tell the Pulse SDK which click causes a navigation, with the pulse-navigate attribute, so the navigation event reports that click as its source even when the navigation happens seconds after the click, or in a new tab.
Why declare a navigation?
Every navigation event carries a click_source that describes the click that caused it. Without a declaration, the SDK guesses that click from recent clicks within short time windows, roughly:
- 500 ms for a navigation inside the same page (a single-page app route change)
- 3.5 seconds for a full page load in the same tab
- 6 seconds for a page opened in a new tab
A button that does some work first, such as an API call or an upload, and only then navigates, misses these windows. Its navigation is sent with an empty click_source, and the click that started it is lost. Declaring the click fixes that.
Usage
Add pulse-navigate to the clicked element, or to any of its parents. Its value is a label that is sent with the click and with the navigation it causes. In React and other frameworks that need the data- prefix, use data-pulse-navigate.
<!-- Standard HTML -->
<button pulse-navigate="checkout cta">Checkout</button>
<!-- React/Framework compatible -->
<button data-pulse-navigate="checkout cta">Checkout</button>
<!-- On a wrapper: every click inside it is declared -->
<div pulse-navigate="template card">
<img src="..." />
<button>Use template</button>
</div>
- The label is optional. Without a value, the navigation is still attributed to the click, but no
navigate_labelis sent. - When both attributes have a value,
pulse-navigatewins. - The label is sent as it is, without a length limit.
What is sent
The label is sent as click_source.navigate_label on two events:
- The click event of the declared click, whether or not a navigation follows. A click event is sent only for buttons, links and
pulse-clickableelements, or for elements inside them: a click on another element of a declared wrapper, such as the image above, still declares the navigation but sends no click event. - The navigation event that the click caused, together with the clicked element, its section, the tab it was clicked in and the URL of the page it was clicked on (
click_source.url).
{
"event": "navigation",
"data": {
"initial": false,
"click_source": {
"element": { "id": "checkout", "type": "BUTTON", "value": "Checkout", "class_list": [] },
"section": "pricing",
"group": "pricing",
"tab_id": "a.c.mvast4rq.1204ebf4-685e-47fe-9891-c2f7263d8b95",
"url": "https://picsart.com/pricing/",
"navigate_label": "checkout cta"
}
}
}
navigate_label has to be defined on the click and navigation events of your app in tools-portal. The event receiver drops fields that the event definition doesn't have, and still answers with HTTP 200.
Rules
- Time window. A declaration is kept for 30 seconds after the click (the
declaredNavigationTimeWindowsetting, at most 10 minutes). A navigation that happens later is not attributed to it. A tab the browser opens has a shorter window, see New tabs. - One declaration, one navigation. The first navigation after the click takes the declaration; the following ones don't. A hash change counts as a navigation.
- The latest click wins. A later click in the tab, declared or not, replaces or drops a pending declaration, so a declared click that didn't navigate is forgotten as soon as the user clicks something else. A right-click or a middle-click doesn't drop it.
- Reloads don't count. A navigation that lands on the URL where the click happened (a reload, or Back to that page) drops the declaration without using it.
- Session renewal. A
navigationsent because the session was renewed (source: "session_reset"or"consent_change") leaves the declaration for the next real navigation. - Same site only. Declarations are kept in the site's localStorage, so only a page on the same origin (scheme, host and port) that runs the Pulse SDK can take one. A navigation to another subdomain or domain gets no label.
- After the SDK loads. Clicks made before the SDK has loaded on the page declare nothing.
- Do not track. A click inside
pulse-dntdeclares nothing. - Modifier keys only change what a link or a submit button opens (see New tabs). On any other element, a Ctrl, Cmd, Shift or Alt click is a normal declared click: your code decides where it goes.
- Nothing to load. An Alt-click on a link or a submit button, a
downloadlink, and amethod="dialog"form declare nothing. - Tab ids are needed. In
anonymousandpage-sessionmode, or with thetab_idenrichment turned off, there are no tab ids, so nothing is declared. The click event still carriesnavigate_label. - Full SDK only. The mini SDK (
@pulse/mini) has no declared navigation.
New tabs
A declared click whose navigation opens in a new tab gives its label to that tab's first navigation event. The tab's click_source.tab_id and its events' opener_tab_id are the id of the tab that was clicked in.
Tabs opened by your code
A tab opened with window.open() from the declared click takes the declaration of the tab that opened it, whether it opens right away, after some delay, or as an about:blank tab whose URL is set later, within the 30 seconds. The tab that was clicked in then no longer holds the declaration, so its own next navigation isn't attributed to that click.
// Both new tabs get navigate_label "open editor" and "export"
<button data-pulse-navigate="open editor" onClick={() => window.open('/create')}>Open</button>
<button
data-pulse-navigate="export"
onClick={async () => {
const tab = window.open('about:blank');
const url = await exportFile();
tab.location.href = url;
}}
>
Export
</button>
When the declared element is a link whose handler also opens another page with window.open(), that new tab takes the declaration only if it lands on the link's own URL. Otherwise the link's own navigation keeps it.
Tabs the browser opens
The browser itself opens a new tab for:
- a link or a form with
target="_blank"(or anytargetother than the current window), - a Ctrl, Cmd or Shift click on a link, and on a submit button in Chromium and Safari,
- a middle-click on a link,
- "Open link in new tab" from a link's context menu.
Such a click is bound to the URL it opens. The new tab that lands on that URL within 6 seconds of the click, from the page the click happened on, gets the label. One click gives its label to one new tab only: another tab opened later on the same URL doesn't get it.
The URL comparison ignores the #fragment and a trailing slash. For a form submitted with GET, only the path is compared, because the browser replaces the query.
Limits
These new tabs get no label, and fall back to the SDK's usual guess:
- A tab opened with
window.open(url, '_blank', 'noopener')or'noreferrer': the tab can't tell which tab opened it. - A tab on another origin, including another subdomain (see Same site only).
- A tab whose server redirects to another URL than the one clicked, other than adding or removing a trailing slash, when its declaration is bound to a URL: a link or a form the browser opens in a new tab, or a declared link handed to a
window.open()tab. - A form submitted with Ctrl or Shift in Firefox, which submits it in the same tab.
When a tab opened by your code can't take the declaration, the tab that was clicked in drops it once the user comes back to it, if it was hidden within a second of the click, as opening a tab in front hides it at once. If your code opens such a tab later than that, or in a separate window, call clearNavigationSource after opening it.
Cancel a declaration
When a declared click ends up not navigating, for example because a form check failed or the user cancelled a dialog, cancel its declaration so that the next navigation of the tab isn't attributed to it:
// From the global pulse object
pulse.clearNavigationSource('checkout cta'); // cancels it only if the label matches
pulse.clearNavigationSource(); // cancels any pending declaration
// As a command
pulse('clearNavigationSource', 'checkout cta');
pulse.cmd('clearNavigationSource', 'checkout cta');
// Through the dataLayer
window.dataLayer.push(['paa.clearNavigationSource', 'checkout cta']);
// Through the tracker, once the SDK has loaded
pulse.tracker.clearNavigationSource('checkout cta');
- With a label, only a declaration with that label is cancelled. Without one, or with a value that isn't a string, any pending declaration of the tab is.
- It also cancels the page's declared clicks that open a new tab, as long as the new tab hasn't taken them yet.
- Once the SDK is loaded, the call takes effect at once, so
pulse.clearNavigationSource(); history.pushState(...)is not attributed. Before the SDK is loaded, it runs after the calls made before it, in order. - The dataLayer command uses the
paa.prefix, like the other tracker commands pushed to the dataLayer. See also the Tracker API reference. pulse.trackerexists only once the SDK has loaded. Calling it earlier throws.
window.pulse can be created by an older version of @pulse/browser or @pulse/core from your bundle, which has no clearNavigationSource method. Until you upgrade it, use the dataLayer command, which works before and after the SDK loads, or pulse('clearNavigationSource', label), which takes effect a moment later.
Storage
The SDK keeps a tab's pending declaration in localStorage under paa_tab_declared_navigation_{tab_id}, and the declared clicks that open a new tab in the tab's paa_tab_activity_{tab_id} entries, with the URL they open. That URL is never sent; the URL of the page where the click happened is sent as click_source.url. paa_tab_new_tab_taken_{tab_id} remembers which of those clicks a new tab already took. A new tab keeps the label it took in its sessionStorage, under paa_tab_opener. See LocalStorage and SessionStorage.