From afd64de18d3f976094a6ce325012b8796cb6906a Mon Sep 17 00:00:00 2001 From: Everett Date: Sat, 22 Aug 2026 15:24:27 -0700 Subject: [PATCH] Track cross-promotion attribution --- docs/cross-promotion-measurement.md | 34 ++++++++++++++++++ site/analytics.js | 54 +++++++++++++++++++++++++++++ site/index.html | 4 +-- 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 docs/cross-promotion-measurement.md create mode 100644 site/analytics.js diff --git a/docs/cross-promotion-measurement.md b/docs/cross-promotion-measurement.md new file mode 100644 index 0000000..c277a9a --- /dev/null +++ b/docs/cross-promotion-measurement.md @@ -0,0 +1,34 @@ +# Cross-promotion measurement + +ScriptWidget's landing page accepts these attribution parameters: + +- `utm_source` +- `utm_medium` +- `utm_campaign` +- `utm_content` + +The page keeps the values for the current browser session and emits these events through +`window.dataLayer`, `window.plausible` (when present), and the `xnu:analytics` custom event: + +- `landing_view` +- `app_store_click` +- `github_click` +- `tutorial_click` + +The initial Remoboard placement uses: + +```text +utm_source=remoboard +utm_medium=in_app +utm_campaign=cross_promo +utm_content=home_featured_card +``` + +Use `landing_view` as the number of visits from Remoboard and `app_store_click` as the +number of App Store handoffs. Report the click-through rate as App Store handoffs divided +by landing visits. + +To attribute first-time downloads, create an App Store Connect campaign named +`remoboard_cross_promo`, then add the generated `pt`, `ct`, and `mt` parameters to the +App Store URL in `site/index.html`. Apple requires a provider token generated by App Store +Connect; it must not be guessed or created manually. diff --git a/site/analytics.js b/site/analytics.js new file mode 100644 index 0000000..a4e65a4 --- /dev/null +++ b/site/analytics.js @@ -0,0 +1,54 @@ +(function () { + "use strict"; + + var allowedParameters = ["utm_source", "utm_medium", "utm_campaign", "utm_content"]; + var query = new URLSearchParams(window.location.search); + var attribution = {}; + + allowedParameters.forEach(function (name) { + var value = query.get(name); + if (value) { + attribution[name] = value.slice(0, 100); + } + }); + + if (Object.keys(attribution).length > 0) { + try { + window.sessionStorage.setItem("scriptwidget_attribution", JSON.stringify(attribution)); + } catch (_) { + // Attribution remains available for this page view when storage is unavailable. + } + } else { + try { + attribution = JSON.parse(window.sessionStorage.getItem("scriptwidget_attribution")) || {}; + } catch (_) { + attribution = {}; + } + } + + function track(name, properties) { + var eventProperties = Object.assign({}, attribution, properties || {}); + window.dataLayer = window.dataLayer || []; + window.dataLayer.push({ event: name, properties: eventProperties }); + + if (typeof window.plausible === "function") { + window.plausible(name, { props: eventProperties }); + } + + window.dispatchEvent(new CustomEvent("xnu:analytics", { + detail: { name: name, properties: eventProperties } + })); + } + + window.xnuTrack = track; + track("landing_view", { page: "scriptwidget" }); + + document.querySelectorAll("[data-track-event]").forEach(function (link) { + link.addEventListener("click", function () { + track(link.dataset.trackEvent, { + label: link.dataset.trackLabel || "", + destination: link.hostname + }); + }); + }); +}()); diff --git a/site/index.html b/site/index.html index 7bfe9ee..6ef517a 100644 --- a/site/index.html +++ b/site/index.html @@ -1,9 +1,9 @@ ScriptWidget — Native widgets with JavaScript + JSX -
Open source · iOS · iPadOS · macOS

Widgets, written your way.

Write JavaScript and JSX, preview in ScriptWidget Studio, and render native WidgetKit experiences across Apple platforms—without authoring every widget in Swift.

A gallery of widgets created with ScriptWidget
+
Open source · iOS · iPadOS · macOS

Widgets, written your way.

Write JavaScript and JSX, preview in ScriptWidget Studio, and render native WidgetKit experiences across Apple platforms—without authoring every widget in Swift.

A gallery of widgets created with ScriptWidget
JavaScriptCore runtimeNative SwiftUI renderingLive preview on MacCommunity gallery

From an idea to a native widget.

Write in JavaScript + JSX

Compose familiar declarative layouts and use versioned APIs for data, files, networking, storage, device state, and more.

Build in Studio

Edit with diagnostics, inspect runtime output, and preview multiple widget families without repeating an Xcode build loop.

Share real packages

Use versioned manifests, explicit permissions, host allowlists, and the community Gallery to distribute reusable widgets.

JSX in. Native WidgetKit out.

$render(
  <vstack frame="max" padding="16" background="#171923">
    <text font="caption" color="#a9b0c0">Today</text>
    <text font="largeTitle" color="#ffffff">Build something useful.</text>
  </vstack>
);
- +