diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e3966f0..8a44d9ce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,6 +123,34 @@ jobs: headers: | x-goog-meta-optable-sdk-version: ${{ github.ref_name }} + deploy-pub-template-to-gcs: + needs: [deploy-sdk-to-npm, define-gcs-versions-to-update] + strategy: + matrix: + sdk-version: ${{ fromJSON(needs.define-gcs-versions-to-update.outputs.sdk-versions) }} + runs-on: ubuntu-22.04 + permissions: + contents: read + id-token: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Auth to google cloud + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ env.workload_identity_provider }} + service_account: ${{ env.service-account }} + + - name: Upload pub.ts template to GCS bucket + uses: google-github-actions/upload-cloud-storage@v2 + with: + path: "browser/pub.ts" + destination: "optable-web-sdk/pub-sdk/${{ matrix.sdk-version }}" + process_gcloudignore: false + headers: | + x-goog-meta-optable-sdk-version: ${{ github.ref_name }} + deploy-demo: needs: [deploy-sdk-to-npm] permissions: @@ -181,7 +209,16 @@ jobs: run: docker push us-docker.pkg.dev/optable-artifact-registry/optable/optable-web-sdk-demos:${{ github.ref_name }} slack-notification: - needs: [tests-prettier, build, deploy-sdk-to-npm, define-gcs-versions-to-update, deploy-sdk-to-gcs, deploy-demo] + needs: + [ + tests-prettier, + build, + deploy-sdk-to-npm, + define-gcs-versions-to-update, + deploy-sdk-to-gcs, + deploy-pub-template-to-gcs, + deploy-demo, + ] runs-on: ubuntu-22.04 if: ${{ failure() }} steps: diff --git a/.github/workflows/reusable-lint-test.yml b/.github/workflows/reusable-lint-test.yml index 655d11d6..dfb84ddd 100644 --- a/.github/workflows/reusable-lint-test.yml +++ b/.github/workflows/reusable-lint-test.yml @@ -14,6 +14,27 @@ jobs: - name: Test run: pnpm test + validate-pub-template: + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check if pub template files changed + id: changed + run: | + if git diff --name-only origin/master...HEAD | grep -qE '^(browser/pub\.ts|scripts/validate-pub-template\.sh)$'; then + echo "pub_changed=true" >> "$GITHUB_OUTPUT" + else + echo "pub_changed=false" >> "$GITHUB_OUTPUT" + fi + + - name: Validate pub.ts template + if: steps.changed.outputs.pub_changed == 'true' + run: ./scripts/validate-pub-template.sh + prettier: runs-on: ubuntu-22.04 steps: diff --git a/.prettierignore b/.prettierignore index 7c81f7a7..492f6364 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,9 @@ # build outputs browser/dist + +# Go text/template file (not valid TypeScript) +browser/pub.ts lib/dist # demos diff --git a/browser/pub.ts b/browser/pub.ts new file mode 100644 index 00000000..fb588dac --- /dev/null +++ b/browser/pub.ts @@ -0,0 +1,150 @@ +// browser/pub.ts — Go text/template + TypeScript +// This file is processed by Go text/template BEFORE esbuild compilation. +// It is NOT valid TypeScript until template directives are resolved. +// +// Template variables: +// .Host - Edge API hostname (e.g. "ca.edge.optable.co") +// .Site - Site slug derived from origin +// .Node - Node slug derived from tenant +// .Timeout - Optional timeout hint (e.g. "500ms", "2s") +// .PrebidGlobal - Prebid.js global variable name (e.g. "pbjs", "__pmc_atlasmg_pbjs") +// .TcfeuVendorID - Optional TCF EU vendor ID (GVLID) for consent checks +// .TcfcaVendorID - Optional TCF CA vendor ID (GVLID) for consent checks +// .EnableSecureSignals - Boolean: enable GPT Secure Signals +// .EnableBotDetection - Boolean: enable bot detection +// .EnableAnalytics - Boolean: enable Prebid analytics +// .GeoTargeting - "regional" or "multi-region" + +import OptableSDK from "@optable/web-sdk"; +{{if .EnableAnalytics}} +import OptablePrebidAnalytics from "@optable/web-sdk/lib/dist/addons/prebid/analytics"; +{{end}} + +declare global { + interface Window { + optable?: Record; + } +} + +// --- Commands queue (executes queued functions immediately) --- +class OptableCommands { + constructor(cmds: OptableCommands | Function[]) { + if (Array.isArray(cmds)) { + for (const cmd of cmds) { + if (typeof cmd === "function") cmd(); + } + } + } + push(cmd: Function) { + cmd(); + } +} + +// --- Debug logging --- +function optableMessage(...args: unknown[]) { + if (sessionStorage.optableDebug) { + console.log("[OPTABLE WRAPPER]", ...args); + } +} + +// --- URL query feature flags --- +function setOptableFlagsFromURLQuery() { + const keys = ["optableDebug", "optableDisableConsent", "optableResolveTD", "optableEnableAnalytics"]; + const search = window.location.search || ""; + keys.forEach((key) => { + if (search.match(new RegExp(key, "g"))) { + sessionStorage.setItem(key, "1"); + } + }); +} + +// --- Consent handling --- +function getConsent() { + if (sessionStorage.optableDisableConsent) { + return { + createProfilesForAdvertising: true, + deviceAccess: true, + measureAdvertisingPerformance: true, + reg: null, + useProfilesForAdvertising: true, + }; + } + return { cmpapi: { + {{- if .TcfeuVendorID}} + tcfeuVendorID: {{.TcfeuVendorID}}, + {{- end}} + {{- if .TcfcaVendorID}} + tcfcaVendorID: {{.TcfcaVendorID}}, + {{- end}} + } }; +} + +{{if .EnableAnalytics}} +// --- Analytics initialization --- +function initPrebidAnalytics(sdk: OptableSDK) { + const pbjsObject = (window as any)["{{.PrebidGlobal}}"] || (window as any)["pbjs"]; + if (!pbjsObject) return; + + const analyticsSDK = new OptableSDK({ + host: "{{.Host}}", + node: "analytics", + site: "analytics", + readOnly: true, + cookies: false, + }); + + const analytics = new OptablePrebidAnalytics(analyticsSDK, { + analytics: true, + tenant: "{{.Node}}", + debug: !!sessionStorage.optableDebug, + samplingRate: 0.1, + }); + + analytics.hookIntoPrebid(pbjsObject); +} +{{end}} + +// --- Main execution --- +(function run() { + setOptableFlagsFromURLQuery(); + + window.optable = window.optable || {}; + window.optable.SDK = OptableSDK; + + const consent = getConsent(); + const sdk = new OptableSDK({ + host: "{{.Host}}", + site: "{{.Site}}", + node: "{{.Node}}", + cookies: false, + consent, + initPassport: false, + {{- if .Timeout}} + timeout: "{{.Timeout}}", + {{- end}} + }); + + window.optable["{{.Node}}_instance"] = sdk; + + {{if .EnableSecureSignals}} + optableMessage("Enabling secure signals"); + sdk.installGPTSecureSignals(); + {{end}} + + {{if .EnableBotDetection}} + optableMessage("Enabling bot detection"); + (sdk as any).enableBotDetection(); + {{end}} + + {{if eq .GeoTargeting "multi-region"}} + optableMessage("Multi-region mode: reading window.optable.countryCode"); + (sdk as any).setCountryCodeFromGlobal(); + {{end}} + + {{if .EnableAnalytics}} + initPrebidAnalytics(sdk); + {{end}} + + window.optable.cmd = new OptableCommands(window.optable.cmd || []); + optableMessage("SDK initialized"); +})(); diff --git a/scripts/validate-pub-template.sh b/scripts/validate-pub-template.sh new file mode 100755 index 00000000..dc3928d8 --- /dev/null +++ b/scripts/validate-pub-template.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Validates that browser/pub.ts renders to compilable TypeScript. +# +# This script simulates Go text/template rendering by replacing template +# directives with sample values, then compiles the result with esbuild +# to catch syntax errors before they reach a release. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +TEMPLATE="${REPO_ROOT}/browser/pub.ts" +WORKDIR="$(mktemp -d)" + +trap 'rm -rf "${WORKDIR}"' EXIT + +echo "Validating pub.ts template..." + +# Render template with sample config values. +# Features that depend on SDK methods not yet implemented are disabled. +rendered="${WORKDIR}/pub.ts" + +sed \ + -e '/{{if/d' \ + -e '/{{end}}/d' \ + -e 's/{{- if [^}]*}}//' \ + -e 's/{{- end}}//' \ + -e 's/{{\.Host}}/sample.edge.example.co/g' \ + -e 's/{{\.Site}}/sample-site/g' \ + -e 's/{{\.Node}}/sample-node/g' \ + -e 's/{{\.PrebidGlobal}}/pbjs/g' \ + -e 's/{{\.Timeout}}/500ms/g' \ + -e 's/{{\.TcfeuVendorID}}/123/g' \ + -e 's/{{\.TcfcaVendorID}}/456/g' \ + "${TEMPLATE}" > "${rendered}" + +echo "Rendered template to ${rendered}" + +# Compile with esbuild to verify the output is valid TypeScript. +# @optable/web-sdk is marked external since it's resolved at bundle time (Phase 5). +npx --yes esbuild "${rendered}" \ + --bundle \ + --format=iife \ + --platform=browser \ + --target=es2015 \ + --external:@optable/web-sdk \ + --external:@optable/web-sdk/* \ + --outfile="${WORKDIR}/pub.js" + +echo "Template validation passed: pub.ts renders to compilable TypeScript."