Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions workers/x-conversions/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# X conversions for the docs

Tracks activation of the **Install AI Skill** button in the main navigation,
including its mobile variant. This is a click on the installation guide, not a
confirmed skill installation. Other links to that guide are excluded.
Tracks two clicks as one X event: the **Install AI Skill** button in the main
navigation (including its mobile variant), and the copy button of the
`npx skills add .../magicblock-dev-skill` command in the AI Dev Skill callout
(`snippets/ai-dev-skill-callout.mdx`). Both are intent signals, not a confirmed
skill installation. Other links to the guide and other code blocks are excluded.

- Worker: `magicblock-docs-conversions`
- Endpoint: `https://tracking.magicblock.app`
Expand All @@ -12,7 +14,7 @@ confirmed skill installation. Other links to that guide are excluded.

## Flow

The root `x-pixel.js` installs the base pixel and a delegated CTA listener. It
The root `x-pixel.js` installs the base pixel and a delegated click listener. It
reads the public event ID from `GET /config`, then reports each click through
the pixel and `POST /events` with the same generated `conversion_id`.
The browser sends its request with `keepalive`, without delaying navigation.
Expand Down
27 changes: 26 additions & 1 deletion workers/x-conversions/test/browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@ const markup = `<script></script><nav aria-label="Main"><ul>
<li class="block lg:hidden"><a id="mobile" href="${destination}"><span>Install AI Skill</span></a></li>
<li id="topbar-cta-button"><a id="desktop" href="${destination}" target="_blank"><span>Install AI Skill</span></a></li>
<li><a id="github" href="https://github.com/magicblock-labs">Github</a></li>
</ul></nav><main><a id="content" href="${destination}">Install AI Skill</a></main>`;
</ul></nav><main><a id="content" href="${destination}">Install AI Skill</a>
<div class="code-block" id="skill-block"><div data-floating-buttons="true"><div class="code-block-copy-button">
<button id="skill-copy" data-testid="copy-code-button" aria-label="Copy the contents from the code block"><svg><path/></svg></button>
</div></div><pre><code><span class="line"><span>npx</span><span> skills</span><span> add</span><span> https://github.com/magicblock-labs/magicblock-dev-skill</span></span>
</code></pre></div>
<div class="code-block" id="other-block"><div data-floating-buttons="true"><div class="code-block-copy-button">
<button id="other-copy" data-testid="copy-code-button" aria-label="Copy the contents from the code block"><svg><path/></svg></button>
</div></div><pre><code><span class="line"><span>cargo</span><span> build</span></span>
</code></pre></div></main>`;

async function browser(options: { blockedConfig?: boolean; blockedStorage?: boolean; url?: string } = {}) {
const dom = new JSDOM(markup, {
Expand Down Expand Up @@ -93,3 +101,20 @@ test("does not send CTA events from local development or preview origins", async
assert.equal(requests.length, 0);
} finally { close(); }
});

test("install command copy button sends the event, other code blocks do not", async () => {
const { window, requests, click, close } = await browser();
try {
await click("#skill-copy path");
await click("#other-copy path");
const sent = requests.filter((r) => r.url.endsWith("/events"));
const pixel = window.twq.queue.filter((args) => args[0] === "event");
assert.equal(sent.length, 1);
assert.equal(pixel.length, 1);
const body = JSON.parse(sent[0].options.body as string);
assert.equal(body.event, "ai_skill_cta_clicked");
assert.equal(body.conversion_id, pixel[0][2].conversion_id);
assert.equal(body.twclid, "ad-click-123");
assert.equal(body.event_source_url, "https://docs.magicblock.xyz/guide");
} finally { close(); }
});
27 changes: 21 additions & 6 deletions x-pixel.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,30 @@ twq('config','p9ilu');
.then((result) => result.ok ? result.json() : null)
.catch(() => null);

// The "Install AI Skill" button in the main navigation (desktop and mobile).
function isNavigationCta(target) {
const link = target.closest("a");
if (!link || !link.closest('nav[aria-label="Main"]')) return false;
const destination = new URL(link.href, window.location.href);
return docsOrigins.includes(destination.origin) &&
destination.pathname.replace(/\/$/, "") === "/pages/overview/additional-information/ai-dev-skill" &&
link.textContent.trim() === "Install AI Skill";
}

// The copy button of the skill install command shown in the AI Dev Skill callout.
const installCommand = "npx skills add https://github.com/magicblock-labs/magicblock-dev-skill";
function isInstallCommandCopy(target) {
if (!target.closest('button[data-testid="copy-code-button"]')) return false;
const block = target.closest(".code-block");
const code = block ? block.querySelector("pre code") : null;
return Boolean(code) && code.textContent.replace(/\s+/g, " ").trim() === installCommand;
}

function trackClick(event) {
if (event.type === "auxclick" && event.button !== 1) return;
if (event.type === "click" && event.button !== 0) return;
const link = event.target instanceof Element ? event.target.closest("a") : null;
if (!link || !link.closest('nav[aria-label="Main"]')) return;
const destination = new URL(link.href, window.location.href);
if (!docsOrigins.includes(destination.origin) ||
destination.pathname.replace(/\/$/, "") !== "/pages/overview/additional-information/ai-dev-skill" ||
link.textContent.trim() !== "Install AI Skill") return;
const target = event.target instanceof Element ? event.target : null;
if (!target || !(isNavigationCta(target) || isInstallCommandCopy(target))) return;

captureClickId();
const conversionId = crypto.randomUUID();
Expand Down
Loading