1. Introduction
Piano's JavaScript library (tinypass.min.js) loads asynchronously, and most modern publishers also run a Consent Management Platform (CMP) that gates, delays, or conditionally allows Piano scripts to execute. When Identity Management, Piano Analytics, and Composer initialize in the wrong order, the downstream effects are often subtle: an experience evaluates against an anonymous user, an analytics event misses the user_id, or a custom variable arrives a few milliseconds too late.
This guide describes the dependencies between these components, the patterns that typically break them, and a recommended order that resolves the most common issues. For broader performance recommendations, see How to Optimize Loading and Executions of Piano Scripts.
2. The Core Problem
Piano's SDKs depend on each other. Identity Management must provide a user state before Composer evaluates user-segment targeting; Composer must have access to your custom variables before it runs targeting logic; Piano Analytics needs the user context from Identity Management before sending the first significant event. Any of these dependencies can be broken by asynchronous loading, CMP gating, or SPA route changes.
2.1 Typical Symptoms
-
Composer experiences not firing, or firing inconsistently.
-
Logged-in subscribers shown a paywall because Identity Management state was not yet known.
-
Analytics events missing user, session, or content data.
-
Custom variables undefined when an experience evaluates.
-
Duplicate or missing pageviews after route changes.
2.2 Typical Order Dependencies
|
# |
Step |
Why It Matters |
|---|---|---|
|
1 |
CMP signals consent state. |
Determines whether Piano can load, set cookies, and fire calls. |
|
2 |
|
Must exist before |
|
3 |
Identity Management initializes and resolves the user. |
Composer and Analytics both consume the resulting user state. |
|
4 |
Custom variables / content metadata pushed. |
Must precede |
|
5 |
Analytics user context set. |
Without it, the first events are anonymous and split user sessions. |
For background on the tp command pattern and how the queue works, see Front-End Basics.
3. Common Breakage Patterns
3.1 Composer Runs Before Identity Management Resolves
If tp.push(["init", () => tp.experience.execute()]) fires before the Identity Management token check completes, the user appears anonymous and gated experiences show paywalls to subscribers.
Symptom: Logged-in subscribers see paywalls; the issue often resolves on a second pageview because Identity Management has hydrated by then.
Fix: Use the loggedIn or loginRequired callbacks defined on tp.pianoId.init(), or defer tp.experience.execute() until after tp.user.isUserValid() can be evaluated reliably. See Identity Management JavaScript Settings for the full list of callbacks (loggedIn, loggedOut, loginSuccess, registrationSuccess, etc.).
tp = window.tp || [];
tp.push(["setUsePianoIdUserProvider", true]);
tp.push(["init", function () {
tp.pianoId.init({
loggedIn: function (data) {
// Safe to evaluate experiences here — user state is known
tp.experience.execute();
},
loggedOut: function () {
tp.experience.execute();
}
});
}]);
tp.experience.init() can only be called once per page; for any subsequent re-evaluation (after login, route change, etc.) use tp.experience.execute().
3.2 CMP Blocks tinypass.min.js but Lets Queue Commands Run
Items pushed to tp.push() accumulate in the queue but never execute because the script is blocked by the CMP. When consent is later granted and tinypass.min.js loads, it processes the queue — but timing-sensitive items (such as setCustomVariable before experience.execute) may now be out of order, or duplicate executions can occur if both your code and the integration script call experience.init().
Fix:
-
Push commands only after the CMP grants consent.
-
Ensure every
tp.push()setup command precedesexperience.execute()in the queue. -
Consider caching consent state in a first-party cookie so subsequent page loads do not wait on a remote CMP request. Important: only do this if it aligns with your compliance requirements and your CMP's policies (see How to Optimize Loading and Executions of Piano Scripts).
For consent integration with Piano Analytics, see the Consent management documentation.
For general information about Consent management, see the documentation here.
3.3 Analytics Initialized Before User Identification
Piano Analytics sends events with an anonymous visitor ID if they fire before Identity Management has provided the user data. Subsequent events carry the proper user_id, which causes a single visit to split into two sessions in reporting.
Fix: Call pa.setUser() from the Identity Management loggedIn callback before sending the first significant event, or delay the initial pa.sendEvent('page.display') until Identity Management resolves. The JavaScript SDK exposes a _paq queue that lets you order these calls safely.
let _paq = window._paq || [];
// Identify the user first, then send the first event
_paq.push(["setUser", "WEB-192203AJ", "premium", true]);
_paq.push(["sendEvent", "page.display", { page: "homepage" }]);
For the standard page.display event schema and properties, see the page.display reference.
3.4 Cookie Consent Revoked Mid-Session
If consent changes mid-session and your code reinitializes SDKs without clearing state, you can end up with stale Composer experience results, duplicate analytics initialization, or experiences that continue to evaluate against the previous user context.
Fix:
-
Listen for consent-change events from your CMP and reload the page or clear Piano state explicitly when consent is revoked.
-
Avoid calling
tp.experience.init()more than once per page — usetp.experience.execute()for re-evaluation (see tp.experience.init() vs tp.experience.execute()).
3.5 Race Condition with SPA Route Changes
On Single Page Applications, calling tp.experience.execute() on route change before re-pushing custom variables means experiences evaluate against stale data from the previous route.
Fix:
-
On every route change, reset the relevant
tpattributes (setTags,setContentSection,setCustomVariable, etc.). -
Then call
tp.experience.execute(). -
For analytics, call
pa.refresh()to regenerate the pageview ID before sending the nextpage.display. The JavaScript SDK also supportsenableAutomaticPageRefresh: truefor SDK version 6.12.0+.
The Composer integration script is not auto-loaded for SPA implementations — see Composer Integration Script for details.
4. Recommended Initialization Order
The following sequence resolves the most common issues described above. Adapt it to your CMP and tag manager, but preserve the dependencies.
-
CMP loads and determines consent state.
-
(If consent granted) Initialize the queue:
tp = window.tp || []; -
Push base configuration:
setAid, endpoint,setUsePianoIdUserProvider, custom variables, tags, content metadata. -
Push Identity Management init with
loggedInandloggedOuthandlers. -
Load
tinypass.min.js(placed in for best performance — see optimization guide). -
Inside the
loggedInhandler (or after the token check resolves):-
Call
pa.setUser()to set the analytics user context. -
Push
tp.experience.execute().
-
-
Send the first analytics page event (
pa.sendEvent('page.display', { ... })).
<!-- STEP 1: Load tinypass.min.js early -->
<script async src="https://cdn.tinypass.com/api/tinypass.min.js"></script>
<!-- STEP 2: Initialize settings BEFORE execution -->
<script>
tp = window.tp || [];
tp.push(["setAid", "<AID>"]);
tp.push(["setUsePianoIdUserProvider", true]);
tp.push(["setCustomVariable", "userState", "Subscriber"]);
tp.push(["setTags", ["homepage", "premium"]]);
</script>
<!-- STEP 3: Initialize Piano ID, then execute Composer + Analytics -->
<script>
let _paq = window._paq || [];
tp.push(["init", function () {
tp.pianoId.init({
loggedIn: function (data) {
_paq.push(["setUser", data.user.uid, "premium", true]);
_paq.push(["sendEvent", "page.display", { page: "homepage" }]);
tp.experience.execute();
},
loggedOut: function () {
_paq.push(["sendEvent", "page.display", { page: "homepage" }]);
tp.experience.execute();
}
});
}]);
</script>
5. Debugging Tips
Use these tools when an integration behaves unexpectedly. A more complete reference is available in Experience Debugging with Dev Tools and How Do I Debug When a Composer Experience Is Not Executing?.
|
Technique |
What It Tells You |
|---|---|
|
|
Confirms which user provider is active when execute fires. |
|
|
Returns |
|
|
Lets you inspect the queued commands. Look for missing or out-of-order calls. |
|
Network panel ordering |
Should be: CMP response → |
|
Append |
Surfaces experience evaluation details: which experience matched, which card failed, and why. |
|
|
Increases verbosity of Piano debug messages in the browser console. |
|
Watch for duplicate |
Indicates that both your code and the auto-init are firing — use |
|
|
Confirms whether custom variables were set before Composer evaluated. |
The ?xpdebug URL parameter surfaces verbose evaluation logs; the alternative ?piano_debug=true opens a structured debug panel showing which experience matched and why each card passed or failed.