From 55f80388b7f3f053dc0fbe5208ffb2632e8f08ca Mon Sep 17 00:00:00 2001 From: Charles Hudson Date: Tue, 2 Jun 2026 19:59:07 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20feat(consent):=20Add=20cross-SDK?= =?UTF-8?q?=20consent=20and=20profile-continuity=20controls=20**Summary**?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds consent-management support across the Optimization SDK Suite and documents how applications should map privacy policy decisions into SDK behavior. - Adds split consent handling for event emission vs durable profile continuity with `consent({ events, persistence })`, while preserving boolean `consent(true | false)` as a shorthand for both. - Exposes `states.persistenceConsent`, adds `PERSISTENCE_CONSENT_KEY`, and gates profile, anonymous ID, selected optimization, and changes persistence behind persistence consent. - Refactors stateless Core/Node event calls behind `forRequest(...)`, so each request binds consent, profile, shared event context, and Experience options before calling `page`, `identify`, `trackView`, etc. - Updates Web, React Web, React Native, iOS, Android, and the native JS bridge to restore profile continuity only when permitted, clear durable continuity when denied, and support split consent APIs. - Updates hybrid Node/Web and Next.js reference implementations to use application-owned consent cookies, render baselines before consent, and persist shared anonymous IDs only when continuity consent allows it. - Adds a cross-SDK consent concept guide and refreshes package READMEs/guides with default-on, strict opt-in, split-consent, revocation, and server/browser alignment guidance. - Expands test coverage for consent gating, storage restore/clear behavior, request-bound stateless calls, native storage, and baseline-before-consent flows. **Notable API/behavior changes** - New/updated exports include `ConsentInput`, `CoreStatelessRequest`, request-scoped stateless types, `EventType`, `PERSISTENCE_CONSENT_KEY`, and `states.persistenceConsent`. - Node/stateless direct event methods now use `forRequest(...).page()/identify()/...`. - Core now fails closed by default; platform SDKs provide their runtime-specific pre-consent allowlists. [[NT-3282](https://contentful.atlassian.net/browse/NT-3282)] --- .github/workflows/main-pipeline.yaml | 2 + AGENTS.md | 413 +++++++----------- documentation/AGENTS.md | 86 ++-- documentation/concepts/README.md | 4 + ...d-sdk-runtime-and-interaction-mechanics.md | 12 +- ...anagement-in-the-optimization-sdk-suite.md | 370 ++++++++++++++++ .../concepts/core-state-management.md | 49 ++- ...-personalization-and-variant-resolution.md | 9 +- ...king-in-node-and-stateless-environments.md | 94 ++-- .../interaction-tracking-in-web-sdks.md | 25 +- ...s-sdk-runtime-and-interaction-mechanics.md | 24 +- ...-handling-in-the-optimization-sdk-suite.md | 18 +- ...nchronization-between-client-and-server.md | 88 ++-- ...tive-sdk-interaction-tracking-mechanics.md | 71 ++- documentation/guides/AGENTS.md | 119 ++--- ...t-to-analytics-and-tag-management-tools.md | 6 +- .../integrating-the-node-sdk-in-a-node-app.md | 175 +++++--- ...timization-android-sdk-in-a-compose-app.md | 44 +- ...optimization-android-sdk-in-a-views-app.md | 40 +- ...e-optimization-ios-sdk-in-a-swiftui-app.md | 36 +- ...the-optimization-ios-sdk-in-a-uikit-app.md | 40 +- ...ptimization-sdk-in-a-nextjs-app-ssr-csr.md | 127 ++++-- ...he-optimization-sdk-in-a-nextjs-app-ssr.md | 123 ++++-- ...-react-native-sdk-in-a-react-native-app.md | 62 ++- ...rating-the-react-web-sdk-in-a-react-app.md | 112 +++-- .../integrating-the-web-sdk-in-a-web-app.md | 38 +- implementations/AGENTS.md | 133 +++--- implementations/android-sdk/AGENTS.md | 152 ++----- .../android-sdk/scripts/run-e2e.sh | 3 + implementations/ios-sdk/AGENTS.md | 63 +-- implementations/ios-sdk/package.json | 2 +- implementations/ios-sdk/swiftui/App.swift | 1 + .../ios-sdk/uikit/SceneDelegate.swift | 1 + .../uitests/Tests/PreviewPanelTests.swift | 4 +- implementations/node-sdk+web-sdk/AGENTS.md | 47 +- implementations/node-sdk+web-sdk/README.md | 9 +- ...ys-identified-user-variants-cookie.spec.ts | 8 + .../displays-identified-user-variants.spec.ts | 13 +- ...isplays-unidentified-user-variants.spec.ts | 22 +- .../e2e/entry-click-tracking.spec.ts | 9 +- .../e2e/entry-view-tracking.spec.ts | 26 +- implementations/node-sdk+web-sdk/src/app.ts | 117 +++-- .../node-sdk+web-sdk/src/index.ejs | 27 +- implementations/node-sdk/AGENTS.md | 39 +- implementations/node-sdk/README.md | 4 +- implementations/node-sdk/src/app.ts | 38 +- implementations/react-native-sdk/AGENTS.md | 67 ++- implementations/react-native-sdk/App.tsx | 7 +- .../AGENTS.md | 59 +-- .../README.md | 23 +- .../app/layout.tsx | 16 +- .../app/page.tsx | 7 +- .../components/InteractiveControls.tsx | 8 + .../lib/optimization-server.ts | 18 +- .../middleware.ts | 35 +- .../AGENTS.md | 49 +-- .../README.md | 66 ++- .../app/page.tsx | 31 +- .../components/InteractiveControls.tsx | 8 + .../middleware.ts | 37 +- implementations/react-web-sdk/AGENTS.md | 51 +-- .../displays-identified-user-variants.spec.ts | 3 + implementations/web-sdk/AGENTS.md | 42 +- .../displays-identified-user-variants.spec.ts | 3 + implementations/web-sdk_react/AGENTS.md | 42 +- .../displays-identified-user-variants.spec.ts | 3 + lib/build-tools/AGENTS.md | 37 +- lib/mocks/AGENTS.md | 40 +- packages/AGENTS.md | 133 ++---- packages/android/AGENTS.md | 47 +- .../android/ContentfulOptimization/AGENTS.md | 85 ++-- .../bridge/QuickJsContextManager.kt | 4 +- .../optimization/core/OptimizationClient.kt | 65 ++- .../optimization/core/OptimizationConfig.kt | 16 +- .../optimization/core/OptimizationState.kt | 3 + .../optimization/core/PreviewState.kt | 2 + .../optimization/storage/PersistentStore.kt | 5 +- .../storage/SharedPreferencesStore.kt | 57 ++- .../tracking/ViewTrackingController.kt | 8 +- .../optimization/views/OptimizedEntryView.kt | 5 +- .../core/OptimizationConfigTest.kt | 22 + .../storage/SharedPreferencesStoreTest.kt | 193 ++++++++ packages/android/README.md | 46 +- packages/ios/AGENTS.md | 38 +- packages/ios/ContentfulOptimization/AGENTS.md | 57 +-- packages/ios/ContentfulOptimization/README.md | 29 ++ .../Bridge/JSContextManager.swift | 4 +- .../Core/OptimizationClient.swift | 91 ++-- .../Core/OptimizationConfig.swift | 25 +- .../Core/OptimizationState.swift | 3 + .../Core/PreviewState.swift | 1 + .../Storage/PersistentStore.swift | 5 +- .../Storage/UserDefaultsStore.swift | 42 +- .../OptimizationClientTests.swift | 191 +++++++- packages/ios/README.md | 2 + packages/node/node-sdk/AGENTS.md | 42 +- packages/node/node-sdk/README.md | 105 +++-- packages/node/node-sdk/dev/server.ts | 82 +++- packages/node/node-sdk/package.json | 6 +- .../src/ContentfulOptimization.test.ts | 202 +++++++-- .../node-sdk/src/ContentfulOptimization.ts | 26 +- packages/react-native-sdk/AGENTS.md | 50 +-- packages/react-native-sdk/README.md | 46 +- .../react-native-sdk/dev/utils/sdkHelpers.ts | 2 +- packages/react-native-sdk/package.json | 2 +- .../src/ContentfulOptimization.test.ts | 406 ++++++++++++++++- .../src/ContentfulOptimization.ts | 182 +++++--- .../src/storage/AsyncStorageStore.test.ts | 182 +++++++- .../src/storage/AsyncStorageStore.ts | 261 ++++++++--- packages/universal/AGENTS.md | 34 +- packages/universal/api-client/AGENTS.md | 21 +- packages/universal/api-client/package.json | 2 +- packages/universal/api-schemas/AGENTS.md | 22 +- packages/universal/api-schemas/package.json | 2 +- packages/universal/core-sdk/AGENTS.md | 27 +- packages/universal/core-sdk/README.md | 52 ++- packages/universal/core-sdk/package.json | 6 +- packages/universal/core-sdk/src/Consent.ts | 16 +- .../src/CoreStateful.detached-states.test.ts | 1 + .../core-sdk/src/CoreStateful.locale.test.ts | 1 + .../core-sdk/src/CoreStateful.test.ts | 124 +++++- .../universal/core-sdk/src/CoreStateful.ts | 58 ++- .../core-sdk/src/CoreStatefulEventEmitter.ts | 22 +- .../core-sdk/src/CoreStateless.test.ts | 372 ++++++++++------ .../universal/core-sdk/src/CoreStateless.ts | 262 ++--------- .../core-sdk/src/CoreStatelessRequest.ts | 336 ++++++++++++++ packages/universal/core-sdk/src/EventType.ts | 22 + packages/universal/core-sdk/src/constants.ts | 7 + .../core-sdk/src/events/EventBuilder.test.ts | 16 + .../core-sdk/src/events/EventBuilder.ts | 19 +- packages/universal/core-sdk/src/index.ts | 4 +- .../core-sdk/src/preview-support/AGENTS.md | 25 +- .../core-sdk/src/queues/ExperienceQueue.ts | 5 + .../core-sdk/src/queues/InsightsQueue.ts | 6 + .../universal/core-sdk/src/signals/signals.ts | 10 + .../optimization-js-bridge/AGENTS.md | 57 +-- .../optimization-js-bridge/package.json | 2 +- .../optimization-js-bridge/src/index.ts | 39 +- packages/web/AGENTS.md | 31 +- .../web/frameworks/react-web-sdk/AGENTS.md | 44 +- .../web/frameworks/react-web-sdk/README.md | 35 +- .../web/frameworks/react-web-sdk/package.json | 2 +- .../react-web-sdk/src/test/sdkTestUtils.tsx | 1 + packages/web/preview-panel/AGENTS.md | 35 +- packages/web/preview-panel/package.json | 2 +- packages/web/web-sdk/AGENTS.md | 36 +- packages/web/web-sdk/README.md | 63 ++- packages/web/web-sdk/package.json | 2 +- .../src/ContentfulOptimization.test.ts | 92 +++- .../web/web-sdk/src/ContentfulOptimization.ts | 87 +++- .../web-sdk/src/storage/LocalStore.test.ts | 19 + .../web/web-sdk/src/storage/LocalStore.ts | 26 ++ 152 files changed, 5516 insertions(+), 2859 deletions(-) create mode 100644 documentation/concepts/consent-management-in-the-optimization-sdk-suite.md create mode 100644 packages/android/ContentfulOptimization/src/test/kotlin/com/contentful/optimization/storage/SharedPreferencesStoreTest.kt create mode 100644 packages/universal/core-sdk/src/CoreStatelessRequest.ts create mode 100644 packages/universal/core-sdk/src/EventType.ts diff --git a/.github/workflows/main-pipeline.yaml b/.github/workflows/main-pipeline.yaml index 047d32d9..49d56886 100644 --- a/.github/workflows/main-pipeline.yaml +++ b/.github/workflows/main-pipeline.yaml @@ -950,6 +950,8 @@ jobs: echo "Installing Maestro CLI ($MAESTRO_VERSION)..." curl -Ls "https://get.maestro.mobile.dev" | bash echo "Installing ${{ matrix.app }} APK (${{ matrix.apk }})..." + echo "Removing any cached install of ${{ matrix.package }} to avoid signature mismatches from the cached AVD..." + adb uninstall ${{ matrix.package }} >/dev/null 2>&1 || true adb install -r android-apks/${{ matrix.apk }} echo "Setting up adb reverse (harmless localhost fallback; the apps use 10.0.2.2)..." adb reverse tcp:8000 tcp:8000 || echo "::warning::adb reverse failed; apps use 10.0.2.2 so continuing" diff --git a/AGENTS.md b/AGENTS.md index 5833975c..c3c528aa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,286 +1,179 @@ # AGENTS.md -This file defines repository-wide rules only. For any change, read this file first, then read each -applicable child `AGENTS.md` from outermost to nearest in the subtree you are editing. +Repository-wide baseline. Child files add local constraints; the nearest child file wins. ## Hierarchy -- The root `AGENTS.md` owns stable repo-wide policy. -- `packages/AGENTS.md` and `implementations/AGENTS.md` own shared policy for all packages and - reference implementations. -- `lib/*/AGENTS.md`, `packages/**/AGENTS.md`, and `implementations/*/AGENTS.md` own more specific - local instructions, commands, and gotchas. -- If local guidance conflicts with this file, follow the more specific `AGENTS.md` for that subtree. -- When adding a new workspace package or implementation, add a sibling `AGENTS.md` in the same - change. -- Keep child `AGENTS.md` files focused on local behavior. Do not duplicate root policy unless the - subtree has a local exception. +- Read this file, then each child `AGENTS.md` from the repository root to the edited path. +- Root owns stable repo policy. `packages/AGENTS.md` and `implementations/AGENTS.md` own shared + package or implementation policy. Deeper files own local boundaries, commands, and gotchas. +- Add a sibling `AGENTS.md` when adding a workspace package or reference implementation. +- Do not repeat parent policy in child files unless the subtree has a real exception. -## Environment and tooling +## Tools and source files -- Use the Node version from [`.nvmrc`](./.nvmrc) when possible. Repository engine constraints live - in the root [`package.json`](./package.json). -- Use `pnpm` only. The pinned package-manager version lives in the root +- Use the Node version in [`.nvmrc`](./.nvmrc) and the pnpm version pinned in [`package.json`](./package.json). -- Prefer `pnpm diff --git a/implementations/node-sdk/AGENTS.md b/implementations/node-sdk/AGENTS.md index 9199e1f3..e236661f 100644 --- a/implementations/node-sdk/AGENTS.md +++ b/implementations/node-sdk/AGENTS.md @@ -1,42 +1,21 @@ # AGENTS.md -Read the repository root `AGENTS.md`, then `implementations/AGENTS.md`, before this file. +Node SSR reference implementation for `@contentful/optimization-node`. -## Scope +## Rules -This is the Node SSR reference implementation for `@contentful/optimization-node`. - -## Key paths - -- `src/` -- `e2e/` -- `.env.example` - -## Local rules - -- Keep this app minimal and documentation-oriented. Reusable SDK behavior belongs in - `packages/node/node-sdk`, not here. -- This implementation uses local mock defaults from `.env.example`. -- `serve` uses PM2-managed processes. Prefer `serve:stop` over broad PM2 cleanup. - -## Common failure modes - -- Playwright reports a missing browser or executable: run `pnpm playwright:install` before retrying - E2E. -- The app or mocks fail to bind local ports such as `3000` or `8000`: stop only this - implementation's local processes with `pnpm implementation:run -- node-sdk serve:stop`. +- Keep this app minimal and documentation-oriented; reusable Node SDK behavior belongs in + `packages/node/node-sdk`. +- Local mock defaults come from `.env.example`. +- `serve` uses PM2-managed processes; use `serve:stop` when done. ## Commands -- `pnpm implementation:run -- node-sdk implementation:install` -- `pnpm implementation:run -- node-sdk typecheck` -- `pnpm implementation:run -- node-sdk serve` -- `pnpm implementation:run -- node-sdk serve:stop` -- `pnpm implementation:run -- node-sdk implementation:test:e2e:run` +- `pnpm implementation:run -- node-sdk