feat: extend SDK with web project bucketing fields and runVariation D… - #377
feat: extend SDK with web project bucketing fields and runVariation D…#377JosephSamirL wants to merge 12 commits into
Conversation
…OM renderer SDK-side of Web project support (Workstream A); the standalone tracking-script companion bundles live in the backend repo. - Add `a/b_fullstack` and `feature_rollout` to ExperienceTypes union/const — manual override of the auto-generated types until the upstream serving API spec catches up. - Add `experienceType?: ExperienceTypes` to BucketedVariation; populated from `experience.type` in DataManager._retrieveBucketing. - Add `experienceTypes?: Array<ExperienceTypes>` filter to BucketingAttributes; ExperienceManager.selectVariations and FeatureManager.runFeatures honor it; Context.runExperiences, runFeature, runFeatures pass it through. - Add `ruleDataProvider?: Record<string, any>` to ConfigBase; DataManager uses it at all four rule-evaluation call sites (site_area locations, selectLocations, filterMatchedRecordsWithRule for audiences, convert for goal rules) when set. RuleManager already supports the custom interface mode via \`data.name === 'RuleData'\` — no RuleManager changes needed. - Add Context.runVariation(bucketedVariation, options?): applies a web variation's CSS and JS to the DOM in execution order (experience global_css → global_js → per-change css/js/custom_js), skips defaultRedirect (handled by the Split Bundle) and fullStackFeature (handled by runFeature), idempotent via DOM marker IDs. Tests - 5 new mocha tests in experience: experienceType value populated + experienceTypes filter (all-types, single-type, no-match, web-type). - 3 new mocha tests in data: ruleDataProvider routes to RuleManager.isRuleMatched in audience evaluation; falls back to plain visitorProperties when not configured. - 7 new Playwright tests in js-sdk/browser: runVariation execution order, defaultCode CSS+JS, customCode CSS+custom_js, defaultRedirect skip, fullStackFeature skip, idempotency, null-safety. - Updated existing test fixtures (shared.js, context.tests.ts, umd-bundle.spec.ts) for the new experienceType field on BucketedVariation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
195a514 to
9b0ad76
Compare
There was a problem hiding this comment.
Code Review
This pull request introduces a ruleDataProvider to the DataManager to handle rule evaluation and implements a runVariation method in the Context class for applying CSS and JS changes to the DOM in browser environments. Additionally, it adds support for filtering experiences by type and includes experienceType in variation metadata. Review feedback highlights potential logic issues where ruleDataProvider might unexpectedly override properties or trigger evaluations without specific rules, and suggests improvements for DOM ID generation and property access robustness within the runVariation implementation.
`corepack prepare yarn@stable --activate` (used by CI) resolves to yarn 4.14.x as of 2026-05, which uses lockfile v9. The in-repo `yarn.lock` is v8 and hardened mode (enabled for public PRs) refuses to migrate the lockfile (`YN0028: The lockfile would have been modified by this install, which is explicitly forbidden`). Pin the packageManager to 4.5.3 (last release using lockfile v8) so corepack uses a compatible yarn version in CI and locally. Bump this when the lockfile is regenerated to v9. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- packages/js-sdk/src/feature-manager.ts: extract the `as Array<…>` cast
in `runFeatures` to a `let allExperiences` binding before the
`.filter(experienceTypes)` chain. The nested-cast-then-filter form
produced an indentation that prettier and the eslint `indent` rule
couldn't agree on.
- eslint.config.mjs: skip linting on the @hey-api/openapi-ts generated
files `packages/types/src/config/types.gen.ts` and the companion
`config/index.ts`. Any prettier cleanup would be wiped on the next
OpenAPI regen, so ignoring them avoids a permanent fight with the
generator.
- packages/utils/src/http-client.ts: in the Node-builtin runtime
detection block, look up `require` via `globalThis` property access
(`(globalThis as {require?:…}).require`) instead of calling the bare
`require(...)` identifier. Same behavior, no
`@typescript-eslint/no-require-imports` violation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
708a8f3 to
9cab0ec
Compare
The QA workflow does `npx playwright install` before `yarn install`, so npx fetches the latest playwright and installs its chromium revision — but `yarn test:browser` runs against the project's pinned playwright, which expects a different chromium revision. The pinned revision is never on disk, so every browser test fails with `browserType.launch: Executable doesn't exist`. Add a `pretest:browser` script that runs `playwright install chromium` just before tests, using the project's pinned playwright (the one in node_modules after `yarn install`). This guarantees the chromium revision matches what the test runner expects, regardless of what npx pre-installed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Yarn 4 dropped automatic `pre`/`post` script hooks, so the previous `pretest:browser` script never ran when CI invoked `yarn test:browser` — the chromium install effectively did nothing. Chain the install inline with `&&` so it always runs as part of the same script invocation, regardless of yarn version: playwright install chromium && playwright test --config … --project … The install is idempotent (no-op if the right revision is already on disk), so local devs don't pay any cost. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issues from the review (#1, #2, #3, #6, #7, #8 in review numbering; #4 deferred to a follow-up). #1 — convert() regression: when a global ruleDataProvider is set, the previous `if (goalRule || this._ruleDataProvider)` always entered the rule-eval block and hit `if (!goal?.rules) return`, silently dropping every rule-less goal conversion. Gate on `goal.rules` first; preserve the pre-PR contract that an explicit `goalRule` on a rule-less goal still returns undefined. #2 — Per-change marker IDs now scoped by experience + variation + change id (conv-chg-${experienceId}-${variationId}-${change.id}-…) instead of just `change.id`. Defense against future ID-semantics changes or two configs merged on one page. #3 — Validate ruleDataProvider at DataManager construction. A provider missing the `name: 'RuleData'` discriminator would fall through RuleManager's flat-key branch and silently return false for every rule, breaking all audience matching with no error surfaced. Warn and ignore the provider so the misconfiguration is visible. Also introduces a proper `RuleDataProvider` interface in @convertcom/js-sdk-types replacing the raw `Record<string, any>` on Config.ruleDataProvider, so consumers get type guidance. #6 — Replace `(change as any).data` access in runVariation with a structural type assertion narrowed to the css/js/custom_js subset. #7 — Delete the no-op "Should store ruleDataProvider on the DataManager instance" test; the next test covers the same behavior. #8 — Add four missing tests: - runVariation execution order (global_css → global_js → per-change css → js → custom_js) verified via appendChild monkey-patch - runVariation warn-and-continue when options.experience is omitted and the experienceKey isn't in config - convert() fires for a rule-less goal when ruleDataProvider is set (regression guard for #1) - DataManager warns and ignores a provider missing the `name` discriminator (regression guard for #3) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`yarn lint` exits non-zero in CI on these two unrelated, pre-existing prettier violations that were blocking the PR's lint check from running clean. Both fixes are produced verbatim by `eslint --fix` — no semantic changes: - packages/rules/src/rule-manager.ts: split a long `||` expression across two lines. - packages/utils/src/comparisons.ts: wrap a single-line value across multiple lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three issues from the follow-up review (issue #3 in that review, about the types.gen.ts manual override, deferred per request). Review #1 — `runExperience` honors `experienceTypes` filter. The singular path silently ignored the option even though BucketingAttributes advertised it. Wire `experienceTypes` through `Context.runExperience` and `ExperienceManager.selectVariation`/ `selectVariationById`: short-circuit to `null` when the experience type isn't in the filter. Brings parity with `runExperiences`, `runFeature`, and `runFeatures`. Review #2 — Flip `ruleDataProvider` precedence and document. Previous behavior: the globally-configured provider always won over per-call args (`provider || arg`). That's the opposite of how config-vs-args APIs normally layer and silently discarded an explicit `goalRule` on `trackConversion`. Flipped to `arg || provider` at all four DataManager rule-eval call sites (site_area locations, selectLocations, filterMatchedRecordsWithRule, convert). Also broadened the two outer `if (visitorProperties)` / `if (locationProperties)` guards so the provider remains reachable when the caller omits the per-call arg. Updated tests: the original "provider beats per-call props" test becomes "provider is used when no per-call props are supplied", plus a new "per-call props win over provider" test guarding the flip. Precedence is now documented on `Config.ruleDataProvider` and in `RuleDataProvider.ts`. Review #4 — `experienceTypes: []` means "no matches", not "all". Previously `if (!typeFilter?.length)` treated `[]` the same as `undefined`. Empty array now correctly means "zero types allowed — no experiences match", matching standard array-filter intuition. `undefined` and omission still mean "no filter applied". Behavior is documented on `BucketingAttributes.experienceTypes` and the `selectVariations` source. New test asserts empty-array → empty result. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion Two remaining items from the Gemini review on the prior commit: #3 — `runVariation` Visual Editor check used `c?.data?.['js']` bracket access while the rest of the body uses dot access (`data.css`, `data.js`, `data.custom_js`). Align via a narrow structural cast, `(c as {data?: {js?: string}})?.data?.js`. No behavior change. #4 — `experienceId` falls back to `'unknown'` when both `experience.id` and `bucketedVariation.experienceId` are missing, so DOM marker IDs never interpolate the string `'undefined'` (which would collide with any other change whose ids were also undefined). Degenerate-config guard; should never trigger with a valid config. SonarCloud duplication on new code — the new mocha tests in `data-manager.tests.ts` and `experience-manager.tests.ts` each repeated the same `server.on('request', …)` boilerplate 4-5 times, pushing new-code duplication to 4.9% (gate is ≤3%). Extracted an `awaitTrackRequest(server, accountId, projectId, done, assertFn)` helper in each file and refactored only the NEW blocks added by this PR. Pre-PR blocks left untouched to keep the diff minimal. `yarn lint --fix` additionally removed three now-unused `eslint-disable-next-line mocha/no-hooks-for-single-case` directives on hook blocks that gained a second test case during this PR. Tests: mocha 352/352 passing locally, Playwright 45/45 passing (including 9 runVariation tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3346eb9 to
41d77ad
Compare
Mirrors the tracking-script monolith's contentSecurityPolicyNonce
plumbing (public/js/tracking/src/render.ts and workflow.ts in the
backend repo). Without this, runVariation's <style> and <script>
elements are silently blocked on customer sites enforcing
`style-src 'nonce-…'; script-src 'nonce-…'` — the visitor is bucketed
as having seen the variation but the DOM mutation never lands, biasing
experiment results toward "variation has no effect."
Resolution order matches the tracking script's
getContentSecurityPolicyNonce():
1. Config.contentSecurityPolicyNonce (explicit, set at SDK init).
2. document.querySelector('[nonce]') — first nonced element on the
page. Reads `el.nonce` IDL first, falls back to getAttribute. The
IDL property persists after the browser hides the HTML attribute
post-connection, so this works for the bootstrap <script> the
server already stamped.
Cached per-Context so the DOM scan happens at most once.
Applied via setAttribute('nonce', value) before appendChild in both
_injectStyle and _executeScript — the DOM-API equivalent of the
monolith's `nonce="${value}"` HTML-string injection.
Tests: three new Playwright cases covering the configured path, the
DOM-auto-detect path, and the no-nonce path (verifies no spurious empty
`nonce=""` attribute is added when there's nothing to stamp, which would
itself break some strict CSPs).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
context.runVariation: - Skip richStructure changes explicitly with debug log. Their data carries a `selector` field (richStructure is intended to be a selector-scoped DOM mutation per the OpenAPI spec); applying just the embedded data.js blob in isolation would lose the selector context and could mutate the wrong elements. Until the SDK gains a selector-aware queue, the safe behavior is to skip rather than silently run the change incorrectly. - Document the defaultCodeMultipage page_id limitation: this method applies the change unconditionally; the caller is responsible for invoking runVariation only on the matching funnel step. A debug log records the page_id so the caller can correlate. - Tighten the Convert Toolkit warning: only fires for `defaultCode` changes with `data.js` (Visual Editor output that calls convert.T.*), not for customCode/richStructure where `data.js` is user-written and doesn't need the Toolkit. Previously every variation carrying a customCode `data.js` would spam the warning on customer sites that intentionally omitted the Toolkit. - Improve CSP nonce auto-detect: scan `script, style, [nonce]` and read the IDL `.nonce` property first. The HTML nonce attribute is blanked by the browser on connected `<script>`/`<style>` elements per CSP spec, so the old `[nonce]` selector missed them. Skip empty-string nonces so an `<el nonce="">` on the page doesn't poison the cache. - Use a dedicated RUN_VARIATION_STYLE_ERROR message for style injection failures (previously reused the script-error message, which read wrong in logs). rules.RuleManager._processRuleItem: - Custom-interface (RuleData) provider with no matching getter for the rule's rule_type now short-circuits to `false`, per RuleDataProvider's documented "unimplemented getters cause the rule to evaluate as false" contract. Previously the fallthrough to the existence-operator branch with `undefined` caused `doesNotExist` rules to silently match every visitor for rule types whose getter the caller never implemented. Plain-object data (non-RuleData) keeps the existing "key not present" → existence-operator semantics, since that case is a genuine "key doesn't exist on this data". data.DataManager: - Tighten `_ruleDataProvider` typing from `Record<string, any> | null` to the new `RuleDataProvider | null` interface. experience.ExperienceManager.selectVariations: - Add comment explaining that the inner `selectVariation` re-applies the same experienceTypes filter as defense-in-depth (callers may invoke selectVariation directly and expect the same semantics). Tests: - 3 new RuleManager unit tests covering custom-interface + missing getter for both `exists` and `doesNotExist`, plus a sanity check that the implemented getter still evaluates. - 2 new Playwright tests: richStructure skip (no DOM injection, no JS execution) and customCode-only variation does not warn about missing convert.T. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
| experienceId?: string; | ||
| experienceKey?: string; | ||
| experienceName?: string; | ||
| experienceType?: ExperienceTypes; |
There was a problem hiding this comment.
@JosephSamirL Why using singular at the property name and plural at the value?
Besides, I do not understand why passing experienceTypes at all? Those should be collected from the generated config types as enum (after you review and merge the latest fix around the serving config at the backend repo).



SDK-side of Web project support (Workstream A); the standalone tracking-script companion bundles live in the backend repo.
a/b_fullstackandfeature_rolloutto ExperienceTypes union/const — manual override of the auto-generated types until the upstream serving API spec catches up.experienceType?: ExperienceTypesto BucketedVariation; populated fromexperience.typein DataManager._retrieveBucketing.experienceTypes?: Array<ExperienceTypes>filter to BucketingAttributes; ExperienceManager.selectVariations and FeatureManager.runFeatures honor it; Context.runExperiences, runFeature, runFeatures pass it through.ruleDataProvider?: Record<string, any>to ConfigBase; DataManager uses it at all four rule-evaluation call sites (site_area locations, selectLocations, filterMatchedRecordsWithRule for audiences, convert for goal rules) when set. RuleManager already supports the custom interface mode via `data.name === 'RuleData'` — no RuleManager changes needed.Tests