E2E Test
crowdin/crowdin-api-client-js
Run end-to-end tests against the real Crowdin or Crowdin Enterprise API using credentials from .env.
End-to-end test a device-mode integration change locally. An agent skill from rudderlabs/rudder-sdk-js.
$ npx skills add rudderlabs/rudder-sdk-js --skill device-mode-e2e -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install rudderlabs/rudder-sdk-js device-mode-e2e --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/rudderlabs/rudder-sdk-js.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/device-mode-e2e .claude/skills/device-mode-e2e && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "device-mode-e2e" agent skill from https://github.com/rudderlabs/rudder-sdk-js/tree/develop/.claude/skills/device-mode-e2e into .claude/skills/device-mode-e2e/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "device-mode-e2e", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/rudderlabs/rudder-sdk-js/tree/develop/.claude/skills/device-mode-e2eType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add rudderlabs/rudder-sdk-js --skill device-mode-e2e -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install rudderlabs/rudder-sdk-js device-mode-e2e --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/rudderlabs/rudder-sdk-js.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/device-mode-e2e .agents/skills/device-mode-e2e && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "device-mode-e2e" agent skill from https://github.com/rudderlabs/rudder-sdk-js/tree/develop/.claude/skills/device-mode-e2e into .agents/skills/device-mode-e2e/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "device-mode-e2e", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add rudderlabs/rudder-sdk-js --skill device-mode-e2e -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install rudderlabs/rudder-sdk-js device-mode-e2e --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/rudderlabs/rudder-sdk-js.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/device-mode-e2e .cursor/skills/device-mode-e2e && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "device-mode-e2e" agent skill from https://github.com/rudderlabs/rudder-sdk-js/tree/develop/.claude/skills/device-mode-e2e into .cursor/skills/device-mode-e2e/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "device-mode-e2e", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/rudderlabs/rudder-sdk-js.git --path .claude/skills/device-mode-e2e--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add rudderlabs/rudder-sdk-js --skill device-mode-e2e -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install rudderlabs/rudder-sdk-js device-mode-e2e --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/rudderlabs/rudder-sdk-js.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/device-mode-e2e .gemini/skills/device-mode-e2e && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "device-mode-e2e" agent skill from https://github.com/rudderlabs/rudder-sdk-js/tree/develop/.claude/skills/device-mode-e2e into .gemini/skills/device-mode-e2e/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "device-mode-e2e", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install rudderlabs/rudder-sdk-js device-mode-e2eInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add rudderlabs/rudder-sdk-js --skill device-mode-e2e -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/rudderlabs/rudder-sdk-js.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/device-mode-e2e .github/skills/device-mode-e2e && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "device-mode-e2e" agent skill from https://github.com/rudderlabs/rudder-sdk-js/tree/develop/.claude/skills/device-mode-e2e into .github/skills/device-mode-e2e/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "device-mode-e2e", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add rudderlabs/rudder-sdk-js --skill device-mode-e2e -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install rudderlabs/rudder-sdk-js device-mode-e2e --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/rudderlabs/rudder-sdk-js.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/device-mode-e2e .opencode/skills/device-mode-e2e && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "device-mode-e2e" agent skill from https://github.com/rudderlabs/rudder-sdk-js/tree/develop/.claude/skills/device-mode-e2e into .opencode/skills/device-mode-e2e/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "device-mode-e2e", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
device-mode-e2eEnd-to-end test a device-mode integration change locally. An agent skill from rudderlabs/rudder-sdk-js.
Device Mode E2E is an agent skill from rudderlabs/rudder-sdk-js. End-to-end test a device-mode integration change locally. Serves the SDK (local build or a released CDN), derives targeted events from the integration's implementation, generates a self-asserting HTML harness that loads the REAL connection from a write key alone, and runs it headed or headless. Reports a generic PASS/FAIL - SDK fetched sourceConfig, loaded the integration bundle, forwarded events, and actually sent data to the destination - plus a check that the destination config arrived in the SDK with the…
Its SKILL.md is about 6.3k tokens, which your agent loads only when the skill is triggered. The skill folder holds 8 other files (for example `harness/README.md`).
It sits in Testing & QA, covering End-to-end testing. It works with JavaScript. The repository describes itself as: JavaScript SDK for RudderStack - the Customer Data Platform for Developers. The licence is MIT.
9 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 8b8757a. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships script files (JavaScript), which the agent can run.
Shell commands in SKILL.md call:
nodenpmnvmnpxgitcurljqFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
cdn.staging.rudderlabs.comcdn.rudderlabs.comcdn.dev.rudderlabs.comapi.rudderstack.comFrom URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
WRITE_KEYFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Device Mode E2E loads about 6.3k tokens when it runs. Until then it costs about 234 tokens; SKILL.md has 2,797 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from rudderlabs/rudder-sdk-js at commit 8b8757a, republished under its MIT licence (© rudderlabs). 2,797 words, ~6,253 tokens.
.claude/skills/device-mode-e2e/SKILL.md (or your agent's skills folder). This skill also uses 7 other files; get the full folder from GitHub.Objective: remove the manual effort of hand-writing an HTML page, opening it, and reading the Network tab to check whether a device-mode integration change actually forwards events. Given a write key alone, this harness builds and serves the SDK, fires targeted events at the real connection, and reports a generic PASS/FAIL. It is destination-agnostic: no integration-specific verification is baked in.
Generic, automated (any device-mode integration):
the SDK fetched the real sourceConfig from the write key and got a 2xx,
it loaded the device-mode integration bundle (/js-integrations/),
ready() fired and every event was forwarded without throwing,
DATA was actually sent to a third-party (non-localhost, non-RudderStack) host — a fetch/XHR/
beacon/pixel, not merely a native-SDK <script> load. "native SDK asset loaded" is reported
separately as info; only "data sent" counts as delivery.
the destination config arrived in the SDK with the shape the control-plane contract says — for
each key rudder-integrations-config lists for a web device-mode connection and that was
delivered, the value has the expected type. A key stored per source type ({ web: … }) must
arrive resolved to its inner value, not as an object; this catches delivery bugs the SDK would
otherwise swallow. Reported as warnings by default; configContract: "strict" fails the run on a
type mismatch.
Keys that are listed but absent are reported as missing and are informational only — most
destination settings are optional, so an unset one legitimately never reaches the SDK. Keys
delivered but not listed appear as unexpected, also informational.
Schema conformance is not checked here — that is rudder-integrations-config's own CI job.
Captured console/page errors (console.error, onerror, unhandledrejection) are shown as
warnings — they do not fail the run (third-party SDKs log noise).
It also SHOWS you the actual outgoing requests — method, URL, query params, and body — so you can
confirm your fix put the right value in the payload (say, the SDK's messageId arriving as the
destination's own event id), not just that a request fired. You can optionally add generic expectations to turn a specific
value check into an automated PASS/FAIL (see step 5 and step 7).
Manual (out of scope — inherently per-destination): confirming the events arrived inside the destination's dashboard. The harness ends by reminding you to check it. Do not try to automate this in the generic skill.
Before building or running anything, ask the user and wait for their answers:
https://cdn.staging.rudderlabs.com) / production (https://cdn.rudderlabs.com) /
dev (https://cdn.dev.rudderlabs.com) — the released SDK for that write key. No build or
serve needed — skip steps 2 & 3 (the step-1 preflight still applies). Use this to reproduce a customer issue or verify a config-only
change against live code.https://… base; ask if they want one.Present those defaults and let them pick (or name a custom base). Set cdn (and optional variant:
modern/legacy) in the config accordingly; local keeps the explicit localhost URLs.
<destination> dashboard" summary). Best for minimal /
targeted changes (e.g. eventId-from-messageId).You may recommend based on the change ("minimal → Report", "config-only → production CDN"), but the
user decides. Only skip a question if they already answered it (or said "just pick"). Do not
silently default to Report, do not run headless without being asked, and do not assume local
if they might want to test the released SDK — surface the choice.
packages/analytics-js-integrations/src/integrations/. Used to locate the source for test-case
derivation and to match the loaded bundle name. Never used for verification logic.v3 (default) or v1.1.The repo's Node version — pinned in .nvmrc at the repo root. The headless runner needs the
global WebSocket (Node ≥ 22), and your shell may default to an older Node; switch first and
verify:
nvm use && node -v # picks up .nvmrc; or point the scripts at an explicit Node >= 22 binaryChrome/Chromium installed. The runner probes standard locations; if it can't find yours, set
CHROME_PATH. On macOS the default is:
export CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"If Node ≥ 22 or Chrome is unavailable, use the zero-dep fallback: serve the directory that
holds only the generated page (<temp>/page, see step 6) with any static server, open it in your
own browser, click ▶ Run tests, and read the on-page PASS/FAIL table — no runner needed.
(run-cdp.mjs is only for the automated report flow.)
Never point a directory server at the dir holding the run config or
preflight.json. A native destination SDK loaded by the page runs in the page's origin, so it could fetch those sibling files and read the write key / destination config.run-cdp.mjsserves the single HTML file for exactly this reason; a plain static server does not, so give it a directory with nothing else in it.
Do this first — it catches "zero destinations" in seconds and is cache-busted (so it reflects a just-connected destination, not a stale control-plane copy):
node .claude/skills/device-mode-e2e/harness/preflight.mjs \
--writeKey <WRITE_KEY> --integration <Integration> --json <temp>/preflight.jsonExit 0 = connected & enabled. Exit 1 = not connected / disabled / zero destinations — fix the
connection first. (Add --configUrl for a non-default control plane.)
--json writes the resolved destinations — id, enabled, connectionMode and their live config.
It is required only for a config override (step 5), but always worth passing: the printed id is
what an override is keyed by.
Use this layout for the temp files (it keeps secrets off the page's own origin — see Security):
<temp>/preflight.json # destination config — NOT served
<temp>/run.json # run config, embeds the write key — NOT served
<temp>/page/harness.html # the only file any server is pointed atcdn: localSkip this entire step (and step 3) when the user chose a CDN (staging/production/dev/custom) — the SDK, integrations, and plugins all load from that CDN, so there's nothing to build or serve. Go straight to derive test cases (step 4) → config with
cdn(step 5) → run.
A v3 local run needs three servers. Run each in its own terminal (they keep watching/serving).
Use the core/v1.1 :no-open scripts so their dev servers serve without opening a demo tab on top of
the harness window (plain start/start:modern still open one):
# 1) core SDK → http://localhost:3001 (rsa.min.js under /cdn/<variant>/iife/)
cd packages/analytics-js && npm run start:modern:no-open
# 2) integrations → http://localhost:3005 (bundles under /cdn/<variant>/js-integrations/)
cd packages/analytics-js-integrations && npm run start:modern
# 3) plugins → http://localhost:3002 (rsa-plugins.js under /cdn/<variant>/plugins/)
cd packages/analytics-js-plugins && npm run start:modernWhy the plugins server is required (this is the #1 gotcha). With the CDN snippet, when
pluginsSDKBaseURLis omitted the SDK does not fall back to the public CDN — it derives the plugins URL from where the core script was loaded (localhost:3001), which does not serve plugins. Every plugin then silently fails to load — including the device-mode-destinations plugin, so no integration bundle is ever requested. You must serve the plugins package and setpluginsSDKBaseURL. (Same mechanism applies todestSDKBaseURLfor integrations.)
Faster iteration: the integrations start rebuilds every bundle (minutes). To rebuild just one,
use the targeted CLI build, then serve dist:
cd packages/analytics-js-integrations
BROWSERSLIST_ENV=modern npm run build:integration:cli --intg=<Integration>
npx serve ./dist -p 3005 --corsBase URLs for a v3 modern build (use legacy instead of modern for a legacy build):
sdkUrl = http://localhost:3001/cdn/modern/iife/rsa.min.jsdestSDKBaseURL = http://localhost:3005/cdn/modern/js-integrationspluginsSDKBaseURL = http://localhost:3002/cdn/modern/pluginsFor v1.1, serve the legacy core (cd packages/analytics-v1.1 && npm run start:no-open;
bundle rudder-analytics.min.js) and keep the integrations server; v1.1 does not use the remote plugins.
Confirm ports/filenames from each dev-server log — they can differ from your setup.
The dev servers are not ready immediately (the integrations start builds everything first).
Poll until all three bundles return 200 before running:
for url in \
http://localhost:3001/cdn/modern/iife/rsa.min.js \
http://localhost:3005/cdn/modern/js-integrations/<Integration>.min.js \
http://localhost:3002/cdn/modern/plugins/rsa-plugins.js ; do
until curl -sf -o /dev/null "$url"; do echo "waiting: $url"; sleep 3; done
done
echo "all servers ready"Read the integration source and the change under test, then synthesize a small, targeted set of events — do not blast every event type:
# the implementation
packages/analytics-js-integrations/src/integrations/<Integration>/browser.js # + utils.js, constants.js, nativeSdkLoader.js
# what changed
git -C <repo> diff -- packages/analytics-js-integrations/src/integrations/<Integration>/Decide which SDK methods the integration implements (identify, track, page, group, alias)
and which the change touches, and craft events exercising exactly those paths and payload shapes.
Examples: trait handling changed → an identify with those traits; a new track property mapping →
a track carrying that property; page-name handling → a page call.
Create a JSON config (full schema in harness/README.md). Write it to a temp dir outside the repo
— it contains the write key in plaintext (see Security below):
cdn: local (build + serve) — give the three localhost URLs:
{
"writeKey": "<WRITE_KEY>",
"integration": "<Integration>",
"sdkVersion": "v3",
"cdn": "local",
"sdkUrl": "http://localhost:3001/cdn/modern/iife/rsa.min.js",
"destSDKBaseURL": "http://localhost:3005/cdn/modern/js-integrations",
"pluginsSDKBaseURL": "http://localhost:3002/cdn/modern/plugins",
"settleMs": 6000,
"events": [
/* the targeted events derived in step 4 */
],
"expectations": [
/* optional — see below */
]
}cdn: staging | production | dev | <https base> — omit the three URLs; generate.mjs derives
them as <base>/v3/<variant>/{rsa.min.js, js-integrations, plugins} (variant defaults to modern):
{
"writeKey": "<WRITE_KEY>",
"integration": "<Integration>",
"cdn": "production",
"settleMs": 6000,
"events": [
/* … */
]
}Explicit sdkUrl/destSDKBaseURL/pluginsSDKBaseURL always override the derived ones. CDN auto-URLs
are v3-only; for v1.1 on a CDN, set them explicitly. Leave dataPlaneUrl unset unless you also want
cloud-mode. Set configUrl only for a non-default control plane (EU / self-hosted / staging CP).
Optional expectations turn a specific value check on the outgoing request into an automated
PASS/FAIL (generic — no per-destination logic). Use them when the change is about what the request
carries:
// e.g. a fix that must forward a value into a specific request field
{ "description": "<what this proves>", "requestIncludes": "<expected value>" }
{ "description": "<what this proves>", "requestBodyPath": "<field>", "equals": "<value>" }Both are evaluated only against DATA requests made after the events fired (a pre-event startup
request can't satisfy them). requestIncludes matches a substring in any such request's URL or body;
requestBodyPath+equals checks a JSON body field with strict === (so 1 ≠ "1" — match the
type). For a user-controlled field, set a recognizable sentinel value in the event and assert it.
For an SDK-generated id (messageId), you can't predict it — rely on the printed request body
(step 7) instead of a static expectation.
Optional configOverride varies the destination config for one run — without editing the
dashboard. Use it to exercise a config-dependent code path (a version switch, a new setting, a
mapping toggle) across several runs against one connection:
{
// …the fields above, plus:
"configOverride": { "<configKey>": "<value>" },
"destinationId": "<id>", // from step 1; or…
"preflightJson": "<temp>/preflight.json", // …let the id be resolved from the snapshot
}"<configKey>": "<value>", not { "<configKey>": { "web": "<value>" } }. The override merges
into the config the integration reads, not into the control plane's stored form. Keys and values
are whatever the destination under test defines — run dest-config.mjs (below) to list them.sourceConfigurationOverride load option, so the real sourceConfig
call still happens and is still verified — only the matched destination's config is adjusted
afterwards.{...real, ...override}), so a nested object is replaced wholesale
— pass the whole object.sourceConfigurationOverride does not exist in v1.1; the combination is rejected.destinationId, or preflightJson to resolve it by name.generate.mjs warns when an overridden key is not one the control plane sends for a web
device-mode connection (db-config.json's destConfig.web + destConfig.defaultConfig) — such an
override does not mirror production.To see a destination's expected web shape, or to check a delivered config offline:
# expected shape (which keys, which delivered type)
node .claude/skills/device-mode-e2e/harness/dest-config.mjs --integration <Integration>
# compare a delivered config (preflight --json, or result.deliveredConfig) against it
node .claude/skills/device-mode-e2e/harness/dest-config.mjs \
--integration <Integration> --config <delivered-config.json> [--strict] # --remote reads GitHubThe contract is read from a local checkout (integrationsConfigPath, env INTEGRATIONS_CONFIG_PATH,
else ~/workspace/rudder-integrations-config), falling back to GitHub (integrationsConfigRef,
default develop; integrationsConfigRemote: true forces it). With no contract available the run
simply skips the check and says so.
# generate the page into its OWN directory (it embeds the write key; nothing else goes in there)
mkdir -p <temp>/page
node .claude/skills/device-mode-e2e/harness/generate.mjs --config <temp>/run.json --out <temp>/page/harness.htmlThe page shows the test cases it will send and a ▶ Run tests button; it does nothing until run. Now run it in the mode the user chose in the first step (don't re-decide here):
Mode A — Interactive. The runner serves the page (from its temp dir) and opens a headed
window where they click ▶ Run and read the on-page report. --no-autorun means it waits for the
click instead of auto-running; it stays open until you Ctrl-C. Works the same for local and any
CDN (the page loads the SDK from sdkUrl, independent of where the page itself is served):
node .claude/skills/device-mode-e2e/harness/run-cdp.mjs --page <harness.html> --no-autorunDo not copy the page into the repo (
dist/) — it embeds the write key. The runner serves it from a temp dir, so nothing write-key-bearing lands in the working tree.
Mode B — Report (minimal changes, e.g. eventId-from-messageId). No clicking: the runner opens the page, auto-runs it, and prints the full report; reading the result is enough.
# headed by default (a window opens and auto-runs); add --headless for CI / no display
node .claude/skills/device-mode-e2e/harness/run-cdp.mjs --page <harness.html> \
--timeout 60000 --json /tmp/e2e-result.json
# --keep-open leaves the window up to inspect; --json dumps the full machine-readable resultExits 0 (PASS) / 1 (FAIL). Both modes render the same three sections: Test cases used,
Outgoing device-mode data requests (method/URL/query/body), and Expectations. Headed vs
--headless changes nothing in the report — the same detail is printed either way.
run-cdp.mjs prints the report to the terminal, which the user does not see — so in Report mode
you must reproduce the whole report in your reply, not a one-line "PASS". Paste back, verbatim or
lightly formatted:
<destination> dashboard" summary (below).Reducing it to "it passed" defeats the whole point — the detail is the deliverable. (Tip: pass
--json <path> and read that file if you need the structured data to build the summary.)
The Outgoing device-mode data requests bodies are where the fix is confirmed — the payload shows the value your change was supposed to put there.
To reduce the user to a reader (report-first flow): after a Mode-B run, produce an "Expected on
the <destination> dashboard" summary so they only have to glance at the dashboard. Build it by
reading the integration's mapping (browser.js: identify→attributes, track→custom event,
ecommerce→purchase, alias→id) together with the captured request bodies, and write, per test case,
what should appear. Shape it like this, using the mapping the integration actually implements:
identify <userId>→ a profile with the traits you senttrack "<event name>"→ an event of that name with those propertiestrack "<ecommerce event>"→ whatever the integration maps it to (e.g. a purchase)page→ however that integration represents a page view
This narrative is LLM-authored at report time — it needs no destination code in the harness (the request bodies already contain the attributes/events/purchases). On PASS the RudderStack side is verified; the only remaining manual step is glancing at the dashboard to confirm arrival. Fully automating even that would need a destination API token (opt-in per-destination verifier) — out of scope for the generic skill.
Only the core bundle differs between v3 and v1.1: point sdkUrl at the respective served core
(rsa.min.js vs rudder-analytics.min.js) and set sdkVersion. Integration bundles are shared;
pluginsSDKBaseURL applies to v3 only.
The report, the --json result and the preflight --json snapshot contain the destination's
delivered config (which can include destination API keys), and the generated page and run config
embed the write key in plaintext. Keep them out of the repo and off shared/hosted pages:
write them to a temp dir outside the working tree and delete them when done. Never commit them
or paste the page URL into a shared location.
Keep the page in its own directory (<temp>/page/harness.html), with the run config and
preflight.json one level up. Anything served next to the page is readable by every script running
in the page's origin — including the destination's own SDK. run-cdp.mjs serves just the one file
and sets no CORS header; a static server used for the fallback needs the isolated directory to get
the same property.
integration bundle loaded: FAIL / no bundle requested at all — usually the plugins server
is missing or pluginsSDKBaseURL is unset (see step 2's "Why"): the device-mode-destinations plugin
never loads, so no integration is requested. Confirm <pluginsSDKBaseURL>/rsa-plugins.js and
<destSDKBaseURL>/<Integration>.min.js both return 200. Check modern vs legacy matches your build.sourceConfig fetched: FAIL — wrong write key, or a non-default control plane (set configUrl).DATA sent: FAIL — the integration initialized but no data was sent after the events; often the
source has no such device-mode destination connected (run the step-1 preflight), or the change
suppressed delivery. (A native-SDK <script> load alone does not count as data.)curl -s "https://api.rudderstack.com/sourceConfig/?writeKey=<KEY>&p=npm&v=3&t=$(date +%s)" | jq '.source.destinations[].destinationDefinition.name'Timed out with no verdict — the SDK never became ready; open the page in a browser to see
console errors. Increase --timeout / settleMs for slow native SDKs.needs Node >= 22 — run nvm use to pick up the repo's .nvmrc version (see Prerequisites),
or use the zero-dep fallback.No Chrome found — set CHROME_PATH (see Prerequisites) or use the zero-dep fallback.harness/template.html — self-asserting page; owns all verification (window.__E2E_RESULT__).harness/generate.mjs — fills the template from a config (pure templating; no verification logic).harness/preflight.mjs — cache-busted sourceConfig check (is the integration connected?); --json
snapshots destination ids + live configs for a config override.harness/dest-config.mjs — reads a destination's contract (db-config.json + schema.json) from
rudder-integrations-config, derives the expected web delivered shape, and compares a delivered
config against it; also a standalone CLI.harness/run-cdp.mjs — headless system-Chrome-over-CDP runner (no Puppeteer); reads the verdict.harness/README.md — config schema, event shapes, extension notes.© rudderlabs, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 7 other files in .claude/skills/device-mode-e2e of rudderlabs/rudder-sdk-js.
Open the folder on GitHubat commit 8b8757a
Device Mode E2E next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Device Mode E2E this skillrudderlabs/rudder-sdk-js | 178 | — | ~6.3k | Automated safety check: Pass | MIT | |
| E2E Testcrowdin/crowdin-api-client-js | 139 | — | ~893 | Automated safety check: Notes | MIT | |
| Javascript Testing Expertsoftmaple/softmaple | 159 | 1 repos | ~4k | Automated safety check: Pass | Apache-2.0 | |
| Acceptance Test Authoringintent-driven-dev/intent-driven-template | 160 | — | ~2k | Automated safety check: Pass | MIT | |
| Python Testing Patternsjh941213/my-cc-harness | 126 | 17 repos | ~5.4k | Automated safety check: Pass | None | |
| Cypress Skillsickn33/agentic-awesome-skills | 47k | 1 repos | ~1.9k | Automated safety check: Pass | MIT |
crowdin/crowdin-api-client-js
Run end-to-end tests against the real Crowdin or Crowdin Enterprise API using credentials from .env.
softmaple/softmaple
Expert-level JavaScript testing skill focused on writing high-quality tests that find bugs, serve as documentation, and prevent regressions.
intent-driven-dev/intent-driven-template
A skill your agent uses when creating or modifying acceptance tests, configuring cucumber-js or behave runners, writing or refactoring step definitions, linting executable Gherkin specs, choosing an…
jh941213/my-cc-harness
Implement comprehensive testing strategies with pytest, fixtures, mocking, and test-driven development.
sickn33/agentic-awesome-skills
Generates production-grade Cypress E2E and component tests in JavaScript or TypeScript.
prime-radiant-inc/greenfield
Layer 1 skill for extracting behavioral intelligence from test suites.
Works with
Categories
End-to-end test a device-mode integration change locally. An agent skill from rudderlabs/rudder-sdk-js. Device Mode E2E is an agent skill from rudderlabs/rudder-sdk-js. End-to-end test a device-mode integration change locally.
Device Mode E2E fits situations like: asked to e2e test; verify device mode; test my <destination changes; test an integration against a different destination config.
Run `npx skills add rudderlabs/rudder-sdk-js --skill device-mode-e2e -a claude-code`. Or copy the skill folder (.claude/skills/device-mode-e2e in rudderlabs/rudder-sdk-js) into .claude/skills/device-mode-e2e in your project. Claude Code loads it when a task matches its description.
Run `npx skills add rudderlabs/rudder-sdk-js --skill device-mode-e2e -a codex`. Or copy the skill folder (.claude/skills/device-mode-e2e in rudderlabs/rudder-sdk-js) into .agents/skills/device-mode-e2e in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add rudderlabs/rudder-sdk-js --skill device-mode-e2e -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/device-mode-e2e, .gemini/skills/device-mode-e2e, .github/skills/device-mode-e2e and .opencode/skills/device-mode-e2e in your project.
Going by SKILL.md and its folder, Device Mode E2E needs JavaScript for the scripts in its folder, the command-line tools its instructions call (node, npm, nvm, npx, git and curl) and credentials named WRITE_KEY. Our summary lists: Node.js; A credential in WRITE_KEY.
SKILL.md names 4 domains. In commands or code: cdn.staging.rudderlabs.com, cdn.rudderlabs.com, cdn.dev.rudderlabs.com and api.rudderstack.com; the agent is likely to contact these when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Device Mode E2E is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 6.3k tokens (SKILL.md is roughly 25k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Device Mode E2E: E2E Test (crowdin/crowdin-api-client-js, 139 stars), Javascript Testing Expert (softmaple/softmaple, 159 stars), Acceptance Test Authoring (intent-driven-dev/intent-driven-template, 160 stars) and Python Testing Patterns (jh941213/my-cc-harness, 126 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
rudderlabs (a GitHub organization) maintains it in rudderlabs/rudder-sdk-js, which has 178 GitHub stars. The repository was last updated on October 7, 2026.
Source: rudderlabs/rudder-sdk-js on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.