Skip to content

Piano destination: setConfigurations (site / collectDomain) never runs when loadScript: true #681

@DavidHemmerle

Description

@DavidHemmerle

Package

@walkeros/web-destination-piano (observed in 4.2.1)

Summary

When the Piano destination is configured with loadScript: true, the site and
collectDomain settings are never applied to the Piano Analytics SDK. init
injects the SDK <script> asynchronously and then, in the same synchronous tick,
tries to read window.pa and call pa.setConfigurations(...). Because the script
has not loaded yet, window.pa is undefined, the guard short-circuits, and
setConfigurations is silently skipped.

Events are still delivered later (each push re-resolves window.pa lazily, by
which point the SDK has loaded), but they go out without the configured site
and collectDomain — i.e. to the default/unconfigured collection endpoint. There
is no error or warning, so it fails silently.

Source

In packages/web/destinations/piano/src/index.ts, init is roughly:

init({ config, env }) {
  const settings = config.settings || {};

  if (config.loadScript) addScript();   // appends <script> — loads ASYNCHRONOUSLY

  const pa = resolvePa(env);            // reads window.pa immediately — still undefined
  if (pa && isDefined(settings.site) && isDefined(settings.collectDomain)) {
    pa.setConfigurations({              // never reached on first load
      site: settings.site,
      collectDomain: settings.collectDomain,
      ...(isObject(settings.options) ? settings.options : {}),
    });
  }

  return config;
}

addScript() only creates the element and appends it; there is no onload
handler, so the subsequent resolvePa/setConfigurations race the script load
and lose every time on a cold page.

Steps to reproduce

  1. Configure the destination with loadScript: true and valid site / collectDomain:

    destinations: {
      piano: {
        code: destinationPiano,
        config: {
          loadScript: true,
          settings: {
            site: 123456789,
            collectDomain: "https://xxxxxxx.pa-cd.com",
          },
        },
      },
    }
  2. Load a fresh page (so window.pa is not already present) and trigger any event.

  3. Observe the network requests to Piano.

Expected

pa.setConfigurations({ site, collectDomain }) is called once the SDK is ready,
and collection hits use the configured site and collectDomain.

Actual

setConfigurations is never called. window.pa is undefined at the moment
init checks it. Events are sent later, but without the configured site/domain.

Workaround

Load the SDK yourself, wait for onload, then init the destination with
loadScript: false so it configures an already-present window.pa:

await new Promise<void>((resolve, reject) => {
  if (window.pa) return resolve();
  const s = document.createElement("script");
  s.src = "https://tag.aticdn.net/piano-analytics.js";
  s.async = true;
  s.onload = () => resolve();
  s.onerror = () => reject(new Error("Failed to load Piano SDK"));
  document.head.appendChild(s);
});

// ...then start the collector with config.loadScript = false

Suggested fix

When loadScript injects the SDK, defer configuration until the script's
onload fires (or pa becomes available) instead of calling setConfigurations
synchronously. For example, have addScript accept a callback / return a promise
and run the setConfigurations block from there:

function addScript(src = PIANO_SDK_URL, onReady?: () => void) {
  if (typeof document === "undefined") return;
  const script = document.createElement("script");
  script.src = src;
  script.async = true;
  if (onReady) script.onload = () => onReady();
  document.head.appendChild(script);
}

init({ config, env }) {
  const settings = config.settings || {};

  const configure = () => {
    const pa = resolvePa(env);
    if (pa && isDefined(settings.site) && isDefined(settings.collectDomain)) {
      pa.setConfigurations({
        site: settings.site,
        collectDomain: settings.collectDomain,
        ...(isObject(settings.options) ? settings.options : {}),
      });
    }
  };

  if (config.loadScript) {
    addScript(PIANO_SDK_URL, configure); // configure after the SDK loads
  } else {
    configure();                          // pa already present
  }

  return config;
}

Optionally also guard against missing pa at onload time and warn if site/
collectDomain are absent, so the silent-failure mode is easier to diagnose.

Environment

  • @walkeros/web-destination-piano: 4.2.1
  • @walkeros/collector / @walkeros/web-source-browser: 4.2.1
  • Next.js (client-side init), but the bug is environment-independent — it is a
    synchronous-read-after-async-load race in the destination itself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions