Repository navigation
Conversation
jakewski
commented
Nov 5, 2026
Collaborator
- Adds consent_mode: 'require_consent': nothing is stored or sent until opt_in_tracking(). Opting out deletes the stored identifiers, queued batches and saved flag variants.
- Fixes set_config() turning storage back on after opt_out_tracking().
- Fixes session replays dropping the spaces between inline elements.
- Fixes session replay events getting stuck in IndexedDB when a recording stops under disable_persistence.
- Stops init() from creating an empty mixpanelFlagsDb IndexedDB database.
- Moves all opt-out logic into one internal ConsentManager; the only public API change is consent_mode.
- Various CI stabillity improvements
Co-authored-by: Udit Malik <256210184+awakebutstillinbed@users.noreply.github.com>
The mask function blanked every whitespace-only text node before checking masking, so spaces between inline elements were dropped from recordings and replays showed words joined together. Made-with-Pi: true Model: claude-opus-5-5-vertex Co-authored-by: mixpanel-tal[bot] <327755774+mixpanel-tal[bot]@users.noreply.github.com>
…nsent_mode 'require_consent' (#513) * Split gdpr-utils into the opt-out flag and the opt-out checks The opt-out flag storage and Do Not Track check move to opt-out-flag.js and the addOptOutCheck wrappers to consent-checks.js, unchanged. * Hold disabled persistence in a no-op storage backend Disabling persistence swaps in a backend that reads and writes nothing, instead of checking the disabled flag in load and save. Removal and the cookie/localStorage upgrade still use the real backend. * Move the opt-out logic into OptOutFlagConsent and add consent_mode createConsentManager picks the consent implementation from consent_mode ('opt_out' or 'opt_in'; unset, opt_out_tracking_by_default picks one). The _gdpr_* methods move out of the core into OptOutFlagConsent unchanged, which turns persistence off or on and deletes the user through functions the core passes in. consent_mode is fixed at init. * Ask the consent implementation whether the opt-out wrappers block a call The wrappers ask the core's _tracking_blocked, which asks the instance's consent implementation. A core without it answers through has_opted_out_tracking(), as before. * Trigger CI * One ConsentManager: consent_mode maps the decision to a state, and services ask what it allows - ConsentManager replaces OptOutFlagConsent and the factory. A mode is a row mapping the decision (none, opted in, opted out, objected/Do Not Track) to a state; each state says whether calls, sends, fetches and storage are allowed. - Under 'opt_out' and 'opt_in', an opt-out stops only the calls, as before; flags, sends and storage behave as they did. - The services take the manager as their consent gate and ask allows(): the request queue and replay registry before storing, flags before fetching or storing variants, replay before starting, the core before sending, fetching remote settings and minting a tab ID. - The recorder bundle asks the core's _consent_allows. * Keep persistence closed after an opt-out when the config changes Persistence tracks the disable_persistence config and consent closing separately. Before, an opt-out turned persistence off through the same flag set_config resets, so any set_config call after an opt-out saved the props to storage again. * Name the modes for what picks them, leave consent_mode to #510, and answer fixed capabilities without a read - The two modes are 'no_consent_required' and 'opt_out_by_default', picked by opt_out_tracking_by_default. The consent_mode config arrives with the modes that need it. - allows() answers a capability every reachable state agrees on without reading the decision, so only 'calls' reads storage under these modes: one read per call, as before. * Add consent_mode 'require_consent' with per-page decisions and stored-identifier purge consent_mode: 'require_consent' blocks storing and sending until opt_in_tracking, applies decisions per page, and on withdrawal deletes stored identifiers, queued batches, flag variants and replay data. Unset consent_mode keeps the legacy opt_out_tracking_by_default behaviour. * Apply a decision on the page even if storing it fails, and recheck consent when the recorder loads _decide() takes the new state from the decision, with Do Not Track still winning, instead of reading it back, as Piano and PostHog do. The recorder load callback asks consent again, so a withdrawal during the load starts nothing. The before-consent browser checks allow opening the replay database for cleanup, which clearAllStorage() leaves behind. * Drop the duplicate opt_out_by_default mode, and give start/stop and the calls-only state one name each opt_out_tracking_by_default no longer picks its own mode row, identical to 'no_consent_required'; init() reads it when consent_mode is unset. The state an opt-out leaves is 'calls_stopped', apart from the 'opted_out' decision. The core's start and stop are _start_tracking/_stop_tracking/_start_services, and the wrappers ask _consent_allows('calls') in place of _tracking_blocked. * Split the consent manager's two decision paths into LegacyDecisions and PageDecisions consent-modes.js holds the tables, mode resolution and the shared Do Not Track and flag reading. LegacyDecisions keeps 'no_consent_required' as the opt_out_* configs have always worked; PageDecisions keeps 'require_consent' decisions with the persistence and applies them per page. ConsentManager picks one by mode and answers allows(), with no branching on the mode. * Name the decision classes OptOutFlagDecisions and ConsentDecisions * Say what each consent state means * Ask the consent gate whether services may write to browser storage, for consent and disable_persistence * Unit test resolveConsentMode directly * Say what the decision constructors and init() each do * Open persistence through open(), from a closed start, at construction too * Clear stored flags and replay through IDBStorageWrapper, which can skip a missing database * Hand the recorder's consent gate to the registry and recordings instead of building it from the instance * Check consent at the top of each public method instead of wrapping it * Fix comments that no longer match the code * Give fake-indexeddb back the DOMException jsdom-global's cleanup deletes * Leave version-0 databases out of the IDB test's listing, as browsers do * Hand the registry's consent gate to the recordings it recovers * Keep unsent replay on the device after a withdrawal instead of deleting it Withdrawal stops replay the usual way; its upload is refused by the consent gate. Replay saved on earlier pages stays in IndexedDB and is only uploaded once the recorder loads again, which needs consent. Removes discardRecording, deleteStoredRecordings and IDBStorageWrapper.deleteMatching; the flags cleanup removes its one key. * Ask the consent gate by named methods instead of allows(capability) ConsentGate is now allowsApiCalls, allowsSending, allowsFetching, allowsStoring and allowsPersistence, all built by buildConsentGate for the ConsentManager, the recorder bundle's gate and the open gate. The fail-open try/catch moves into allowsApiCalls, replacing allowsCall. The 'calls' capability and 'calls_stopped' state become 'api_calls' and 'api_calls_stopped'. * Require a consent gate instead of defaulting to one that allows everything Every production constructor already passes a gate, so OPEN_CONSENT_GATE was only reached by tests. A service built without a gate now fails instead of storing and sending without consent. Tests pass a shared ALLOW_ALL_CONSENT_GATE, and the request-queue consent tests use buildConsentGate. * Leave the registry entry when replay stops without persistence Stopping replay without persistence deleted this tab's registry entry but left its unsent events in IndexedDB, where nothing would upload or remove them. The entry now stays, so those events wait for a load that allows them like any other tab's, and the tab ID no longer has to outlive the stop. * Let flag persistence answer whether a save is allowed FeatureFlagPersistence.save() already skips the write when the consent gate doesn't allow persistence, so the fetch handler no longer asks too. * Update the replay and people unit tests for the required gate and the kept registry entry The people test from 2.85.0-rc needs the mock instance's gate, and the recorder test expected the registry entry to be deleted without persistence. The recovered-recording gate test now flushes a later batch, so it fails without the gate. * Clear persisted flag variants without creating the database, whether or not storing is allowed clear() opens only a database the device already has, so clearIfStored() goes. It no longer skips when persistence isn't allowed: after a withdrawal under require_consent it skipped, and left the variants and their distinct_id on the device. Without saved variants, init no longer creates an empty mixpanelFlagsDb. * Pass resolveConsentMode and readDecision values instead of getConfigFunc resolveConsentMode takes the consent_mode value, and readDecision takes whether Do Not Track applies. Each decision class works that out from the call's ignore_dnt option and the config, so dntOn goes. The flag is still read only when Do Not Track is off. * Inject the consent gate into people and group instead of reaching through the instance The mock instance no longer needs a gate of its own; the people test passes one in. * Drop the consent checks from track_pageview and track_with_groups Both only build properties and then call track(), which makes the same check first, so the return value and callback are unchanged. * Pass the stored flag to readDecision as a value * Call each stopped method's own callback instead of the last argument Each method works out its callback before the consent check and calls it with 0; flushEventsWithOptOut checks consent itself instead of passing _flushEvents an extra opt-out callback. Removes skipStoppedCall. * Read the stored decision on the first state check instead of in the constructor * Update tests for the lazy decision read, the unopened flags database and SDK logs in replay * Move consentGateFor into consent-gate.js * Share one Do Not Track check between the decision classes * Skip clearing persisted flag variants when persistence is disabled clear() opened an existing mixpanelFlagsDb under disable_persistence, failing the disable_persistence legacy tests. The consent purge clears through a persistence with no disabled check, so a withdrawal still removes the variants. * Check stopped-method callbacks with _.isFunction * Port the GDPR and disable_persistence QUnit tests to the browser suite Moves the opt-in/out matrix for track and the people methods, the persistence-on-opt-out tests for cookie and localStorage, and the disable_persistence storage and tab ID tests. The cross-subdomain and secure cookie tests stay in QUnit: they need a dotted hostname or https, which the browser suite's localhost doesn't have. The storage test now seeds mixpanelFlagsDb so an open of an existing database fails it. --------- Co-authored-by: Udit Malik <256210184+awakebutstillinbed@users.noreply.github.com>
tdumitrescu
approved these changes
Nov 5, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.