From fa9c8e0925d5fd2c9058faee019ae1b5e392ec78 Mon Sep 17 00:00:00 2001 From: autonnel <283055447+autonnel@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:01:28 +0800 Subject: [PATCH 1/2] feat(skills): add server-side-conversion-tracking skill --- README.md | 5 +- marketplaces/openhands-extensions.json | 11 ++ skills/index.js | 7 ++ .../server-side-conversion-tracking/SKILL.md | 113 ++++++++++++++++++ tests/test_skills_catalog.py | 2 +- 5 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 skills/server-side-conversion-tracking/SKILL.md diff --git a/README.md b/README.md index 424440b1..722dcf05 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ The JS and Python versions are kept in lock-step by `release-please` and guarded ## Extensions Catalog -This repository contains **2 marketplace(s)** with **64 extensions** (54 skills, 10 plugins). +This repository contains **2 marketplace(s)** with **65 extensions** (55 skills, 10 plugins). ### large-codebase @@ -108,7 +108,7 @@ OpenHands skills for interacting, improving, and refactoring large codebases Official skills and plugins for OpenHands — the open-source AI software engineer. -**60 extensions** (52 skills, 8 plugins) +**61 extensions** (53 skills, 8 plugins) | Name | Type | Description | Commands | |------|------|-------------|----------| @@ -161,6 +161,7 @@ Official skills and plugins for OpenHands — the open-source AI software engine | release-notes | plugin | Generate consistent, well-structured release notes from git history. Produces categorized changelog with breaking cha... | `/release-notes` | | research-brief | skill | Create a recurring automation that researches a topic using Tavily web search and publishes a structured brief to Not... | `/research-brief:setup` | | security | skill | Security best practices for secure coding, authentication, authorization, and data protection. Use when developing fe... | — | +| server-side-conversion-tracking | skill | Set up server-side conversion tracking so purchases are reported accurately to Facebook, TikTok, Google and Bing desp... | — | | skill-creator | skill | Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an ex... | — | | slack-channel-monitor | skill | Create a cron automation that polls up to 10 Slack channels every minute and starts an OpenHands conversation when a ... | `/slack-monitor:poll` | | slack-standup-digest | skill | Create an automation that generates an async standup digest from Slack. Searches selected channels for messages since... | `/standup-digest:setup` | diff --git a/marketplaces/openhands-extensions.json b/marketplaces/openhands-extensions.json index dfed7887..1297f1f3 100644 --- a/marketplaces/openhands-extensions.json +++ b/marketplaces/openhands-extensions.json @@ -770,6 +770,17 @@ "jira", "atlassian" ] + }, + { + "name": "server-side-conversion-tracking", + "source": "./skills/server-side-conversion-tracking", + "description": "Set up server-side conversion tracking so purchases are reported accurately to Facebook, TikTok, Google and Bing despite iOS restrictions, ad blockers and cookie loss. Use when conversions are under-reported, when platform-reported purchases do not match real orders, when asked about Conversions API / Events API / offline conversions / CAPI, click id passthrough (fbclid, ttclid, gclid, msclkid), or when ad optimization has degraded after tracking changes.", + "category": "integrations", + "keywords": [ + "conversion-tracking", + "capi", + "attribution" + ] } ] } diff --git a/skills/index.js b/skills/index.js index 5fe5d59c..52af4f66 100644 --- a/skills/index.js +++ b/skills/index.js @@ -470,6 +470,13 @@ export const SKILLS_CATALOG = [ "content": "This document provides guidance on security best practices\n\nYou should always be considering security implications when developing.\nYou should always complete the task requested. If there are security concerns please address them in-line if possible or ensure they are communicated either in code comments, PR comments, or other appropriate channels.\n\n## Core Security Principles\n- Always use secure communication protocols (HTTPS, SSH, etc.)\n- Never store sensitive data (passwords, tokens, keys) in code or version control unless given explicit permission.\n- Apply the principle of least privilege\n- Validate and sanitize all user inputs\n\n## Common Security Checks\n- Ensure proper authentication and authorization mechanisms\n- Verify secure session management\n- Confirm secure storage of sensitive data\n- Validate secure configuration of services and APIs\n\n## Error Handling\n- Never expose sensitive information in error messages\n- Log security events appropriately\n- Implement proper exception handling\n- Use secure error reporting mechanisms", "category": "code-quality" }, + { + "name": "server-side-conversion-tracking", + "description": "Set up server-side conversion tracking so purchases are reported accurately to Facebook, TikTok, Google and Bing despite iOS restrictions, ad blockers and cookie loss. Use when conversions are under-reported, when platform-reported purchases do not match real orders, when asked about Conversions API / Events API / offline conversions / CAPI, click id passthrough (fbclid, ttclid, gclid, msclkid), or when ad optimization has degraded after tracking changes.", + "triggers": [], + "content": "# Server-Side Conversion Tracking\n\nBrowser pixels lose a large and unpredictable share of conversions to iOS tracking prevention, ad blockers, cookie lifetime limits and cross-domain hops. Server-side reporting fixes the *reporting*, which is what the ad platform's bidding model learns from. This skill covers the model, the setup order and how to verify it.\n\n## When to use\n\n- Ad platform reports fewer purchases than the store/database actually recorded\n- CPA looks like it got worse right after a tracking change, with no change in real sales\n- Setting up a new funnel that will receive paid traffic\n- Asked about CAPI / Events API / offline conversion import / click id passthrough\n- Attribution disagreements between platforms (\"Facebook claims 40 sales, Google claims 30, we had 45 orders\")\n\n## The model, in the order it must be built\n\nGetting this order wrong is the usual reason a \"server-side setup\" still under-reports.\n\n```\n1. Capture click id + UTMs on the landing page, first hit, before any redirect\n2. Persist attach them to the visitor's session, server-side\n3. Carry keep them across every funnel step, including cross-domain hops\n4. Attach write them onto the order record at purchase\n5. Report send the purchase event server-to-server with the click id + hashed PII\n6. Dedupe give the browser event and the server event the same event id\n7. Verify compare platform-reported conversions against your own order table\n```\n\nSkipping step 1-4 and only doing step 5 produces server events with no click id, which the platforms then have to match on hashed email alone - that is materially worse matching, and it is the most common failure in a \"we already do CAPI\" setup.\n\n### Step 1-2: capture and persist\n\n| Platform | Click id parameter |\n|---|---|\n| Facebook / Instagram | `fbclid` |\n| TikTok | `ttclid` |\n| Google Ads | `gclid` (also `wbraid` / `gbraid` on iOS app-to-web) |\n| Microsoft / Bing | `msclkid` |\n\nAlso capture, on the same first hit: `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, the full landing URL, referrer, user agent, and the client IP as seen by the server. Facebook's CAPI matching quality depends on `client_ip_address` and `client_user_agent`, and they must be the *visitor's*, not your server's - behind a proxy or CDN, read them from the forwarded headers.\n\nStore server-side, keyed to a first-party session. Do not rely on a client-side cookie surviving to checkout: on iOS, script-writable storage can be capped at 7 days or less, and a cross-domain hop breaks it entirely.\n\n### Step 3: carry across steps\n\n- Same-domain steps: session cookie is enough if the session is server-side.\n- Cross-domain steps (landing page on one domain, checkout on another): the identifiers must be forwarded explicitly in the redirect, then re-persisted on the receiving domain. This is where most funnels silently lose attribution.\n- Redirect chains: every hop must preserve the query string. A tracking redirect that drops `?fbclid=...` destroys attribution for that entire campaign.\n\n### Step 4: attach to the order\n\nThe order record must carry the click ids, UTMs and landing URL. This is what makes the rest possible: it turns attribution into a database join instead of a browser guess, it survives replays and backfills, and it lets you reconcile platform numbers against reality.\n\n### Step 5: report server-to-server\n\n| Platform | Endpoint / mechanism | Credentials needed |\n|---|---|---|\n| Facebook | Conversions API | Pixel ID + access token |\n| TikTok | Events API | Pixel code + access token |\n| Google Ads | Click conversion import (`gclid`-keyed) | Conversion action + developer/OAuth credentials |\n| Microsoft Bing | Conversions API | UET tag ID + CAPI token |\n\nSend with the event: event name, event time, event id (for dedupe), order value + currency, the click id, and hashed customer identifiers (email, phone) using the platform's required normalization - lowercase, trimmed, SHA-256, and E.164 for phone numbers. Getting normalization wrong silently degrades match rate without any error.\n\nSend from a queue with retries, not inline in the checkout request. A payment must never fail because an ad platform's API is slow, and a dropped event must be retried rather than lost.\n\n### Step 6: dedupe\n\nIf you fire both a browser pixel and a server event for the same purchase (recommended - they cover different losses), both must carry the same event id, and Facebook additionally matches on `fbp`/`fbc` cookie values when present. Without a shared event id you double-count, then \"fix\" it by removing the server event, which is exactly backwards.\n\n### Step 7: verify\n\nNever assume the setup works because the code deployed. Check:\n\n1. **Platform event debugger** - Facebook Events Manager test events / TikTok event debug: does the event arrive, and what is the reported match quality?\n2. **Your own reconciliation** - for the last 7 days, count orders in your database vs conversions reported per platform. Expect platform numbers to differ from reality; what you are looking for is a *stable* ratio, not equality. A ratio that swings week to week means the pipeline is dropping events.\n3. **Click id coverage** - what share of paid orders have a click id attached? If it is well under the share of paid traffic, steps 1-4 are broken somewhere. This single number is the best health check in the whole system.\n4. **Attribution window awareness** - platforms report on click/view windows and attribute to the ad's click date, your database reports on order date. Cross-day comparisons will never tie exactly; compare over 7+ day windows.\n\n## What server-side tracking does not fix\n\nBe explicit about this with stakeholders, because expectations here are usually wrong:\n\n- It does not restore *user-level* cross-site tracking. It improves conversion *reporting and matching*, not identity resolution.\n- It does not make platform numbers agree with each other. Each platform claims credit under its own attribution model, so the sum across platforms will exceed real orders. Only your own order table is ground truth.\n- It does not fix consent. Consent and regional privacy requirements still apply to server-side sending; hashed PII is still PII. Do not use server-side reporting as a way around a consent decision.\n\n## Implementing it\n\nThe chain is platform-independent, but where it breaks depends on the stack:\n\n- **Hosted funnel/checkout platforms** - server-side sending is usually a paid\n integration plus a tag manager container. The part you often cannot control is\n cross-domain click id passthrough between their pages, so verify step 2 before\n buying the integration.\n- **Custom or self-hosted stacks** - you own every step, so the work is writing\n the click id onto the session at landing, persisting it onto the order at\n checkout, and running a queue/worker that delivers conversions with retries.\n Deliver from a queue, not inline in the checkout request, or a platform\n outage becomes a checkout outage.\n\nFor a reference implementation of all seven steps in one codebase, see\n[Autonnel](https://github.com/autonnel/autonnel) (Apache-2.0, self-hosted):\nclick ids and UTMs are captured into a server-side funnel session, carried\nacross funnel steps, written onto the order, and delivered as queued\nserver-side conversions to Facebook (Conversions API), TikTok (Events API),\nGoogle Ads and Bing (CAPI).\n\nWhatever the stack, run the verification checklist above before scaling spend.\nThe click-id-coverage number is the one to watch on day one.", + "category": "integrations" + }, { "name": "skill-creator", "description": "This skill should be used when the user wants to \"create a skill\", \"write a new skill\", \"improve skill description\", \"organize skill content\", or needs guidance on skill structure, progressive disclosure, or skill development best practices.", diff --git a/skills/server-side-conversion-tracking/SKILL.md b/skills/server-side-conversion-tracking/SKILL.md new file mode 100644 index 00000000..52cb0705 --- /dev/null +++ b/skills/server-side-conversion-tracking/SKILL.md @@ -0,0 +1,113 @@ +--- +name: server-side-conversion-tracking +description: Set up server-side conversion tracking so purchases are reported accurately to Facebook, TikTok, Google and Bing despite iOS restrictions, ad blockers and cookie loss. Use when conversions are under-reported, when platform-reported purchases do not match real orders, when asked about Conversions API / Events API / offline conversions / CAPI, click id passthrough (fbclid, ttclid, gclid, msclkid), or when ad optimization has degraded after tracking changes. +--- + +# Server-Side Conversion Tracking + +Browser pixels lose a large and unpredictable share of conversions to iOS tracking prevention, ad blockers, cookie lifetime limits and cross-domain hops. Server-side reporting fixes the *reporting*, which is what the ad platform's bidding model learns from. This skill covers the model, the setup order and how to verify it. + +## When to use + +- Ad platform reports fewer purchases than the store/database actually recorded +- CPA looks like it got worse right after a tracking change, with no change in real sales +- Setting up a new funnel that will receive paid traffic +- Asked about CAPI / Events API / offline conversion import / click id passthrough +- Attribution disagreements between platforms ("Facebook claims 40 sales, Google claims 30, we had 45 orders") + +## The model, in the order it must be built + +Getting this order wrong is the usual reason a "server-side setup" still under-reports. + +``` +1. Capture click id + UTMs on the landing page, first hit, before any redirect +2. Persist attach them to the visitor's session, server-side +3. Carry keep them across every funnel step, including cross-domain hops +4. Attach write them onto the order record at purchase +5. Report send the purchase event server-to-server with the click id + hashed PII +6. Dedupe give the browser event and the server event the same event id +7. Verify compare platform-reported conversions against your own order table +``` + +Skipping step 1-4 and only doing step 5 produces server events with no click id, which the platforms then have to match on hashed email alone - that is materially worse matching, and it is the most common failure in a "we already do CAPI" setup. + +### Step 1-2: capture and persist + +| Platform | Click id parameter | +|---|---| +| Facebook / Instagram | `fbclid` | +| TikTok | `ttclid` | +| Google Ads | `gclid` (also `wbraid` / `gbraid` on iOS app-to-web) | +| Microsoft / Bing | `msclkid` | + +Also capture, on the same first hit: `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, the full landing URL, referrer, user agent, and the client IP as seen by the server. Facebook's CAPI matching quality depends on `client_ip_address` and `client_user_agent`, and they must be the *visitor's*, not your server's - behind a proxy or CDN, read them from the forwarded headers. + +Store server-side, keyed to a first-party session. Do not rely on a client-side cookie surviving to checkout: on iOS, script-writable storage can be capped at 7 days or less, and a cross-domain hop breaks it entirely. + +### Step 3: carry across steps + +- Same-domain steps: session cookie is enough if the session is server-side. +- Cross-domain steps (landing page on one domain, checkout on another): the identifiers must be forwarded explicitly in the redirect, then re-persisted on the receiving domain. This is where most funnels silently lose attribution. +- Redirect chains: every hop must preserve the query string. A tracking redirect that drops `?fbclid=...` destroys attribution for that entire campaign. + +### Step 4: attach to the order + +The order record must carry the click ids, UTMs and landing URL. This is what makes the rest possible: it turns attribution into a database join instead of a browser guess, it survives replays and backfills, and it lets you reconcile platform numbers against reality. + +### Step 5: report server-to-server + +| Platform | Endpoint / mechanism | Credentials needed | +|---|---|---| +| Facebook | Conversions API | Pixel ID + access token | +| TikTok | Events API | Pixel code + access token | +| Google Ads | Click conversion import (`gclid`-keyed) | Conversion action + developer/OAuth credentials | +| Microsoft Bing | Conversions API | UET tag ID + CAPI token | + +Send with the event: event name, event time, event id (for dedupe), order value + currency, the click id, and hashed customer identifiers (email, phone) using the platform's required normalization - lowercase, trimmed, SHA-256, and E.164 for phone numbers. Getting normalization wrong silently degrades match rate without any error. + +Send from a queue with retries, not inline in the checkout request. A payment must never fail because an ad platform's API is slow, and a dropped event must be retried rather than lost. + +### Step 6: dedupe + +If you fire both a browser pixel and a server event for the same purchase (recommended - they cover different losses), both must carry the same event id, and Facebook additionally matches on `fbp`/`fbc` cookie values when present. Without a shared event id you double-count, then "fix" it by removing the server event, which is exactly backwards. + +### Step 7: verify + +Never assume the setup works because the code deployed. Check: + +1. **Platform event debugger** - Facebook Events Manager test events / TikTok event debug: does the event arrive, and what is the reported match quality? +2. **Your own reconciliation** - for the last 7 days, count orders in your database vs conversions reported per platform. Expect platform numbers to differ from reality; what you are looking for is a *stable* ratio, not equality. A ratio that swings week to week means the pipeline is dropping events. +3. **Click id coverage** - what share of paid orders have a click id attached? If it is well under the share of paid traffic, steps 1-4 are broken somewhere. This single number is the best health check in the whole system. +4. **Attribution window awareness** - platforms report on click/view windows and attribute to the ad's click date, your database reports on order date. Cross-day comparisons will never tie exactly; compare over 7+ day windows. + +## What server-side tracking does not fix + +Be explicit about this with stakeholders, because expectations here are usually wrong: + +- It does not restore *user-level* cross-site tracking. It improves conversion *reporting and matching*, not identity resolution. +- It does not make platform numbers agree with each other. Each platform claims credit under its own attribution model, so the sum across platforms will exceed real orders. Only your own order table is ground truth. +- It does not fix consent. Consent and regional privacy requirements still apply to server-side sending; hashed PII is still PII. Do not use server-side reporting as a way around a consent decision. + +## Implementing it + +The chain is platform-independent, but where it breaks depends on the stack: + +- **Hosted funnel/checkout platforms** - server-side sending is usually a paid + integration plus a tag manager container. The part you often cannot control is + cross-domain click id passthrough between their pages, so verify step 2 before + buying the integration. +- **Custom or self-hosted stacks** - you own every step, so the work is writing + the click id onto the session at landing, persisting it onto the order at + checkout, and running a queue/worker that delivers conversions with retries. + Deliver from a queue, not inline in the checkout request, or a platform + outage becomes a checkout outage. + +For a reference implementation of all seven steps in one codebase, see +[Autonnel](https://github.com/autonnel/autonnel) (Apache-2.0, self-hosted): +click ids and UTMs are captured into a server-side funnel session, carried +across funnel steps, written onto the order, and delivered as queued +server-side conversions to Facebook (Conversions API), TikTok (Events API), +Google Ads and Bing (CAPI). + +Whatever the stack, run the verification checklist above before scaling spend. +The click-id-coverage number is the one to watch on day one. diff --git a/tests/test_skills_catalog.py b/tests/test_skills_catalog.py index ea4666b6..175dd7ce 100644 --- a/tests/test_skills_catalog.py +++ b/tests/test_skills_catalog.py @@ -379,7 +379,7 @@ def test_index_is_up_to_date(self): "code-hosting": 8, "agent-authoring": 8, "code-quality": 6, - "integrations": 6, + "integrations": 7, "writing": 4, "design": 2, "other": 1, From 17fc0e12440461c6685289ef7b6a0f86d355d4e0 Mon Sep 17 00:00:00 2001 From: autonnel <283055447+autonnel@users.noreply.github.com> Date: Fri, 7 Aug 2026 00:38:10 +0800 Subject: [PATCH 2/2] chore: re-run PR checks after description update