Skip to content

feat: extend SDK with web project bucketing fields and runVariation D… - #377

Open
JosephSamirL wants to merge 12 commits into
main-convertfrom
feat/web-project-sdk-extension
Open

feat: extend SDK with web project bucketing fields and runVariation D…#377
JosephSamirL wants to merge 12 commits into
main-convertfrom
feat/web-project-sdk-extension

Conversation

@JosephSamirL

Copy link
Copy Markdown
Contributor

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.

…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>
@JosephSamirL
JosephSamirL force-pushed the feat/web-project-sdk-extension branch from 195a514 to 9b0ad76 Compare May 12, 2026 10:03

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/data/src/data-manager.ts Outdated
Comment thread packages/data/src/data-manager.ts Outdated
Comment thread packages/js-sdk/src/context.ts
Comment thread packages/js-sdk/src/context.ts Outdated
JosephSamirL and others added 2 commits May 12, 2026 13:21
`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>
@JosephSamirL
JosephSamirL force-pushed the feat/web-project-sdk-extension branch from 708a8f3 to 9cab0ec Compare May 12, 2026 10:45
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>
@JosephSamirL JosephSamirL self-assigned this May 12, 2026
JosephSamirL and others added 6 commits May 12, 2026 14:28
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>
@JosephSamirL
JosephSamirL force-pushed the feat/web-project-sdk-extension branch from 3346eb9 to 41d77ad Compare May 18, 2026 11:50
JosephSamirL and others added 2 commits May 18, 2026 15:20
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>
@sonarqubecloud

Copy link
Copy Markdown

@JosephSamirL
JosephSamirL requested a review from abbaseya May 18, 2026 13:49
experienceId?: string;
experienceKey?: string;
experienceName?: string;
experienceType?: ExperienceTypes;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants