-
Notifications
You must be signed in to change notification settings - Fork 61
test: add playwright script that proxies Amplitude scripts to test customer pages #1682
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
daniel-graham-amplitude
wants to merge
11
commits into
main
Choose a base branch
from
ai-week-playwright-mcp
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
95bd0e8
add playwright script that proxies unified scripts on customer pages
daniel-graham-amplitude d29debd
add manual testing skill
daniel-graham-amplitude 9f50717
rewrite cdn scripts
daniel-graham-amplitude 04c1246
rewrite cdn scripts
daniel-graham-amplitude c3a4b9b
again
daniel-graham-amplitude 89672c5
again
daniel-graham-amplitude 7537ebb
again
daniel-graham-amplitude b075e10
again
daniel-graham-amplitude e4cc039
again
daniel-graham-amplitude d881865
again
daniel-graham-amplitude f7f5ca8
again
daniel-graham-amplitude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| --- | ||
| name: amplitude-typescript-repo | ||
| description: Contributes to the Amplitude TypeScript analytics SDK monorepo (pnpm workspaces, Lerna, Nx). Covers PR prep, local test-server workflow, and customer-site manual tests via e2e/manual-test.js. Use when changing packages under packages/, running repo scripts, preparing PRs, testing a live site against a local bundle, or when the user mentions Amplitude-TypeScript, browser/node SDKs, manual-test, or customer-site validation. | ||
| --- | ||
|
|
||
| # Amplitude TypeScript monorepo | ||
|
|
||
| ## Layout | ||
|
|
||
| - **Package manager**: `pnpm` with workspaces (`packages/*`). | ||
| - **Build orchestration**: Lerna (`pnpm build`, `pnpm test`, `pnpm lint` stream across packages); Nx available for affected/graph targets (`package.json` scripts with `nx` prefix). | ||
| - **Source**: SDK and plugin code lives under `packages/` (for example `analytics-browser`, `analytics-core`, `analytics-node`). | ||
|
|
||
| ## Before opening a PR (match CI) | ||
|
|
||
| Run in order after substantive changes: | ||
|
|
||
| 1. `pnpm install` | ||
| 2. `pnpm build` | ||
| 3. `pnpm docs:check` | ||
| 4. `pnpm test` and `pnpm test:examples` | ||
| 5. `pnpm lint` | ||
|
|
||
| Full contributor notes: [AGENTS.md](../../../AGENTS.md) at repo root. | ||
|
|
||
| ## Environment | ||
|
|
||
| - CI uses Node.js **18.17.x**, **20.x**, and **22.x** — avoid APIs or syntax that break that range. | ||
|
|
||
| ## PR titles | ||
|
|
||
| Use [Conventional Commits](https://www.conventionalcommits.org/) and include the affected module when it helps, for example `feat(browser): …`, `fix(plugin): …`. | ||
|
|
||
| ## Scoped commands | ||
|
|
||
| - Prefer root scripts above for consistency. | ||
| - To target one package: `pnpm --filter <package-name> <script>` (see each package’s `package.json` for local script names). | ||
|
|
||
| ## Change discipline | ||
|
|
||
| - Keep diffs focused on the requested behavior; avoid unrelated refactors or new docs unless asked. | ||
| - Match existing patterns in the touched package (imports, types, test style). | ||
|
|
||
| ## Customer site manual test of analytics (`e2e/manual-test.js`) | ||
|
|
||
| **Tell the user up front:** this flow only applies to sites that load Amplitude’s **unified script** or that use cdn.amplitude.com tags. | ||
|
|
||
| ### Why `dev:ssh` | ||
|
|
||
| `e2e/manual-test.js` rewrites the CDN request to `https://local.website.com:5173/unified-script-local.js`. The Vite test server must be reachable at that **HTTPS** origin. Use `pnpm dev:ssh` after the one-time HTTPS setup in [test-server/README.md](../../../test-server/README.md) (`/etc/hosts`, `generate-signed-cert`, trust cert in Keychain). If that setup is missing, the manual test will not load the local bundle. If the user struggles to find the script, make sure they ran `dev:ssh` first. | ||
|
|
||
| ### Run the manual test | ||
|
|
||
| `node ./e2e/manual-test.js <website-url>`. Example: `node ./e2e/manual-test.js https://example.com` | ||
|
|
||
| Playwright opens a headed browser, proxies HTML to strip SRI on Amplitude script tags, and swaps the unified script for the local `test-server/unified-script-local.js` chain. Leave Terminal A running until finished; stop with Ctrl+C in each terminal when done. | ||
|
|
||
| ### Integrity Hashes | ||
|
|
||
| If the user experiences a problem with the proxying not working due to SRI failures, instruct the user to take the shasum of the failing integrity hash and add it to manual-test.js under INTEGRITY_HASHES. | ||
|
|
||
| ### Testing specific versions | ||
|
|
||
| Aside from testing local versions, users need to be able to go back and test old versions of the analytics or session replay SDK. If the user asks to test a specific version (e.g.: session replay v1.22.7) than set the environment variable (e.g.: SESSION_REPLAY_VERSION) to have it set to that instead of using local | ||
|
|
||
| ### Building before testing | ||
|
|
||
| Don't build bundles before testing. Leave that up to the user. But notify them if they're having troubles that they may be using a stale bundle. | ||
|
|
||
| ### Manual tests not automatic | ||
|
|
||
| These tests should be manual. The manual script opens the browser and the user tests. Never use HEADLESS and always leave the script running indefinitely |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| // Headed Playwright session: proxy publisher pages to load local Amplitude bundles (see test-server README + pnpm dev:ssh). | ||
| import { chromium } from 'playwright'; | ||
|
|
||
| // Get URL from command line args | ||
| const targetUrl = process.argv[2]; | ||
|
|
||
| if (!targetUrl) { | ||
| console.error('Usage: node ./e2e/manual-test.js <website-url>'); | ||
| console.error('Example: node ./e2e/manual-test.js https://example.com'); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| // Integrity Hashes to rip out | ||
| // (RATIONALE: proxying fails if the integrity hash is set so just remove them | ||
| // from all HTML and JS responses for testing only) | ||
| // when you encounter a new integrity hash that's failing, just add it here | ||
| const INTEGRITY_HASHES = [ | ||
| 'sha384-7OMex1WYtzbDAdKl8HtBEJJB+8Yj6zAJRSeZhWCSQmjLGr4H2OBdrKtiw8HEhwgI', | ||
| 'sha384-1JFhJprHbtX4G26DXID9oEguDxAc6L0h+pxDQaCvp4eIQuAtu0kWQWbJVdkx+k1x', | ||
| 'sha384-tO0IrD5wYnaoQXROJVMmDUd7cp41nJ8GVLSjquFPrzzmYLdTiy5ePe8jbADN3UTJ', | ||
| ]; | ||
|
|
||
| function dropEncodingHeaders(headers) { | ||
| const out = { ...headers }; | ||
| for (const key of Object.keys(out)) { | ||
| const lower = key.toLowerCase(); | ||
| if (lower === 'content-encoding' || lower === 'content-length' || lower === 'transfer-encoding') { | ||
| delete out[key]; | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
|
|
||
| const SESSION_REPLAY_VERSION = process.env.SESSION_REPLAY_VERSION; | ||
| const ANALYTICS_BROWSER_VERSION = process.env.ANALYTICS_BROWSER_VERSION; | ||
|
|
||
| async function main() { | ||
| const browser = await chromium.launch({ headless: false }); | ||
| const context = await browser.newContext(); | ||
|
|
||
| await context.route('**/*', async (route) => { | ||
| const url = route.request().url(); | ||
| if ( | ||
| url.includes('cdn.amplitude.com/script/') && | ||
| !url.includes('.async.js') | ||
| ) { | ||
| console.log('Rerouting analytics-browser bundle'); | ||
| const redirectedUrl = 'https://local.website.com:5173/unified-script-local.js'; | ||
| return route.continue({ url: redirectedUrl }); | ||
| } | ||
|
|
||
| if( | ||
| url.includes('cdn.amplitude.com/libs/analytics-browser-gtm-') | ||
| ) { | ||
| console.log('Rerouting analytics-browser-gtm bundle'); | ||
| const redirectedUrl = ANALYTICS_BROWSER_VERSION | ||
| ? `https://cdn.amplitude.com/libs/analytics-browser-gtm-${ANALYTICS_BROWSER_VERSION}-min.js.gz` | ||
| : 'https://local.website.com:5173/analytics-browser/lib/scripts/amplitude-gtm-min.js.gz'; | ||
| return route.continue({ url: redirectedUrl }); | ||
| } | ||
|
|
||
| if( | ||
| url.includes('cdn.amplitude.com/libs/analytics-browser-') | ||
| ) { | ||
| console.log('Rerouting analytics-browser bundle'); | ||
| const redirectedUrl = ANALYTICS_BROWSER_VERSION | ||
| ? `https://cdn.amplitude.com/libs/analytics-browser-${ANALYTICS_BROWSER_VERSION}-min.js.gz` | ||
| : 'https://local.website.com:5173/analytics-browser/lib/scripts/amplitude-min.js.gz'; | ||
| return route.continue({ url: redirectedUrl }); | ||
| } | ||
|
|
||
| if( | ||
| url.includes('cdn.amplitude.com/libs/plugin-session-replay-browser-') | ||
| ) { | ||
| console.log('Rerouting plugin-session-replay-browser bundle'); | ||
| const redirectedUrl = SESSION_REPLAY_VERSION | ||
| ? `https://cdn.amplitude.com/libs/plugin-session-replay-browser-${SESSION_REPLAY_VERSION}-min.js.gz` | ||
| : 'https://local.website.com:5173/plugin-session-replay-browser/lib/scripts/plugin-session-replay-browser-min.js.gz'; | ||
| return route.continue({ url: redirectedUrl }); | ||
| } | ||
|
|
||
| const AMPLITUDE_API_KEY = process.env.AMPLITUDE_API_KEY; | ||
|
|
||
| if ( | ||
| url.includes('amplitude.com/2/httpapi') && | ||
| AMPLITUDE_API_KEY | ||
| ) { | ||
| // Change API Key to your own local API Key | ||
| const request = route.request(); | ||
| const postData = request.postDataJSON(); | ||
| const modifiedPostData = { ...postData, api_key: AMPLITUDE_API_KEY }; | ||
|
|
||
| // re-direct to EU API endpoint | ||
| const modifiedUrl = url.replace('api.eu.amplitude.com', 'api.amplitude.com'); | ||
|
|
||
| return await route.continue({ url: modifiedUrl, postData: JSON.stringify(modifiedPostData) }); | ||
| } | ||
|
|
||
| // GTM injects Amplitude loader tags with SRI inside JS; strip so rerouted local bytes validate. | ||
| if ( | ||
| route.request().resourceType() === 'script' && | ||
| url.includes('googletagmanager.com') | ||
| ) { | ||
| try { | ||
| const response = await route.fetch(); | ||
| let body = await response.text(); | ||
| for (const hash of INTEGRITY_HASHES) { | ||
| body = body.replaceAll(hash, ''); | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| await route.fulfill({ | ||
| status: response.status(), | ||
| headers: dropEncodingHeaders(response.headers()), | ||
| body, | ||
| }); | ||
| return; | ||
| } catch (e) { | ||
| console.warn('GTM script rewrite failed, passing through:', url, e.message); | ||
| return route.continue(); | ||
| } | ||
| } | ||
|
|
||
| if (route.request().resourceType() !== 'document') { | ||
| return route.continue(); | ||
| } | ||
| // Only rewrite top-level document navigations. Subframe "document" loads (ads, sync pixels) | ||
| // often fail or reset on `route.fetch()` and do not need SRI stripping for our Amplitude swap. | ||
| if (route.request().frame().parentFrame() !== null) { | ||
| return route.continue(); | ||
| } | ||
| let response; | ||
| try { | ||
| response = await route.fetch(); | ||
| } catch (e) { | ||
| console.warn('route.fetch failed, passing through:', route.request().url(), e.message); | ||
| return route.continue(); | ||
| } | ||
| let content = await response.text(); | ||
| for (const hash of INTEGRITY_HASHES) { | ||
| content = content.replaceAll(hash, ''); | ||
| } | ||
|
|
||
| const ct = (response.headers()['content-type'] || '').toLowerCase(); | ||
| if (!ct.includes('text/html')) { | ||
| return route.fulfill({ | ||
| status: response.status(), | ||
| headers: dropEncodingHeaders(response.headers()), | ||
| body: content, | ||
| }); | ||
| } | ||
| let html = content; | ||
|
|
||
| for (const hash of INTEGRITY_HASHES) { | ||
| html = html.replaceAll(hash, ''); | ||
| } | ||
|
daniel-graham-amplitude marked this conversation as resolved.
|
||
|
|
||
| await route.fulfill({ | ||
| status: response.status(), | ||
| headers: dropEncodingHeaders(response.headers()), | ||
| body: html, | ||
| }); | ||
| }); | ||
|
|
||
| const page = await context.newPage(); | ||
|
|
||
| console.log(`\nNavigating to: ${targetUrl}\n`); | ||
| // `networkidle` often never settles on publisher sites (ads, analytics, long-poll). | ||
| await page.goto(targetUrl, { | ||
| waitUntil: 'load', | ||
| timeout: 0, | ||
| }); | ||
| console.log('\nPage loaded. Browser will stay open. Press Ctrl+C to exit.'); | ||
|
|
||
| // Keep process alive | ||
| process.on('SIGINT', async () => { | ||
| console.log('\nClosing browser...'); | ||
| await browser.close(); | ||
| process.exit(0); | ||
| }); | ||
| } | ||
|
|
||
| main(); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.