diff --git a/.agents/plugins/api_marketplace.json b/.agents/plugins/api_marketplace.json index 19120ef54..2be69ce29 100644 --- a/.agents/plugins/api_marketplace.json +++ b/.agents/plugins/api_marketplace.json @@ -318,6 +318,21 @@ "authentication": "ON_INSTALL" }, "category": "Developer Tools" + }, + { + "name": "openai-ads-conversions", + "source": { + "source": "local", + "path": "./plugins/openai-ads-conversions" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL", + "products": [ + "CODEX" + ] + }, + "category": "Developer Tools" } ] } diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 71eba6b3f..149de5276 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -2118,6 +2118,18 @@ "authentication": "ON_INSTALL" }, "category": "Finance" + }, + { + "name": "openai-ads-conversions", + "source": { + "source": "local", + "path": "./plugins/openai-ads-conversions" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Developer Tools" } ] } diff --git a/plugins/openai-ads-conversions/.codex-plugin/plugin.json b/plugins/openai-ads-conversions/.codex-plugin/plugin.json new file mode 100644 index 000000000..ed481d4c6 --- /dev/null +++ b/plugins/openai-ads-conversions/.codex-plugin/plugin.json @@ -0,0 +1,40 @@ +{ + "name": "openai-ads-conversions", + "version": "0.1.0", + "description": "Set up OpenAI Ads Measurement Pixel and optional Conversions API instrumentation.", + "author": { + "name": "OpenAI", + "email": "support@openai.com", + "url": "https://openai.com/" + }, + "homepage": "https://developers.openai.com/ads/", + "repository": "https://github.com/openai/plugins/tree/main/plugins/openai-ads-conversions", + "license": "Proprietary", + "keywords": [ + "openai-ads", + "ads", + "conversions", + "measurement-pixel", + "capi" + ], + "skills": "./skills/", + "interface": { + "displayName": "OpenAI Ads Conversions", + "shortDescription": "Set up OpenAI Ads Pixel and CAPI tracking", + "longDescription": "Use OpenAI Ads Conversions to guide Codex through adding or extending Measurement Pixel and optional Conversions API instrumentation in a repository, with local verification helpers and guidance for safe secret handling, deduplication, attribution context, and setup reporting.", + "developerName": "OpenAI", + "category": "Developer Tools", + "capabilities": [ + "Interactive", + "Read", + "Write" + ], + "websiteURL": "https://developers.openai.com/ads/", + "privacyPolicyURL": "https://openai.com/policies/privacy-policy/", + "termsOfServiceURL": "https://openai.com/policies/row-terms-of-use/", + "defaultPrompt": [ + "Set up OpenAI Ads conversions tracking in this repository." + ], + "screenshots": [] + } +} diff --git a/plugins/openai-ads-conversions/README.md b/plugins/openai-ads-conversions/README.md new file mode 100644 index 000000000..09c7dd004 --- /dev/null +++ b/plugins/openai-ads-conversions/README.md @@ -0,0 +1,32 @@ +# OpenAI Ads Conversions + +OpenAI Ads Conversions helps Codex instrument repositories with OpenAI Ads +Measurement Pixel and optional Conversions API (CAPI) tracking. The plugin +packages a reusable skill with public OpenAI Ads documentation references and +local verification helpers. + +## Codex Usage + +Install `OpenAI Ads Conversions` from the Codex plugin marketplace, start a new +Codex thread in the target repository, and ask Codex to set up OpenAI Ads +conversions tracking. + +## Portable Usage + +The underlying setup guidance is plain Markdown plus helper scripts. If you +cannot use Codex plugins, use your preferred tool's skill or instruction setup +process to load the files under `skills/openai-ads-conversions-setup/`: + +- Provide `SKILL.md` as the agent instructions. +- Keep `references/` available for setup details and reporting expectations. +- Run the helper scripts in `scripts/` locally when validating a generated + integration. + +Other tools may not support Codex skill-loading semantics directly, so follow +that tool's setup instructions and treat this portable path as best effort. + +## Deployment Review + +Generated instrumentation should be reviewed before deployment. Confirm the +implementation satisfies the advertiser's privacy, security, consent, and data +handling requirements, and never commit CAPI secrets or API keys. diff --git a/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/SKILL.md b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/SKILL.md new file mode 100644 index 000000000..10fd1a911 --- /dev/null +++ b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/SKILL.md @@ -0,0 +1,172 @@ +--- +name: openai-ads-conversions-setup +description: Guide Codex through instrumenting or extending repositories with OpenAI Ads Measurement Pixel and optional Conversions API (CAPI). Use when adding Ads conversion tracking, browser pixel events, server-side conversion events, event_id deduplication, CAPI secret placeholders, incremental conversion coverage, or validating Ads conversion setup. Applies to local repositories and PR review contexts; prioritize safe, reviewable diffs and never place API keys or secrets in source code or client bundles. +--- + +# OpenAI Ads Conversions Setup + +## Overview + +Use this skill to add, extend, or review OpenAI Ads conversion instrumentation in an advertiser repository. It supports browser Pixel setup, server-side Conversions API setup, Pixel+CAPI deduplication, and incremental reruns after the app adds new pages or flows, while keeping diffs small and avoiding secret exposure. + +Use only public OpenAI Ads documentation and repository-local patterns for implementation choices. If the docs are unavailable or unclear, ask for clarification or leave a documented follow-up instead of relying on undocumented behavior. + +## Required Inputs + +Before editing code, identify: + +- Setup mode: `pixel`, `capi`, or `pixel+capi`. +- Frontend/runtime surface: browser JavaScript/TypeScript, server-rendered web, backend-only, mobile/native, or unknown. +- Pixel ID, if Pixel is in scope. +- CAPI authentication plan, if CAPI is in scope: existing secret/env var name, secret manager path, or placeholder-only. +- Conversion events to instrument and where they occur in the product flow. +- Whether this is a first-time setup or an incremental rerun extending existing OpenAI Ads instrumentation. +- Whether the desired behavior is `propose patch`, `apply local patch`, or `review existing integration`. + +Treat the linked OpenAI Ads docs as the source of truth for exact SDK syntax, endpoint shape, supported events, request fields, and validation rules. When browsing is available, check the current docs before writing concrete calls; if the docs conflict with this skill, follow the docs and mention the conflict in the setup report. When browsing is unavailable, use this skill's references as a fallback. Do not invent SDK functions, event names, request schemas, or endpoint paths. + +Primary docs to prefer when available: + +- `https://developers.openai.com/ads/measurement-pixel` +- `https://developers.openai.com/ads/conversions-api` +- `https://developers.openai.com/ads/supported-events` + +## Workflow + +1. Inspect the repository shape. + - Detect framework, package manager, router, server runtime, frontend surface, env convention, test commands, and existing analytics integrations. + - Determine whether a browser JavaScript Pixel is supportable. If the customer-facing surface is mobile/native, backend-only, or otherwise lacks a safe browser JS insertion point, use CAPI-only and report that Pixel was skipped. + - Search for existing OpenAI Ads instrumentation: Pixel initialization, CAPI clients, event helpers, config/env vars, dedupe IDs, attribution context, tests, and setup docs. + - Search for existing pixels or server conversion APIs such as Meta Pixel/CAPI, Google Ads/gtag, TikTok Events API, Pinterest, Snap, Segment, RudderStack, or custom analytics wrappers. + - Use other ad platform integrations as evidence for local conversion boundaries, consent gates, config/secret patterns, non-blocking dispatch, dedupe IDs, and tests. Do not copy their event names, payload schemas, user matching fields, credential exposure patterns, or retry behavior. + - Prefer the repository's existing analytics abstraction over adding a parallel abstraction. If OpenAI Ads instrumentation already exists, treat its helper/client/config pattern as canonical. + - Search for existing config and secret retrieval patterns before choosing env vars: framework settings, typed config objects, deployment env conventions, Vault, cloud secret managers, Kubernetes secrets, Doppler, 1Password, or similar. + +2. Confirm the setup plan. + - For Pixel, identify a single browser-only initialization point, event call sites, and any approved browser user-data path. + - For CAPI, identify a server-only execution point, secret retrieval pattern, and outbound HTTP/client pattern. + - For Pixel+CAPI, define one logical Pixel ID and a stable `event_id` strategy so browser and server events can deduplicate. + - For CAPI web events, define an `oppref` strategy and a sanitized `source_url` strategy before editing. Prefer a server-readable raw `__oppref` cookie value; if unavailable, pass minimal optional conversion context from the browser to an existing server conversion request. For deduped Pixel+CAPI events, make CAPI `source_url` describe the same browser conversion context as the paired Pixel event when practical. If the browser fires the Pixel event before navigation and the server would otherwise derive a different URL, pass the browser's current `sourceUrl` to the server and validate it against a trusted request/configured origin before using it. If a configured canonical site origin exists, prefer it for fallback `source_url` construction while still allowing the request origin as trusted. + - For CAPI-only web setup, explicitly plan how the server event will include matching and attribution context that the Pixel would otherwise help provide: `oppref`, `source_url`, client `user_agent`, trusted client IP, and documented hashed user identifiers when safely available. + - If adding a CAPI validation toggle, keep checked-in defaults production-capable. `validate_only: true` is documented for local smoke tests, but committed env templates and runtime defaults should be blank or false unless the user explicitly asks for validation-only behavior. + - Select supported event names from current docs. Evaluate the primary supported events (`page_viewed`, `contents_viewed`, `items_added`, `checkout_started`, `order_created`, `lead_created`, `registration_completed`, `appointment_scheduled`, `subscription_created`, and `trial_started`) and report high-confidence events you will instrument plus supported events you intentionally skip. + - If Pixel is installed and `page_viewed` is not instrumented, explicitly say why: too noisy, SPA route semantics unclear, no meaningful landing pages, already covered by an existing helper, or intentionally deferred. + - Do not silently skip plausible events. For each skipped supported event, state why: not applicable, no confirmed success boundary found, ambiguous product semantics, unsupported surface, or safe follow-up available if the advertiser wants broader coverage. + - If the target flow is ambiguous, ask one concise question before editing. + +3. Implement the smallest useful patch. + - Add initialization once. + - Instrument only clearly identified conversion events. + - Favor a small high-confidence initial patch over speculative broad coverage. Put plausible but unconfirmed events in the setup report as optional follow-ups, not half-guessed instrumentation. + - Keep config names explicit. Public framework aliases such as `NEXT_PUBLIC_OPENAI_ADS_PIXEL_ID` or `VITE_OPENAI_ADS_PIXEL_ID` are allowed for browser Pixel code, but document that they must contain the same Pixel ID as the server-side Pixel ID when CAPI sends the same event. + - Never put CAPI keys in browser-visible env vars, frontend code, logs, comments, docs, test snapshots, or generated reports. + - Use documented OpenAI Ads event names and request field names only. Do not use legacy CAPI field names such as `event_name`, `event_time_epoch_ms`, `event_source_url`, or `event_data`. + - For commerce flows, ensure each instrumented event covers the actual success path. For `items_added`, cover all successful add-to-cart and quantity-increment paths you can confirm; if you only cover a subset, say that clearly in the setup report and offer the missing paths as follow-ups. + - If Pixel is installed and documented user matching data, such as hashed email or location fields, becomes available client-side after the initial Pixel init, add a second `oaiq("init", { user })` call near that point. Do this for normal web checkout/signup flows unless consent, opt-out, or repository policy prevents it; if skipped, explain why in the setup report. Do not include `pixelId` again after a successful first init, and do not put Pixel user data in `measure` calls. + - Conversion reporting must not fail, block, or materially slow the core business flow. Pixel helpers should catch/suppress their own errors. The whole CAPI path, including event construction, source URL derivation, user-data hashing, timeout setup, and dispatch, must be inside a non-blocking/catch boundary so checkout, signup, lead submission, and other success paths still complete if conversion reporting code throws. + +4. Verify locally. + - Run relevant typecheck, lint, unit tests, or framework-specific checks when available. + - Resolve the directory containing this installed skill before using helper scripts. + - Run this skill's static helpers when useful: + - `python3 /scripts/verify_capi_secret_not_exposed.py ` + - `python3 /scripts/verify_ads_setup.py --pixel-id --require pixel --require capi` + - For Pixel+CAPI web dedupe, add `--require dedupe --require shared-pixel-id --require oppref --require source-url`. + - These helpers run locally. By default, they report paths, line numbers, rules, and summaries without printing source-line context or transmitting repository contents. + - If verification cannot run, report the blocker and the residual risk. + +5. Produce a setup report. + - Include changed files, detected stack, setup mode, existing OpenAI Ads instrumentation found, events added or changed in this run, events already covered, supported events skipped with reasons, optional follow-up events the advertiser can add if desired, skipped surfaces/platforms, Pixel ID/config reference, Pixel user-data strategy, Pixel opt-out behavior, CAPI secret reference, CAPI `oppref` strategy, CAPI `source_url` strategy, browser-provided `sourceUrl` trust boundary, CAPI user-data strategy, event deduplication strategy, checks run, manual follow-ups, and risks. + - Do not include raw secrets or sensitive advertiser source snippets. + - End with a clear deployment review warning: before deploying, the advertiser should review that the implementation satisfies their privacy, security, consent, and data handling requirements. + +## Incremental Reruns + +When rerunning this skill on a repository that may already have OpenAI Ads instrumentation: + +- Build an inventory first: Pixel init locations, CAPI clients, event helpers, analytics sinks, event call sites, Pixel ID/config sources, `event_id` generation, `oppref` and `source_url` handling, user-data handling, tests, and docs. +- Treat existing OpenAI Ads integration surfaces as canonical. Extend the existing helper/client/adapter instead of creating a second Pixel initializer, second CAPI client, second config convention, or scattered one-off call sites. +- Compare current app flows against the existing event inventory. Add only missing coverage at confirmed success boundaries. +- Check duplicate-risk before editing: a new page or flow may already publish through a shared cart service, checkout service, confirmation component, analytics wrapper, or server conversion helper. +- Preserve existing dedupe, Pixel ID, attribution, user-data, failure-handling, test, and reporting patterns unless they are unsafe or incompatible with current docs. +- If another ad platform already publishes from a new flow but OpenAI Ads does not, treat that as a strong signal of missing OpenAI Ads coverage. Still map it to documented OpenAI Ads event names and payload fields. +- Keep rerun diffs focused. Do not churn env templates, setup docs, or shared helpers unless the extension requires it or they are stale. +- In the final report, separate existing events found, new events added, events skipped because they are already covered, and optional follow-ups. + +## Pixel Rules + +Use `references/measurement-pixel.md` for browser Pixel details. + +- Initialize in a client-only location that runs once per page/app load. +- Use the documented browser SDK pattern: load `https://bzrcdn.openai.com/sdk/oaiq.min.js`, initialize with `oaiq("init", { pixelId })`, and emit events with `oaiq("measure", ...)`. +- Use Pixel only for browser JavaScript/TypeScript surfaces supported by current docs. For mobile/native or unsupported frontends, skip Pixel and prefer CAPI-only when a server conversion boundary exists. +- Avoid duplicate initialization across route transitions, hydration, tests, or nested layouts. +- Instrument conversion events close to the confirmed success boundary, not merely on button click unless the click itself is the conversion. +- Respect consent and privacy gating patterns already present in the repository. +- Do not manually send Pixel `oppref`, `source_url`, timestamps, or batching metadata; the browser SDK handles those transport details. Add `debug: true` only for local/test debugging, not as a committed production default. +- If documented Pixel user data becomes available after initial page-load initialization, call `oaiq("init", { user })` again with only documented, normalized fields. For checkout, signup, lead, subscription, or registration flows where the browser has email or location data before or after the confirmed conversion, add this second init unless consent, opt-out, or repository policy prevents it, and report any skip. Preserve the repository's existing consent gates for whether measurement and user-data collection are allowed. Put `opt_out` on the Pixel event options when measurement is allowed but the event should be opted out of future user-level personalization. +- Use supported standard event names from current docs, such as `page_viewed`, `contents_viewed`, `items_added`, `checkout_started`, `order_created`, `lead_created`, `registration_completed`, `appointment_scheduled`, `subscription_created`, or `trial_started`. If uncertain, use the closest documented event or leave an explicit follow-up instead of inventing a name. +- Custom events are a fallback when no standard event fits. Use `custom` only with a valid, documented `custom_event_name`. + +## CAPI Rules + +Use `references/conversions-api.md` for server-side setup details. + +- CAPI code must run server-side only. +- Do not create, request, store, or print CAPI keys. +- Prefer an existing secret manager/env retrieval pattern; otherwise add a placeholder config reference and report that the user must provision the secret. +- Use existing HTTP clients, retry conventions, telemetry, and privacy filters. +- Do not add new queues, databases, background jobs, or broad infra unless the user explicitly asks. +- If Pixel and CAPI both emit the same conversion, use the same Pixel ID, event name, and `event_id` in both paths. +- For web CAPI events, include `action_source: "web"` and a sanitized HTTP(S) `source_url` containing only origin plus pathname. Strip query strings and fragments before sending; reject or replace non-HTTP(S) URL schemes with a trusted configured web app URL. For deduped Pixel+CAPI events, prefer a `source_url` from the same browser conversion context as the paired Pixel event. If accepting a browser-provided `sourceUrl`, require its origin to match the current request origin or a configured canonical site origin before using it; otherwise fall back to a server-derived URL. When a configured canonical site origin exists, use it as the preferred fallback origin and do not let request service/proxy origins override it. If the final CAPI `source_url` intentionally differs from the Pixel event's browser page, call that out in the setup report. For non-web server conversions, choose the documented `action_source` that matches the source (`mobile_app`, `offline`, `physical_store`, `phone_call`, `email`, or `other`) instead of forcing `web`. +- Treat `oppref` as opaque attribution context. Prefer a server-readable `__oppref` cookie over client-supplied request context; pass the raw value unchanged and do not parse, URL-decode, cookie-decode, generate, transform, or log it. +- Use CAPI `events[].user` only for documented fields already available through an approved repository pattern. Never send raw email or raw external IDs. For CAPI-only web conversions, make a best effort to include client `user_agent`, trusted client IP, and hashed email or external ID when available; these fields are especially important when there is no paired Pixel event. +- Use current event timestamps in `timestamp_ms`; do not send stale backfills older than the documented window or future-dated events. +- Preserve existing opt-out/consent semantics and map them to documented `opt_out` fields when the repository has a relevant user-level personalization opt-out. +- CAPI failure handling must not throw into checkout, signup, lead submission, or other core success paths. Wrap event construction, source URL derivation, user-data hashing, timeout setup, and dispatch in the same non-blocking failure boundary. Prefer existing background-task or queue patterns; if none exist, use a bounded fire-and-log implementation and document remaining latency risk. +- For `contents` payloads, use top-level `data.amount` for the event total. For item-level `contents[].amount`, use the unit price when the repository exposes it; omit item-level amount if only line totals are available or the semantics are unclear. + +## Framework Hints + +Use `references/framework-patterns.md` for common locations and pitfalls in Next.js, React/Vite, Remix, Express, Rails, Django, Flask, and monorepos. + +Treat these as hints, not rules. The repository's existing conventions win. + +## Verification And Reporting + +Use `references/verification-checklist.md` for validation steps and `references/report-schema.md` for the final setup report format. + +High-priority failure cases: + +- CAPI secret or API key appears in client code, browser-visible env vars, logs, comments, generated docs, snapshots, or reports. +- Pixel initialization can run multiple times in normal navigation. +- CAPI code can run from a browser bundle. +- Pixel and CAPI duplicate the same event without the same logical Pixel ID and shared dedupe key. +- OpenAI Ads event names are invented or unsupported by current docs. +- CAPI payload uses undocumented or legacy field names, mismatched event/data types, or web events without a sanitized `source_url`. +- Pixel user data is attached to `measure` instead of `init`, includes undocumented/raw identity fields, or bypasses the repository's existing consent gates. +- CAPI `timestamp_ms` is stale, future-dated beyond the documented allowance, or not tied to the actual conversion time. +- CAPI action source is forced to `web` for non-web/mobile/offline events instead of using an appropriate documented `action_source`. +- CAPI custom event names are placed in the wrong field, or Pixel custom events omit the options-level `custom_event_name`. +- CAPI web events ignore available `oppref` context or log `oppref` alongside raw identifiers. +- CAPI `source_url` accepts non-HTTP(S) schemes or preserves query strings/fragments. +- CAPI decodes or transforms `oppref` instead of passing the raw opaque value through unchanged. +- CAPI uses a browser-provided `sourceUrl` with an untrusted origin instead of falling back to a server-derived URL. +- CAPI lets request service/proxy origins override a configured canonical site origin when constructing fallback `source_url`. +- Existing consent or user-level personalization opt-out behavior is bypassed or lost. +- Pixel is installed but available checkout/signup/lead user matching data is neither sent through a second `init({ user })` call nor explicitly skipped with a consent/policy reason. +- Deduped Pixel+CAPI events derive materially different browser context or `source_url` without explanation. +- The setup report does not explain why plausible supported events were skipped. +- The patch instruments likely pre-conversion intent instead of confirmed conversion success. +- Any CAPI setup step, including event construction before dispatch, can fail or materially delay the core business flow. + +## When To Stop And Ask + +Ask before proceeding when: + +- The conversion event boundary is unclear. +- CAPI is requested but no server runtime or secret pattern exists. +- Exact OpenAI Ads SDK/API syntax cannot be verified from docs or provided context. +- Pixel is requested but the repository has no browser JavaScript/TypeScript surface and no safe browser insertion point. +- The only viable implementation would require new infrastructure, a deploy step, or changing authentication/checkout behavior. +- The user provides or asks you to embed a raw CAPI key in the repository. diff --git a/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/agents/openai.yaml b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/agents/openai.yaml new file mode 100644 index 000000000..7d46ee629 --- /dev/null +++ b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "OpenAI Ads Conversions Setup" + short_description: "Instrument Ads Pixel and CAPI safely" + default_prompt: "Use $openai-ads-conversions-setup to instrument or extend this repository with OpenAI Ads Measurement Pixel and optional Conversions API, then produce a setup report." diff --git a/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/conversions-api.md b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/conversions-api.md new file mode 100644 index 000000000..7a1154d8e --- /dev/null +++ b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/conversions-api.md @@ -0,0 +1,137 @@ +# Conversions API Reference + +## Purpose + +Conversions API (CAPI) sends conversion events from trusted server-side code. Use it for conversions that are confirmed on the server, for improved reliability, or to pair with Pixel events through deduplication. + +## Non-Negotiable Safety Rules + +- CAPI authentication material must never be written to source code, frontend code, browser-visible env vars, logs, generated reports, snapshots, or comments. +- Do not mint or store secrets. Add only a config reference or placeholder unless the user explicitly asks for secret provisioning through an approved system. +- Do not expose server conversion endpoints that allow arbitrary client event injection. +- Do not log full request payloads if they contain identifiers, customer data, or auth metadata. +- Treat `oppref` as opaque attribution context, not a user identifier. Pass the raw value unchanged when available, but do not parse, URL-decode, cookie-decode, generate, transform, store broadly, or log it with raw identifiers. + +## Implementation Checklist + +1. Find server runtime and outbound API conventions. + - Look for route handlers, API controllers, background jobs, webhook handlers, service objects, typed HTTP clients, retries, and logging standards. + - Prefer existing conversion/event service patterns when present. + - On reruns, inventory existing OpenAI Ads CAPI clients, payload builders, event boundaries, tests, and config before adding new code. Extend the existing CAPI path instead of creating a second client or config convention. + - Use server-side conversion publishers for other ad platforms as evidence for boundaries, secret retrieval, non-blocking dispatch, dedupe IDs, and tests. Do not copy their request schema, user matching fields, retry policy, or credential handling. + - Search for existing config and secret retrieval patterns before defaulting to env vars: typed settings, framework config, deployment env conventions, Vault, cloud secret managers, Kubernetes secrets, Doppler, 1Password, or similar. + +2. Choose the event boundary. + - Purchase: order/payment success persisted on the server. + - Lead: server accepts and stores the lead, or the upstream CRM request succeeds. + - Signup: user/account creation succeeds. + - Subscription/trial: entitlement or plan activation succeeds. + +3. Add safe configuration. + - Use an existing config/env abstraction. + - Use a clear placeholder such as `OPENAI_ADS_CONVERSIONS_API_KEY` for server-only auth. + - If the framework distinguishes public env vars, never use a public prefix for CAPI keys. + - Use one logical Pixel ID for Pixel and CAPI when both emit the same event. A browser public env alias may exist, but it should resolve to the same Pixel ID used by server-side CAPI. + - If adding a `validate_only` toggle, make checked-in env templates and runtime defaults blank or false. `validate_only: true` is for local smoke tests and should not be the default deployed behavior unless the user explicitly asks for validation-only instrumentation. + +4. Build the request using documented schema only. + - Send `POST https://bzr.openai.com/v1/events?pid=` with `Content-Type: application/json` and `Authorization: Bearer `. + - Use current field names: top-level `validate_only` and `events`, and per-event `id`, `type`, `timestamp_ms`, `oppref`, `source_url`, `action_source`, `user`, and `data`. + - Include `custom_event_name` as a CAPI event field when `type` is `custom`. Do not put it inside `data` for CAPI. + - Include `opt_out` when the repository has an existing opt-out or consent concept that should opt the event out of future user-level personalization. + - Do not use legacy field names such as `event_name`, `event_time_epoch_ms`, `event_id`, `event_source_url`, or `event_data` in the CAPI request body. `event_id` is valid for Pixel options, but CAPI uses `id`. + - Verify endpoint path, auth header, event names, field names, and required identifiers from current docs. + - Reuse existing HTTP client and timeout/retry conventions. + - Keep payload construction small and testable. + +5. Add deduplication for Pixel+CAPI. + - Include the same Pixel ID, event name, and `event_id` in Pixel and CAPI events for the same conversion. + - Prefer existing order/payment/lead IDs when stable and non-sensitive. + +6. Add failure behavior consistent with product semantics. + - Conversion reporting failure must not fail, block, or materially slow checkout, signup, lead submission, or other core success flows unless the user explicitly asks for fail-closed behavior. + - Prefer existing background-task, queue, or after-commit patterns. If none exist and adding one would be too broad, use a bounded fire-and-log implementation and report any remaining latency risk. + - Log a redacted warning/metric if the repository has a standard observability pattern. + - Avoid retry loops that can duplicate conversions unless the API supports idempotency/deduplication. + +7. Add web attribution context for CAPI when available. + - If the conversion request is same-site and the server can read cookies, read the raw `__oppref` cookie value and pass it as `events[].oppref` without decoding or normalizing it. + - If the server cannot read the cookie but an existing browser-to-server conversion request exists, add a minimal optional context object such as `openaiAds: { oppref, sourceUrl }`. Treat this as a fallback attribution path, not as trusted business state. + - On the browser, source `oppref` from the `oppref` URL parameter or existing `__oppref` cookie when available. Pass it unchanged and omit it when absent. + - For `action_source: "web"`, send `source_url` as HTTP(S) origin plus pathname only. Strip query strings and fragments before sending, and reject or replace non-HTTP(S) schemes with a trusted configured app URL. + - If using browser-provided `sourceUrl`, validate its origin against the current request origin or a configured canonical site origin before using it. If it is missing, malformed, non-HTTP(S), or from an untrusted origin, fall back to a server-derived URL such as the current route, checkout/confirmation route, or configured canonical site URL. + - If the repository has a configured canonical site origin, such as `OPENAI_ADS_SITE_ORIGIN` or an existing public app URL setting, prefer that origin when constructing fallback `source_url`. The request origin can still be trusted for validation, but it should not override the configured canonical origin for fallback URLs. + - For non-web conversions, choose the documented `action_source` that matches the source: `mobile_app`, `offline`, `physical_store`, `phone_call`, `email`, or `other`. Do not force `web` unless a browser/webpage source URL is available and meaningful. + - Do not build a new public endpoint solely to accept arbitrary conversion events from the browser. + +## Event Contract + +Every CAPI event needs a stable non-empty `id`, supported `type`, integer `timestamp_ms`, and `data` object. `timestamp_ms`, `amount`, and `quantity` values should be integers. `timestamp_ms` must represent the event time in milliseconds, be within the last 7 days, and not be more than 10 minutes in the future. If a `data.amount` is present, include a valid ISO 4217 `data.currency`. + +For `action_source: "web"`, `source_url` is required. For other action sources, include `source_url` only when it is documented and meaningful for the source. Use one of the documented action sources: `web`, `mobile_app`, `offline`, `physical_store`, `phone_call`, `email`, or `other`. + +Use this event-to-data mapping: + +| Event type | `data.type` | Typical boundary | +| --- | --- | --- | +| `page_viewed` | `contents` | meaningful page view, usually Pixel-first | +| `contents_viewed` | `contents` | product/content detail shown | +| `items_added` | `contents` | item added to cart after success | +| `checkout_started` | `contents` | checkout session started | +| `order_created` | `contents` | order/payment success persisted | +| `lead_created` | `customer_action` | lead accepted/stored | +| `registration_completed` | `customer_action` | account creation succeeds | +| `appointment_scheduled` | `customer_action` | appointment booking succeeds | +| `subscription_created` | `plan_enrollment` | paid subscription activates | +| `trial_started` | `plan_enrollment` | trial entitlement activates | +| `custom` | `custom` | no standard event fits | + +For `contents` payloads, use documented content item fields such as `id`, `name`, `content_type`, `quantity`, `amount`, and `currency`. Do not use removed object shapes such as `checkout_session`, `line_items`, or `order`. For commerce totals, use top-level `data.amount` and `data.currency` for the order, checkout, or cart total. If item-level `contents[].amount` is included, prefer unit price. Omit item-level amount when only line totals are available or the repository does not make unit price semantics clear. + +For custom events, set event-level `type: "custom"` and event-level `custom_event_name`, with `data.type: "custom"`. Use lowercase names where possible. Names must be 1-64 ASCII letters, numbers, underscores, or dashes, start and end with an alphanumeric character, and not overlap with standard event names. + +## User Data + +CAPI user data belongs on each event as `events[].user`. Pixel user data belongs in Pixel initialization, not in each Pixel `measure` call. + +Only include documented user fields that the repository already has an approved pattern to collect or derive: + +- `email_sha256` +- `external_id_sha256` +- `country` +- `city` +- `zip_code` +- `ip_address` +- `user_agent` + +Hash `email_sha256` and `external_id_sha256` before sending; values must be lowercase 64-character SHA-256 hex strings. Do not send raw email, raw external IDs, phone numbers, or phone hashes. For `ip_address` and `user_agent`, only pass values already available on the server request path and consistent with the repository's privacy/consent model. + +## Validation And Smoke Testing + +When the repository supports it, add a dry-run or test path that sends `validate_only: true` to CAPI. `validate_only` validates the request but should not be used for Pixel/SDK traffic, and it should not be the default in committed env templates or production runtime config. Keep conversion reporting failures non-blocking for core business logic even when validation or network calls fail. + +If batching events, cap batches at 1000 events. One invalid event fails the full batch, so prefer one event per conversion unless the repository already has a safe batching abstraction, validation path, and retry behavior. + +Add focused tests when practical: + +- Payload builder test for documented field names, event/data type pairing, integer amount/quantity values, and shared dedupe `id`. +- Rerun/extension test or review note showing the new flow uses the existing OpenAI Ads helper/client rather than duplicating CAPI setup. +- `source_url` sanitizer tests that strip query/fragment and reject or replace non-HTTP(S) schemes. +- Browser-provided `sourceUrl` tests that reject untrusted origins, prefer a configured canonical origin for fallback when present, and otherwise fall back to a server-derived URL. +- `oppref` tests or review notes showing cookie/URL values are passed through as raw opaque values without decoding or normalization. +- Conversion boundary test showing reporting is attempted only after the core action succeeds, and skipped when the core action fails. +- Failure-path test or review note showing CAPI network/API failures do not fail checkout, signup, lead submission, or the relevant core flow. + +## When To Defer CAPI + +Defer to a manual follow-up if: + +- There is no server-side conversion boundary in the repository. +- There is no safe secret/config pattern. +- The exact CAPI schema/auth behavior cannot be verified. +- The implementation cannot safely capture `oppref` or `source_url`; omit/report those fields rather than creating a brittle or privacy-risky path. +- Implementing CAPI would require new infrastructure such as queues, cron, secret provisioning, database migrations, or webhook contracts beyond the user's request. + +## Suggested Output + +Report the server files changed, event boundaries, secret reference name, `oppref` strategy, `source_url` strategy, user-data strategy, dedupe strategy, tests/checks run, and manual secret provisioning steps. If no user data is sent, say so and explain why, such as no approved email/customer identifier path. Never include a raw key, token, raw user data, or sensitive source snippets. diff --git a/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/framework-patterns.md b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/framework-patterns.md new file mode 100644 index 000000000..07d875d01 --- /dev/null +++ b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/framework-patterns.md @@ -0,0 +1,80 @@ +# Framework Patterns + +Use these as starting points while inspecting a repository. Existing local conventions should override these hints. + +## Next.js + +- App Router Pixel candidates: client providers imported by `app/layout.tsx`, analytics provider components, or client-only tag modules. +- Pages Router Pixel candidates: `_app.tsx`, analytics providers, or browser entry modules. +- CAPI candidates: route handlers, API routes, server actions, checkout webhooks, order services, or backend package code. +- `NEXT_PUBLIC_*` values are browser-visible. Pixel IDs may be public if docs require it; CAPI keys must not use this prefix. +- Watch for server components. Pixel code must be inside a client component or browser-only module. +- For CAPI web attribution, prefer reading `__oppref` from request cookies in route handlers/server actions. If the conversion is submitted from a client component and cookies are not available server-side, pass a minimal optional context object through the existing request body. + +## React/Vite/SPA + +- Pixel candidates: `src/main.tsx`, `src/App.tsx`, analytics providers, or router-level providers. +- CAPI usually requires a backend package or API service. If the repo is frontend-only, add Pixel and report that CAPI needs server ownership. +- Avoid embedding CAPI calls in frontend API clients. +- If the SPA posts conversion data to a backend, pass only optional attribution context such as `oppref` and sanitized `sourceUrl`; never pass the CAPI key or let the browser choose arbitrary event names. + +## Mobile / Native Apps + +- Do not add the browser Measurement Pixel to iOS, Android, React Native, Flutter, or other native-only surfaces unless current docs explicitly provide a supported SDK for that platform. +- Prefer CAPI-only from a trusted backend conversion boundary when available. +- If the repo has both web and mobile apps, instrument only the supported web surface with Pixel and report mobile as CAPI-only or deferred. + +## Remix + +- Pixel candidates: root document scripts, client analytics modules, or route success pages. +- CAPI candidates: actions/loaders that confirm conversion, server utilities, or webhook handlers. +- Keep server-only code out of files imported by browser bundles. + +## Express/Fastify/Nest + +- Pixel is usually in a separate frontend package. +- CAPI candidates: controllers/routes for checkout, lead submission, signup, or webhook processing. +- Use existing config providers and HTTP client abstractions. +- Read `__oppref` from request cookies when same-site; otherwise accept optional attribution context only on existing trusted conversion routes. + +## Rails + +- Pixel candidates: layout templates, Stimulus controllers, frontend packs, or tag manager partials. +- CAPI candidates: controllers after successful persistence, service objects, ActiveJob jobs, or webhook handlers. +- Use credentials/env patterns already present. +- Prefer controller/request cookie access for `__oppref` before adding browser payload plumbing. + +## Django/Flask + +- Pixel candidates: base templates, frontend bundles, or success-page templates. +- CAPI candidates: views after successful persistence, service modules, Celery tasks, or webhook handlers. +- Use existing settings/secrets patterns. +- Prefer request cookie access for `__oppref` before adding browser payload plumbing. + +## Monorepos + +- Identify the deployed web app and backend package before editing. +- Avoid adding dependencies at the repo root unless the repository already centralizes analytics there. +- Run the package-specific checks rather than broad repo-wide checks when the monorepo is large. + +## Existing Analytics Abstractions + +If an analytics abstraction already exists, prefer adding an OpenAI Ads adapter/sink. This usually gives better consent handling, fewer call sites, and easier testing. + +Avoid bypassing a mature analytics layer unless it cannot represent the required OpenAI Ads event fields. + +## Existing Advertising Platform Integrations + +Search for Meta Pixel/CAPI, Google Ads/gtag, TikTok Events API, Pinterest, Snap, Segment, RudderStack, or custom/first-party ad conversion publishers before adding new OpenAI Ads code. + +Use those integrations to infer: + +- Confirmed conversion boundaries such as checkout success, lead accepted, signup completed, trial started, or subscription activated. +- Existing analytics adapters or event dispatchers to extend. +- Consent/privacy gates. +- Server-side secret/config retrieval patterns. +- Non-blocking dispatch, queue, background-task, or fire-and-log patterns. +- Dedupe or idempotency IDs such as order ID, payment intent ID, lead ID, or signup transaction ID. +- Local tests and fixtures for conversion behavior. + +Do not copy platform-specific event names, request schemas, user matching fields, retry semantics, or credential exposure patterns. Map each event back to current OpenAI Ads docs. diff --git a/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/measurement-pixel.md b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/measurement-pixel.md new file mode 100644 index 000000000..695c3b015 --- /dev/null +++ b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/measurement-pixel.md @@ -0,0 +1,96 @@ +# Measurement Pixel Reference + +## Purpose + +The Measurement Pixel captures browser-side conversion events for OpenAI Ads. Use it when the repository has a client-rendered flow or a browser-visible success page that can emit a conversion after user consent and after the conversion has actually happened. + +The Pixel is a browser JavaScript integration. If the customer-facing surface is mobile/native, backend-only, or otherwise lacks a safe browser JavaScript/TypeScript insertion point, skip Pixel setup and prefer CAPI-only when a server conversion boundary exists. + +## Inputs + +- Pixel ID or repository config key that resolves to the Pixel ID. +- Event list and event boundaries. +- Consent/privacy gating requirements. +- Current OpenAI Ads Pixel docs or SDK reference for exact syntax. Prefer the docs when available; use this reference as a fallback when browsing is unavailable. + +## Implementation Checklist + +1. Find the existing analytics layer. + - Prefer wrappers such as `analytics.track`, `trackEvent`, `useAnalytics`, `gtag`, `fbq`, Segment, RudderStack, or custom/first-party event dispatchers. + - If a wrapper exists, add OpenAI Ads as another sink rather than scattering calls throughout the app. + - On reruns, inventory existing OpenAI Ads browser helpers and event call sites before adding new ones. Extend the existing helper or analytics sink instead of adding a second Pixel initialization or parallel wrapper. + - Use other ad platform browser events as clues for local boundaries and consent gates, but map event names and payloads to OpenAI Ads docs. + +2. Initialize once in browser code. + - Good candidates include app/root layout client providers, analytics providers, main browser entrypoints, or tag manager modules. + - The current SDK pattern is: insert the loader script near the top of ``, load `https://bzrcdn.openai.com/sdk/oaiq.min.js`, initialize with `oaiq("init", { pixelId: "" })`, and emit events with `oaiq("measure", ...)`. + - Use the exact current docs snippet when possible. If framework conventions require a component/module wrapper, preserve the same loader URL, global queue name, init command, and measure command semantics. + - `pixelId` is required. `debug` is optional and should be enabled only for local/test troubleshooting unless the user explicitly asks otherwise. + - Avoid server components, SSR-only files, route handlers, tests, stories, and build scripts. + - Guard against repeated initialization during client-side navigation. + - If user data is already available through an approved browser analytics path, pass documented user fields during Pixel initialization. Do not attach user data to each `measure` call unless current docs explicitly require it. + - Pixel user fields are `email_sha256`, `external_id_sha256`, `country`, `city`, and `zip_code`. Hash raw email or external IDs before sending, or omit them if the repository has no approved browser hashing/normalization pattern. + - If user data becomes available after the first init, such as during login, checkout, lead submission, or signup, call `oaiq("init", { user })` again with the complete documented `user` object before or near the relevant conversion event. After a successful first init, the follow-up init does not need to include `pixelId`. + - Follow the repository's existing consent gates for whether Pixel measurement and Pixel user-data updates are allowed. If measurement is allowed but the event should be opted out of future user-level personalization, send the conversion event with Pixel options `{ opt_out: true }`. + +3. Emit events at confirmed conversion boundaries. + - Use only supported event names from current docs. + - Commerce funnel examples: `page_viewed`, `contents_viewed`, `items_added`, `checkout_started`, `order_created`. + - Lead/signup examples: `lead_created`, `registration_completed`, `appointment_scheduled`. + - Subscription examples: `subscription_created`, `trial_started`. + - Purchase: after payment/order confirmation, not on checkout click. + - Add-to-cart: after the cart mutation succeeds. Cover each confirmed add path, including product-card add buttons, product-detail add buttons, and cart quantity-increment controls when they create or increase a cart line. If only some paths are safe to instrument, report the uncovered paths as optional follow-ups. + - Lead: after lead submission succeeds, not when a form opens. + - Signup: after account creation succeeds, not when signup starts. + - Subscription/trial: after plan/trial activation succeeds. + - Custom events: only if current docs support them, no standard event fits, and the event has a valid `custom_event_name`. + - If you install Pixel but intentionally omit `page_viewed`, include the reason in the report: too noisy, SPA route semantics unclear, no meaningful landing pages, already covered elsewhere, or deferred for a smaller first patch. + +4. Preserve existing privacy behavior. + - Follow existing consent, cookie, and data minimization gates. + - Do not add user identifiers or hashed PII unless current docs require/allow it and the repository already has an approved normalization path. + - For Pixel-only events, do not manually pass `oppref`, `source_url`, timestamps, or batching metadata. The SDK captures `oppref` from the landing page URL, stores it in the first-party `__oppref` cookie, adds `source_url`, timestamps events, and batches `measure` calls. + - If Pixel and CAPI are both in scope, preserve attribution context for CAPI by passing `oppref` from the URL or `__oppref` cookie to the server only through an existing conversion request path when the server cannot read the cookie itself. + +5. Keep config explicit. + - Browser-visible Pixel ID may use the repository's public env convention, such as `NEXT_PUBLIC_OPENAI_ADS_PIXEL_ID` in Next.js. + - If CAPI sends the same event, the browser-visible Pixel ID must be the same logical Pixel ID used by server-side CAPI. Separate env variable names are fine only as framework aliases for the same value. + - Do not use browser-visible config for CAPI credentials. + +## Event Deduplication + +When Pixel and CAPI both emit the same conversion, use the same Pixel ID, same event name, and same `event_id` in both the browser and server paths. Prefer an existing order ID, payment intent ID, lead ID, or signup transaction ID when it is stable and non-sensitive. If no stable ID exists, generate one at conversion start and persist it through the successful completion path. + +For Pixel, pass `event_id` in the optional fourth `oaiq("measure", ...)` options object. For CAPI, the same value is the event's `id`. For custom events, keep the same `custom_event_name` on both Pixel and CAPI. + +## Standard Event Selection + +Evaluate all primary supported event types before choosing the smallest safe set. Instrument high-confidence events now; put plausible but unconfirmed events in the report as optional follow-ups the advertiser can add if desired. + +- `page_viewed`: page load or important landing page view; usually rely on Pixel if browser support exists. +- `contents_viewed`: specific product, listing, article, plan, or content unit view, including interactions that happen after a page has loaded. +- `items_added`: add-to-cart or quantity increment succeeds; cover all confirmed successful add paths or report partial coverage. +- `checkout_started`: checkout session starts. +- `order_created`: order or payment is confirmed. +- `lead_created`: lead submission is accepted. +- `registration_completed`: account creation completes. +- `appointment_scheduled`: booking completes. +- `subscription_created`: subscription activates. +- `trial_started`: trial starts. +- `custom`: use only when no standard event maps cleanly. + +For Pixel custom events, call `oaiq("measure", "custom", { type: "custom" }, { custom_event_name: "" })`. Custom event names must be 1-64 characters, contain only letters, numbers, underscores, or dashes, start and end with a letter or number, and not match a standard event name. Use lowercase names for consistency. + +## Common Risks + +- Initializing the Pixel in both a root layout and a nested page. +- Rerunning the setup and adding duplicate event calls to a page that already publishes through a shared analytics helper. +- Emitting a conversion on attempted action instead of success. +- Putting Pixel user data on `measure` calls instead of `init`, or sending raw email/raw external IDs as Pixel user fields. +- Bypassing the repository's existing consent gates for Pixel measurement or Pixel user-data updates. +- Using a standard event name that is not supported by current docs. +- Duplicating a Pixel and CAPI event without a shared `event_id`. +- Adding browser plumbing for `oppref` when the server already has the `__oppref` cookie. +- Silently skipping obvious funnel events such as product views, add-to-cart, or checkout start without explaining whether they were inapplicable, ambiguous, or simply left as optional follow-ups. +- Adding Pixel logic before consent checks that other analytics obey. +- Coupling Pixel setup to checkout implementation details more than necessary. diff --git a/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/report-schema.md b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/report-schema.md new file mode 100644 index 000000000..c8444b654 --- /dev/null +++ b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/report-schema.md @@ -0,0 +1,71 @@ +# Setup Report Schema + +Use this structure for the final response after applying or reviewing instrumentation. + +```markdown +**OpenAI Ads Conversions Setup Report** + +Setup mode: pixel | capi | pixel+capi | review-only +Detected stack: + +Changed files: +- : + +Existing instrumentation found: +- + +Existing ad platform patterns used: +- + +Events instrumented: +- : + +Events already covered: +- : + +Events/surfaces skipped: +- : + +Optional follow-up events: +- : + +Pixel: +- Pixel ID/config: +- Initialization point: +- Docs syntax source: +- debug behavior: +- Pixel user data: +- Pixel opt_out: +- Consent handling: + +CAPI: +- Server boundary: +- Secret reference: +- Request/auth docs verified from: +- validate_only behavior: +- action_source/source_url: +- Browser-provided source_url validation: +- timestamp_ms: +- oppref handling: +- user data: +- opt_out handling: +- Failure behavior: + +Deduplication: +- event_id source: +- Pixel+CAPI pairing: + +Verification: +- : passed | failed | not run () + +Manual follow-ups: +- + +Risks / unknowns: +- + +Deployment review warning: +- Before deploying, review that this implementation satisfies your privacy, security, consent, and data handling requirements. +``` + +Never paste raw CAPI keys, customer data, or sensitive advertiser code snippets into the report. diff --git a/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/verification-checklist.md b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/verification-checklist.md new file mode 100644 index 000000000..626b2a730 --- /dev/null +++ b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/references/verification-checklist.md @@ -0,0 +1,71 @@ +# Verification Checklist + +## Static Checks + +- Pixel initialization appears once in the normal browser startup path. +- Pixel user data, if used, is sent through `oaiq("init", { user })`, not through `measure` calls. +- Late-arriving Pixel user data, such as checkout/login/signup fields, triggers a follow-up `init` without repeating `pixelId` after the first successful init. +- Existing OpenAI Ads instrumentation was inventoried before changes on reruns. +- Pixel code is not imported by server-only execution paths unless guarded correctly. +- CAPI code is server-only and never imported by browser bundles. +- CAPI auth config uses a server-only env/secret convention. +- New event coverage extends existing OpenAI Ads helpers/clients/adapters instead of creating duplicate initialization, duplicate CAPI clients, or new config conventions. +- Pixel+CAPI duplicate events use the same logical Pixel ID, same event name, and same stable `event_id`. +- New call sites are not already covered by a shared analytics wrapper, cart/checkout service, confirmation component, or server conversion helper. +- Existing Meta/Google/TikTok/other ad platform integrations were used only as local pattern evidence, not as the OpenAI Ads API contract. +- Event names and payload fields match current OpenAI Ads docs. +- Checked-in env templates and runtime defaults do not set CAPI `validate_only` to true unless the user explicitly requested validation-only behavior. +- CAPI request body uses current field names (`id`, `type`, `timestamp_ms`, `source_url`, `action_source`, `data`) and not removed legacy names. +- CAPI `timestamp_ms` is generated from the actual event time, not a stale backfill or future timestamp. +- CAPI event `type` matches the documented `data.type` family. +- CAPI custom events use event-level `custom_event_name`; Pixel custom events use the Pixel options object. +- Commerce CAPI totals use top-level `data.amount`; item-level `contents[].amount` is unit price or omitted when semantics are unclear. +- CAPI web events include `action_source: "web"` and a sanitized HTTP(S) `source_url` with origin plus pathname only. +- Non-web CAPI events use an appropriate documented `action_source` such as `mobile_app`, `offline`, `physical_store`, `phone_call`, `email`, or `other`. +- CAPI `source_url` sanitizer strips query/fragment and rejects or replaces non-HTTP(S) schemes. +- Browser-provided CAPI `sourceUrl` is accepted only when its origin matches the current request origin or a configured canonical site origin; otherwise the implementation falls back to a server-derived URL. +- If a configured canonical site origin exists, fallback `source_url` construction prefers it over request service/proxy origins. +- Available `oppref` context is passed as a raw opaque value to CAPI and not decoded, normalized, transformed, or logged; server cookie access is preferred over client-supplied context. +- CAPI user data is event-scoped, documented, and uses hashed identity fields rather than raw email or raw external IDs. If no user data is sent, the report explains why. +- Existing consent or personalization opt-out behavior is preserved and mapped to documented `opt_out` fields when applicable. +- Existing consent gates control whether Pixel measurement and Pixel user-data updates are allowed. When measurement is allowed but the event should be opted out of future user-level personalization, the Pixel event uses documented `opt_out`. +- Pixel implementation relies on SDK-managed `oppref`, `source_url`, timestamps, and batching; manual plumbing for those fields is only added for CAPI when needed. +- Plausible supported events that are not instrumented are listed with reasons and optional follow-up guidance; if Pixel is installed and `page_viewed` is omitted, the reason is explicit. +- `items_added` covers all confirmed add-to-cart and quantity-increment success paths, or the report explicitly calls out partial coverage. +- Consent/privacy gating matches existing analytics behavior. +- Unsupported frontend surfaces such as mobile/native are skipped or reported as CAPI-only. + +## Suggested Commands + +Run the narrowest relevant commands the repository supports: + +- TypeScript: `npm run typecheck`, `pnpm typecheck`, `yarn typecheck`, or package-specific equivalents. +- Lint: `npm run lint`, `pnpm lint`, `yarn lint`, or package-specific equivalents. +- Tests: targeted unit tests for changed files, checkout/lead/signup handlers, or analytics adapters. +- Static helper: `python3 /scripts/verify_capi_secret_not_exposed.py `. +- Static helper: `python3 /scripts/verify_ads_setup.py --pixel-id --require pixel --require capi`. +- If Pixel+CAPI dedupe is in scope, include `--require dedupe --require shared-pixel-id`. +- If CAPI web attribution is in scope, include `--require oppref --require source-url`. + +`` is the directory containing this installed skill. + +The helper scripts run locally. By default, they report paths, line numbers, rules, and summaries without printing source-line context or transmitting repository contents. + +## Manual Checks + +- Confirm the Pixel script initializes only after consent when consent is required. +- Confirm docs-focused Pixel coverage when applicable: current loader/init syntax, late `user` init placement, Pixel options-level `opt_out`, and no user data on `measure`. +- Confirm docs-focused CAPI coverage when applicable: event-scoped `user`, `opt_out`, valid `timestamp_ms`, documented `action_source`, trusted/sanitized `source_url`, configured canonical origin fallback, raw opaque `oppref` pass-through, and batch failure behavior if batching exists. +- Confirm conversion event fires after a successful test conversion, not before. +- Confirm Pixel or CAPI failures do not fail, block, or materially slow the user flow unless explicitly intended. +- Confirm no secret value appears in browser devtools, source maps, build output, logs, or reports. +- Confirm CAPI uses `Content-Type: application/json` and bearer auth from a server-only secret. +- Confirm CAPI dry-run or test hooks can use `validate_only: true` where practical, but deployed/default config sends real events. +- Confirm any CAPI batching has a clear failure strategy, because one invalid event can fail the whole batch. +- Confirm route/controller tests cover success-only CAPI scheduling when practical. +- Confirm rerun reports separate existing events found, new events added, events already covered, and optional follow-ups. +- Confirm final diff is limited to conversion setup and nearby tests/config. + +## Failure Handling + +If a check fails, either fix the issue or leave the integration unapplied and report the exact blocker. Do not ship a known secret leak, duplicate conversion emission, or undocumented API call. diff --git a/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/scripts/verify_ads_setup.py b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/scripts/verify_ads_setup.py new file mode 100644 index 000000000..de45e6d35 --- /dev/null +++ b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/scripts/verify_ads_setup.py @@ -0,0 +1,435 @@ +#!/usr/bin/env python3 +"""Static sanity checks for OpenAI Ads Pixel and CAPI integrations.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from pathlib import Path +from typing import Iterable + +SKIP_DIRS = { + ".git", + ".hg", + ".svn", + ".next", + ".nuxt", + ".turbo", + ".venv", + "build", + "coverage", + "dist", + "node_modules", + "out", + "target", + "test", + "tests", + "venv", + "__tests__", +} + +SKIP_FILES = { + "verify_capi_secret_not_exposed.py", + "verify_ads_setup.py", +} + +TEXT_EXTENSIONS = { + ".astro", + ".cjs", + ".erb", + ".go", + ".html", + ".java", + ".js", + ".json", + ".jsx", + ".mjs", + ".php", + ".py", + ".rb", + ".rs", + ".svelte", + ".ts", + ".tsx", + ".vue", + ".yaml", + ".yml", +} + +DOC_EXTENSIONS = { + ".md", + ".mdx", + ".rst", +} + + +def is_env_file(filename: str) -> bool: + return filename == ".env" or filename.startswith(".env.") + + +def is_env_template_file(filename: str) -> bool: + return is_env_file(filename) and ( + filename in {".env.example", ".env.sample", ".env.template"} + or filename.endswith((".example", ".sample", ".template")) + ) + + +PIXEL_MARKER_RE = re.compile( + r"(?i)(openai[_\-\s]?ads[_\-\s]?pixel|openai[_\-\s]?pixel|measurement[_\-\s]?pixel|OPENAI_ADS_PIXEL_ID)" +) +CAPI_MARKER_RE = re.compile( + r"(?i)(openai[_\-\s]?ads[_\-\s]?(conversions[_\-\s]?)?(api|capi)|conversions[_\-\s]?api|OPENAI_ADS_CONVERSIONS_API_KEY)" +) +EVENT_ID_RE = re.compile(r"(?i)\b(event[_\-]?id|eventId|dedupe|deduplication|idempotency)\b") +OPPREF_RE = re.compile(r"(?i)\b(__oppref|oppref)\b") +SOURCE_URL_RE = re.compile(r"(?i)\b(source_url|sourceUrl|sourceURL)\b") +OPENAI_ADS_CAPI_CONTEXT_RE = re.compile( + r"(?i)(bzr\.openai\.com/v1/events|/v1/events\?pid=|OPENAI_ADS_CONVERSIONS_API_KEY|" + r"OPENAI_ADS_CAPI_KEY|openai[_\-\s]?ads[_\-\s]?(conversions[_\-\s]?)?(api|capi))" +) +CAPI_LEGACY_FIELD_RE = re.compile( + r"(?i)(?:[\"']|\b)(event_name|event_time_epoch_ms|event_source_url|event_data)(?:[\"']|\b)\s*:" +) +VALIDATE_ONLY_TRUE_RE = re.compile( + r"(?i)^\s*(?:export\s+)?" + r"OPENAI_ADS(?:_(?:CAPI|CONVERSIONS|CONVERSIONS_API))?_VALIDATE_ONLY" + r"\s*[:=]\s*[\"']?(?:true|1|yes)[\"']?\s*(?:#.*)?$" +) +PUBLIC_CAPI_ENV_RE = re.compile( + r"\b(?:NEXT_PUBLIC|NUXT_PUBLIC|PUBLIC|REACT_APP|VITE)_" + r"(?:[A-Z0-9]+_)*(?:OPENAI_ADS|OPENAI|ADS|CAPI|CONVERSION|CONVERSIONS)" + r"(?:_[A-Z0-9]+)*_(?:API_KEY|KEY|SECRET|TOKEN)[A-Z0-9_]*\b", +) +PUBLIC_PIXEL_ENV_RE = re.compile( + r"\b(?:NEXT_PUBLIC|NUXT_PUBLIC|PUBLIC|REACT_APP|VITE)_OPENAI_ADS_PIXEL_ID\b" +) +SERVER_PIXEL_ENV_RE = re.compile(r"\bOPENAI_ADS_PIXEL_ID\b") +SHARED_PIXEL_GUIDANCE_RE = re.compile( + r"(?i)(same|shared|reuse|match|identical|alias|same value).{0,80}" + r"(pixel id|OPENAI_ADS_PIXEL_ID)" +) +ADS_MEASURE_EVENT_RE = re.compile( + r"(?is)(?:\bwindow\s*\.\s*)?\boaiq\s*(?:\?\.)?\(\s*" + r"[\"']measure[\"']\s*,\s*[\"']([a-z0-9_-]+)[\"']" +) +SUPPORTED_EVENT_NAMES = { + "appointment_scheduled", + "checkout_started", + "contents_viewed", + "custom", + "items_added", + "lead_created", + "order_created", + "page_viewed", + "registration_completed", + "subscription_created", + "trial_started", +} + + +def iter_candidate_files(root: Path, *, include_docs: bool = False) -> Iterable[Path]: + for current_root, dirs, files in os.walk(root): + dirs[:] = [ + directory + for directory in dirs + if directory not in SKIP_DIRS and not (Path(current_root) / directory).is_symlink() + ] + for filename in files: + if filename in SKIP_FILES: + continue + path = Path(current_root) / filename + suffix = path.suffix.lower() + if ( + is_env_file(filename) + or suffix in TEXT_EXTENSIONS + or (include_docs and suffix in DOC_EXTENSIONS) + ): + yield path + + +def read_file(path: Path) -> str: + try: + return path.read_text(encoding="utf-8", errors="ignore") + except OSError: + return "" + + +def first_hits( + root: Path, + pattern: re.Pattern[str], + limit: int = 20, + *, + include_docs: bool = False, +) -> list[dict[str, object]]: + hits: list[dict[str, object]] = [] + for path in iter_candidate_files(root, include_docs=include_docs): + text = read_file(path) + for line_number, line in enumerate(text.splitlines(), start=1): + if pattern.search(line): + hits.append( + { + "path": str(path.relative_to(root)), + "line": line_number, + } + ) + break + if len(hits) >= limit: + break + return hits + + +def literal_hits(root: Path, literal: str, limit: int = 20) -> list[dict[str, object]]: + if not literal: + return [] + + hits: list[dict[str, object]] = [] + for path in iter_candidate_files(root): + text = read_file(path) + for line_number, line in enumerate(text.splitlines(), start=1): + if literal in line: + hits.append( + { + "path": str(path.relative_to(root)), + "line": line_number, + } + ) + break + if len(hits) >= limit: + break + return hits + + +def unsupported_event_hits(root: Path, limit: int = 20) -> list[dict[str, object]]: + hits: list[dict[str, object]] = [] + for path in iter_candidate_files(root): + text = read_file(path) + for match in ADS_MEASURE_EVENT_RE.finditer(text): + event_name = match.group(1) + if event_name in SUPPORTED_EVENT_NAMES: + continue + + hits.append( + { + "path": str(path.relative_to(root)), + "line": text.count("\n", 0, match.start(1)) + 1, + "event_name": event_name, + } + ) + if len(hits) >= limit: + return hits + return hits + + +def legacy_capi_field_hits(root: Path, limit: int = 20) -> list[dict[str, object]]: + hits: list[dict[str, object]] = [] + for path in iter_candidate_files(root): + text = read_file(path) + if not OPENAI_ADS_CAPI_CONTEXT_RE.search(text): + continue + + for match in CAPI_LEGACY_FIELD_RE.finditer(text): + hits.append( + { + "path": str(path.relative_to(root)), + "line": text.count("\n", 0, match.start(1)) + 1, + "field_name": match.group(1), + } + ) + if len(hits) >= limit: + return hits + return hits + + +def validate_only_default_hits(root: Path, limit: int = 20) -> list[dict[str, object]]: + hits: list[dict[str, object]] = [] + for path in iter_candidate_files(root): + if not is_env_template_file(path.name): + continue + + text = read_file(path) + for line_number, line in enumerate(text.splitlines(), start=1): + if not VALIDATE_ONLY_TRUE_RE.search(line): + continue + + hits.append( + { + "path": str(path.relative_to(root)), + "line": line_number, + } + ) + if len(hits) >= limit: + return hits + return hits + + +def make_check( + name: str, passed: bool, hits: list[dict[str, object]], message: str +) -> dict[str, object]: + return { + "name": name, + "passed": passed, + "message": message, + "hits": hits, + } + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Run static sanity checks for OpenAI Ads Pixel and CAPI instrumentation." + ) + parser.add_argument("root", nargs="?", default=".", help="Repository root to verify.") + parser.add_argument("--pixel-id", default="", help="Expected Pixel ID literal, if known.") + parser.add_argument( + "--capi-env", + default="OPENAI_ADS_CONVERSIONS_API_KEY", + help="Expected server-only env var or secret config name for CAPI.", + ) + parser.add_argument( + "--require", + action="append", + choices=( + "pixel", + "capi", + "dedupe", + "shared-pixel-id", + "supported-events", + "oppref", + "source-url", + ), + default=[], + help="Require a specific check to pass. Can be passed more than once.", + ) + args = parser.parse_args() + + root = Path(args.root).resolve() + + pixel_hits = ( + literal_hits(root, args.pixel_id) if args.pixel_id else first_hits(root, PIXEL_MARKER_RE) + ) + capi_marker_hits = first_hits(root, CAPI_MARKER_RE) + capi_env_hits = literal_hits(root, args.capi_env) if args.capi_env else [] + event_id_hits = first_hits(root, EVENT_ID_RE) + public_capi_env_hits = first_hits(root, PUBLIC_CAPI_ENV_RE) + public_pixel_env_hits = first_hits(root, PUBLIC_PIXEL_ENV_RE) + server_pixel_env_hits = first_hits(root, SERVER_PIXEL_ENV_RE) + shared_pixel_guidance_hits = first_hits(root, SHARED_PIXEL_GUIDANCE_RE, include_docs=True) + oppref_hits = first_hits(root, OPPREF_RE) + source_url_hits = first_hits(root, SOURCE_URL_RE) + event_name_unsupported_hits = unsupported_event_hits(root) + legacy_capi_fields = legacy_capi_field_hits(root) + validate_only_defaults = validate_only_default_hits(root) + has_public_and_server_pixel_config = bool(public_pixel_env_hits and server_pixel_env_hits) + + checks = [ + make_check( + "pixel_marker", + bool(pixel_hits), + pixel_hits, + "Found expected Pixel ID or OpenAI Ads Pixel marker.", + ), + make_check( + "capi_marker", + bool(capi_marker_hits or capi_env_hits), + capi_marker_hits + capi_env_hits, + "Found CAPI marker or expected server-only secret reference.", + ), + make_check( + "dedupe_marker", + bool(event_id_hits), + event_id_hits, + "Found event_id/deduplication/idempotency marker.", + ), + make_check( + "no_public_capi_env", + not public_capi_env_hits, + public_capi_env_hits, + "No browser-visible CAPI/API secret env references found.", + ), + make_check( + "shared_pixel_id_guidance", + not has_public_and_server_pixel_config or bool(shared_pixel_guidance_hits), + public_pixel_env_hits + server_pixel_env_hits + shared_pixel_guidance_hits, + "If browser and server Pixel ID config names both exist, docs/code explain they are the same logical Pixel ID.", + ), + make_check( + "supported_event_names", + not event_name_unsupported_hits, + event_name_unsupported_hits, + "No unsupported OpenAI Ads Pixel event names found in measure calls.", + ), + make_check( + "oppref_marker", + bool(oppref_hits), + oppref_hits, + "Found oppref or __oppref marker for CAPI attribution context.", + ), + make_check( + "source_url_marker", + bool(source_url_hits), + source_url_hits, + "Found source_url/sourceUrl marker for CAPI web source context.", + ), + make_check( + "no_legacy_capi_fields", + not legacy_capi_fields, + legacy_capi_fields, + "No removed legacy CAPI request field names found in OpenAI Ads CAPI files.", + ), + make_check( + "no_validate_only_default", + not validate_only_defaults, + validate_only_defaults, + "Checked-in env templates do not default OpenAI Ads CAPI validate_only mode to true.", + ), + ] + + required_to_check = { + "pixel": "pixel_marker", + "capi": "capi_marker", + "dedupe": "dedupe_marker", + "shared-pixel-id": "shared_pixel_id_guidance", + "supported-events": "supported_event_names", + "oppref": "oppref_marker", + "source-url": "source_url_marker", + } + check_by_name = {str(check["name"]): check for check in checks} + failed_required = [ + required + for required in args.require + if not check_by_name[required_to_check[required]]["passed"] + ] + failed_security = [ + check["name"] + for check in checks + if check["name"] == "no_public_capi_env" and not check["passed"] + ] + failed_correctness = [ + check["name"] + for check in checks + if check["name"] + in { + "supported_event_names", + "no_legacy_capi_fields", + "no_validate_only_default", + } + and not check["passed"] + ] + + result = { + "root": str(root), + "required": args.require, + "checks": checks, + "passed": not failed_required and not failed_security and not failed_correctness, + "failed_required": failed_required, + "failed_security": failed_security, + "failed_correctness": failed_correctness, + } + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 if result["passed"] else 2 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/scripts/verify_capi_secret_not_exposed.py b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/scripts/verify_capi_secret_not_exposed.py new file mode 100644 index 000000000..ce893236e --- /dev/null +++ b/plugins/openai-ads-conversions/skills/openai-ads-conversions-setup/scripts/verify_capi_secret_not_exposed.py @@ -0,0 +1,351 @@ +#!/usr/bin/env python3 +"""Verify OpenAI Ads CAPI credentials are not exposed to browser-visible code.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from pathlib import Path +from typing import Iterable + +SKIP_DIRS = { + ".git", + ".hg", + ".svn", + ".next", + ".nuxt", + ".turbo", + ".venv", + "build", + "coverage", + "dist", + "node_modules", + "out", + "target", + "venv", +} + +SKIP_FILES = { + "verify_capi_secret_not_exposed.py", + "verify_ads_setup.py", +} + +TEXT_EXTENSIONS = { + ".astro", + ".cjs", + ".css", + ".erb", + ".go", + ".html", + ".java", + ".js", + ".json", + ".jsx", + ".mjs", + ".php", + ".py", + ".rb", + ".rs", + ".scss", + ".svelte", + ".ts", + ".tsx", + ".vue", + ".yaml", + ".yml", +} + + +def is_env_file(filename: str) -> bool: + return filename == ".env" or filename.startswith(".env.") + + +CLIENT_HINTS = { + "app", + "assets", + "browser", + "client", + "components", + "frontend", + "pages", + "public", + "src", + "static", + "ui", + "web", +} + +SERVER_ONLY_HINTS = { + "backend", + "server", +} + +SERVER_ROUTE_PATTERNS = { + ("app", "api"), + ("pages", "api"), + ("src", "app", "api"), + ("src", "pages", "api"), +} + +BROWSER_EXTENSIONS = { + ".astro", + ".html", + ".js", + ".jsx", + ".mjs", + ".svelte", + ".ts", + ".tsx", + ".vue", +} + +STATIC_ASSET_EXTENSIONS = { + ".json", +} + +STATIC_ASSET_HINTS = { + "assets", + "public", + "static", +} + +PUBLIC_ENV_RE = re.compile( + r"\b(?:NEXT_PUBLIC|NUXT_PUBLIC|PUBLIC|REACT_APP|VITE)_" + r"(?:[A-Z0-9]+_)*(?:OPENAI_ADS|OPENAI|ADS|CAPI|CONVERSION|CONVERSIONS)" + r"(?:_[A-Z0-9]+)*_(?:API_KEY|KEY|SECRET|TOKEN)[A-Z0-9_]*\b", +) + +OPENAI_ADS_SECRET_RE = re.compile( + r"\b(" + r"OPENAI_ADS_CONVERSIONS_API_KEY|" + r"OPENAI_ADS_CAPI_KEY|" + r"OPENAI_CAPI_KEY|" + r"OPENAI_CONVERSIONS_API_KEY|" + r"CONVERSIONS_API_KEY|" + r"CAPI_API_KEY|" + r"CAPI_SECRET" + r")\b", + re.IGNORECASE, +) + +OPENAI_ADS_SECRET_ASSIGNMENT_RE = re.compile( + r"['\"]?\b(" + r"OPENAI_ADS_CONVERSIONS_API_KEY|" + r"OPENAI_ADS_CAPI_KEY|" + r"OPENAI_CAPI_KEY|" + r"OPENAI_CONVERSIONS_API_KEY|" + r"CONVERSIONS_API_KEY|" + r"CAPI_API_KEY|" + r"CAPI_SECRET" + r")\b['\"]?\s*[:=]\s*['\"]?([A-Za-z0-9_\-./+=]{12,})['\"]?", + re.IGNORECASE, +) +PLACEHOLDER_VALUE_RE = re.compile( + r"(?i)^(process\.env|os\.environ|env\.|import\.meta\.env|your|replace|placeholder|example|todo)" +) + +LITERAL_SECRET_RE = re.compile( + r"(?i)\b(openai|ads|capi|conversion|conversions)[a-z0-9_\-.\s]{0,50}" + r"(api[_\-\s]?key|secret|token)\b\s*[:=]\s*['\"][A-Za-z0-9_\-./+=]{16,}['\"]" +) + +MASK_VALUE_RE = re.compile(r"(['\"])[A-Za-z0-9_\-./+=]{12,}(['\"])") +UNQUOTED_SECRET_VALUE_RE = re.compile( + r"(?i)(\b[A-Z0-9_.-]*(?:API[_-]?KEY|CAPI|SECRET|TOKEN)[A-Z0-9_.-]*\b\s*[:=]\s*)" + r"([^\s#,'\"]{8,})" +) + + +def iter_candidate_files(root: Path) -> Iterable[Path]: + for current_root, dirs, files in os.walk(root): + dirs[:] = [ + directory + for directory in dirs + if directory not in SKIP_DIRS and not (Path(current_root) / directory).is_symlink() + ] + + for filename in files: + if filename in SKIP_FILES: + continue + path = Path(current_root) / filename + if is_env_file(filename) or path.suffix.lower() in TEXT_EXTENSIONS: + yield path + + +def is_client_visible(path: Path, root: Path) -> bool: + rel = path.relative_to(root) + parts = tuple(part.lower() for part in rel.parts) + lower_parts = set(parts) + if path.suffix.lower() in {".html", ".css", ".scss"}: + return True + if path.suffix.lower() in STATIC_ASSET_EXTENSIONS and bool(lower_parts & STATIC_ASSET_HINTS): + return True + if path.suffix.lower() in BROWSER_EXTENSIONS and bool(lower_parts & STATIC_ASSET_HINTS): + return True + # "api" alone is often a browser API wrapper, e.g. src/api/client.ts. + # Only classify well-known framework API route paths as server-only. + if any( + parts[index : index + len(pattern)] == pattern + for pattern in SERVER_ROUTE_PATTERNS + for index in range(len(parts) - len(pattern) + 1) + ): + return False + if bool(lower_parts & SERVER_ONLY_HINTS): + return False + return path.suffix.lower() in BROWSER_EXTENSIONS and bool(lower_parts & CLIENT_HINTS) + + +def masked_context(line: str) -> str: + masked = MASK_VALUE_RE.sub(r"\1***\2", line.strip()) + masked = UNQUOTED_SECRET_VALUE_RE.sub(r"\1***", masked) + return masked[:240] + + +def has_literal_openai_ads_secret_assignment(line: str) -> bool: + match = OPENAI_ADS_SECRET_ASSIGNMENT_RE.search(line) + if not match: + return False + return PLACEHOLDER_VALUE_RE.search(match.group(2)) is None + + +def maybe_add_context( + finding: dict[str, object], line: str, include_context: bool +) -> dict[str, object]: + if include_context: + finding["context"] = masked_context(line) + return finding + + +def scan_file(path: Path, root: Path, include_context: bool) -> list[dict[str, object]]: + try: + text = path.read_text(encoding="utf-8", errors="ignore") + except OSError: + return [] + + client_visible = is_client_visible(path, root) + findings: list[dict[str, object]] = [] + + for line_number, line in enumerate(text.splitlines(), start=1): + public_env_match = PUBLIC_ENV_RE.search(line) + if public_env_match: + findings.append( + maybe_add_context( + { + "severity": "high", + "rule": "public_env_secret_name", + "path": str(path.relative_to(root)), + "line": line_number, + "message": "Potential CAPI/API secret uses a browser-visible env var prefix.", + }, + line, + include_context, + ) + ) + + secret_name_match = OPENAI_ADS_SECRET_RE.search(line) + if secret_name_match and client_visible: + findings.append( + maybe_add_context( + { + "severity": "high", + "rule": "capi_secret_name_in_client_file", + "path": str(path.relative_to(root)), + "line": line_number, + "message": "Potential OpenAI Ads CAPI secret reference appears in client-visible code.", + }, + line, + include_context, + ) + ) + + if has_literal_openai_ads_secret_assignment(line): + findings.append( + maybe_add_context( + { + "severity": "high", + "rule": "literal_capi_secret_assignment", + "path": str(path.relative_to(root)), + "line": line_number, + "message": "Potential literal OpenAI Ads CAPI credential assignment found.", + }, + line, + include_context, + ) + ) + + literal_match = LITERAL_SECRET_RE.search(line) + if literal_match: + findings.append( + maybe_add_context( + { + "severity": "high", + "rule": "literal_secret_like_assignment", + "path": str(path.relative_to(root)), + "line": line_number, + "message": "Potential literal conversion API credential assignment found.", + }, + line, + include_context, + ) + ) + + return findings + + +def exit_code(findings: list[dict[str, object]], fail_on: str) -> int: + if fail_on == "none": + return 0 + severities = {str(finding["severity"]) for finding in findings} + if fail_on == "high" and "high" in severities: + return 2 + if fail_on == "medium" and severities & {"high", "medium"}: + return 1 + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Verify OpenAI Ads CAPI credentials are not exposed to browser-visible code." + ) + parser.add_argument("root", nargs="?", default=".", help="Repository root to verify.") + parser.add_argument( + "--include-context", + action="store_true", + help="Include masked source-line context in findings. By default, only paths, line numbers, rules, and summaries are reported.", + ) + parser.add_argument( + "--fail-on", + choices=("high", "medium", "none"), + default="high", + help="Minimum severity that should produce a non-zero exit code.", + ) + args = parser.parse_args() + + root = Path(args.root).resolve() + findings: list[dict[str, object]] = [] + for path in iter_candidate_files(root): + findings.extend(scan_file(path, root, args.include_context)) + + result = { + "root": str(root), + "output_policy": { + "runs_locally": True, + "transmits_repository_contents": False, + "source_context_included": args.include_context, + }, + "summary": { + "total": len(findings), + "high": sum(1 for finding in findings if finding["severity"] == "high"), + "medium": sum(1 for finding in findings if finding["severity"] == "medium"), + }, + "findings": findings, + } + print(json.dumps(result, indent=2, sort_keys=True)) + return exit_code(findings, args.fail_on) + + +if __name__ == "__main__": + sys.exit(main())