We’ve migrated our documentation to a new site, which means some URLs have changed.
Subscriptions

Piano Multi Product Script Initialization Guide

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

tp.push() queue is set up.

Must exist before tinypass.min.js loads or any push will be lost.

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 tp.experience.execute() or Composer evaluates against stale data.

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 precedes experience.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.

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 — use tp.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:

  1. On every route change, reset the relevant tp attributes (setTags, setContentSection, setCustomVariable, etc.).

  2. Then call tp.experience.execute().

  3. For analytics, call pa.refresh() to regenerate the pageview ID before sending the next page.display. The JavaScript SDK also supports enableAutomaticPageRefresh: true for SDK version 6.12.0+.

The Composer integration script is not auto-loaded for SPA implementations — see Composer Integration Script for details.

The following sequence resolves the most common issues described above. Adapt it to your CMP and tag manager, but preserve the dependencies.

  1. CMP loads and determines consent state.

  2. (If consent granted) Initialize the queue: tp = window.tp || [];

  3. Push base configuration: setAid, endpoint, setUsePianoIdUserProvider, custom variables, tags, content metadata.

  4. Push Identity Management init with loggedIn and loggedOut handlers.

  5. Load tinypass.min.js (placed in for best performance — see optimization guide).

  6. Inside the loggedIn handler (or after the token check resolves):

    1. Call pa.setUser() to set the analytics user context.

    2. Push tp.experience.execute().

  7. 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

tp.push(["init", () => console.log(tp.user.getProvider())])

Confirms which user provider is active when execute fires.

tp.pianoId.isUserValid() in the console

Returns true if Identity Management currently recognizes the user as logged in.

window.tp before tinypass.min.js loads

Lets you inspect the queued commands. Look for missing or out-of-order calls.

Network panel ordering

Should be: CMP response → tinypass.min.js/xbuilder/experience/execute → Piano Analytics events.

Append ?xpdebug or ?piano_debug=true to the URL

Surfaces experience evaluation details: which experience matched, which card failed, and why.

tp.push(["setDebug", true]);

Increases verbosity of Piano debug messages in the browser console.

Watch for duplicate tp.experience.init calls

Indicates that both your code and the auto-init are firing — use execute() for re-runs.

tp.customVariables in the console

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.

Last updated: