Add EventTrackingBehavior for GDPR-compliant event control (v4.16.0)#479
Add EventTrackingBehavior for GDPR-compliant event control (v4.16.0)#479yusuftor wants to merge 10 commits into
Conversation
Introduces `EventTrackingBehavior` (.all / .superwallOnly / .none) to replace the deprecated `isExternalDataCollectionEnabled` bool. The new enum is settable both at configure time via `SuperwallOptions.eventTrackingBehavior` and at runtime via `Superwall.shared.eventTrackingBehavior`, following the same pattern as `logLevel`. `.none` suppresses all events (GDPR consent flow use case); `.superwallOnly` mirrors the old `false` value by blocking only user-initiated events, trigger fires, and attribute updates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Important
The analytics wire format changes: SuperwallOptions.encode now drops the boolean isExternalDataCollectionEnabled key and emits a string eventTrackingBehavior instead. This reaches the backend via ConfigAttributes. Confirm the server accepts the new key/shape before merging.
Reviewed changes — replaces the deprecated boolean isExternalDataCollectionEnabled with a tri-state EventTrackingBehavior enum controlling which events reach the Superwall servers, configurable at init and at runtime.
- Add
EventTrackingBehaviorenum —@objc(SWKEventTrackingBehavior),Int-backed.all/.superwallOnly/.none, conforming toCustomStringConvertible,Encodable,Sendable. SuperwallOptions.eventTrackingBehavior— new init-time option (default.all);isExternalDataCollectionEnabledbecomes a deprecated computed shim (true↔.all,false→.superwallOnly, getter== .all).Superwall.shared.eventTrackingBehavior— runtime setter mirroring the existinglogLevel/delegatepattern, firing aConfigAttributestrack on change.PlacementsQueuegating —externalDataCollectionAllowed→trackingAllowed, aswitchover the enum;.superwallOnlyblocksTriggerFire/UserAttributes/UserInitiatedPlacement.Track,.noneblocks everything.- Encoding contract change —
encode(to:)swaps the boolisExternalDataCollectionEnabledkey foreventTrackingBehavior.description(string). - Version bump 4.15.4 → 4.16.0 across
Constants.swift, podspec, andCHANGELOG.md; 15 newPlacementsQueueTestsplus aNetworkMock.sentEventscapture.
⚠️ Analytics payload key for event-tracking changed shape
The ConfigAttributes event (sent on configure and on every runtime change to this setting) serializes SuperwallOptions, and that payload no longer contains isExternalDataCollectionEnabled. It now carries eventTrackingBehavior as a string. If the backend reads the old boolean key for dashboards, audience filters, or reporting, those will silently stop populating, and the new string key may be ignored until the server is updated.
Technical details
# Analytics payload key for event-tracking changed shape
## Affected sites
- `Sources/SuperwallKit/Config/Options/SuperwallOptions.swift:394-395` — `CodingKeys` renamed `isExternalDataCollectionEnabled` → `eventTrackingBehavior`.
- `Sources/SuperwallKit/Config/Options/SuperwallOptions.swift:430` — encodes `eventTrackingBehavior.description` (e.g. `"all"`) instead of the bool.
- Flows to backend via `InternalSuperwallEvent.ConfigAttributes.getSuperwallParameters()` → `options.toDictionary()` (`TrackableSuperwallEvent.swift:212-227`).
## Required outcome
- Confirm the Superwall backend ingests the new `eventTrackingBehavior` string key, or keep emitting the legacy boolean alongside it during a transition window so existing server-side consumers don't regress.
## Open questions for the human
- Does any server-side consumer (audience filters, dashboard, reporting) still key off `isExternalDataCollectionEnabled`? If so, should both keys be emitted for one release?ℹ️ Tri-state → boolean round-trip silently re-enables collection on .none
The deprecated shim getter returns false for both .superwallOnly and .none, but the setter maps false → .superwallOnly. Legacy code doing a read-modify-write — e.g. options.isExternalDataCollectionEnabled = options.isExternalDataCollectionEnabled — against a .none config silently promotes it to .superwallOnly, re-enabling internal SDK event collection. For a GDPR consent feature this is a sharp edge worth a doc note, even though it's inherent to collapsing three states onto a bool.
Technical details
# Tri-state → boolean round-trip silently re-enables collection on `.none`
## Affected sites
- `Sources/SuperwallKit/Config/Options/SuperwallOptions.swift:316-323` — getter (`== .all`) returns `false` for both `.superwallOnly` and `.none`; setter maps `false` → `.superwallOnly` (never `.none`).
## Required outcome
- Document on `isExternalDataCollectionEnabled` that the bool cannot represent `.none`, so round-tripping through it on a `.none` config upgrades to `.superwallOnly`. No code change required if the deprecation is short-lived.ℹ️ Nitpicks
Tests/SuperwallKitTests/Storage/PlacementsQueueTests.swift:232—makeQueuemutates theSuperwall.sharedsingleton'seventTrackingBehaviorand never restores it.@Suite(.serialized)orders within the suite, but the last-set value leaks to any other suite touchingSuperwall.shared.options. Consider resetting to.allin a deinit/teardown.
Claude Opus | 𝕏
… deprecated setter - trackingAllowed now reads configManager.options.eventTrackingBehavior instead of Superwall.shared to honour the DI contract (matches how networkEnvironment is read) - Deprecated isExternalDataCollectionEnabled setter now preserves .none when set to false, preventing a silent downgrade to .superwallOnly for stricter callers - Tests no longer mutate Superwall.shared global state; behavior is scoped to the injected SuperwallOptions instance - Add deprecatedFalse_preservesNone test to cover the new preservation behaviour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Events enqueued while tracking was allowed could still be transmitted on the next timer tick or willResignActive flush after the user opts out via the GDPR consent flow. flushInternal now clears elements immediately and returns early when behavior is .none, preventing any pre-buffered events from being sent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No critical issues — one minor test-coverage suggestion inline.
Reviewed changes — this run covers commit dcfa0b0, which addresses the two correctness findings and the nitpick from the prior pullfrog review.
- Preserve
.nonein deprecated setter —isExternalDataCollectionEnabled's setter now only mapsfalse → .superwallOnlywhen the current value isn't already.none, closing the silent-downgrade footgun; covered by the newdeprecatedFalse_preservesNonetest. - Inject
configManagerinto the queue gate —PlacementsQueue.trackingAllowedreadsconfigManager.options.eventTrackingBehaviorinstead of theSuperwall.sharedglobal, restoring the DI contract. - Rewire tests to injected
options—makeQueueand the deprecated/runtime tests now construct a localSuperwallOptionsand pass it to bothNetworkMockandConfigManager, eliminating the prior global-state leak.
The wire-format concern (encode now emits the string eventTrackingBehavior key and drops the boolean) and the flushInternal buffered-event gap on .none from the prior reviews remain open but are untouched by this commit, so they're out of scope for this delta.
Claude Opus | 𝕏
The test was operating on a local SuperwallOptions instance rather than Superwall.shared, so it wasn't actually covering the runtime setter path it claimed to. The property round-trip it tested is already implicit in the deprecated-getter tests. Removed the test and its MARK section rather than keeping a misleading name. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — this run covers commits 29018b0 and 7b36651, which close the buffered-event flush gap and retire the redundant runtime-setter test from the prior pullfrog reviews.
- Guard
flushInternalon.none—PlacementsQueue.flushInternalnow discards the bufferedelementsand returns early wheneventTrackingBehavioris.none, so events queued before a runtime opt-out are no longer transmitted on the next timer tick orwillResignActiveflush. This closes the GDPR opt-out gap raised in the prior reviews. - Cover the discard path — new
none_discardsAlreadyBufferedEventstest enqueues under.all, switches the injectedconfigManager.optionsto.none, flushes, and asserts nothing was sent. The sharedSuperwallOptionsreference makes the mutation reachflushInternal's read correctly. - Remove misnamed runtime test —
runtimeSetter_updatesOptionsand its// MARK: - Runtime Superwall.shared propertyheading were deleted, resolving the prior inline thread; the test only re-asserted plainSuperwallOptionsbehavior already covered elsewhere.
The two remaining open items from earlier reviews — the analytics wire-format change (encode emits the string eventTrackingBehavior key and drops the boolean) and the optional doc note about the tri-state→bool round-trip — are untouched by these commits and remain out of scope for this delta.
Claude Opus | 𝕏
Stale events belong to the setter, not to flushInternal. When eventTrackingBehavior is set to .none at runtime, the setter now immediately calls placementsQueue.clearBuffer() so buffered events are discarded the moment the user opts out. flushInternal no longer needs to know about tracking behavior at all. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Important
Moving the .none enforcement out of flushInternal and into a fire-and-forget clearBuffer Task reopens the GDPR opt-out gap that commit 29018b0 closed. The buffer is now protected only if clearBuffer wins a race against any in-flight flush.
Reviewed changes — this run covers commits 29018b0, 7b36651, and d8e4497, which reworked where the .none opt-out empties the buffered-event queue.
- Move opt-out enforcement to the runtime setter —
Superwall.shared.eventTrackingBehavior's setter now fires aTask { await placementsQueue.clearBuffer() }when set to.none, and the prior.noneguard at the top offlushInternalwas removed. - Add
PlacementsQueue.clearBuffer()— a new actor method that emptieselements, replacing the flush-time discard. - Retarget the test —
none_discardsAlreadyBufferedEventswas renamed toclearBuffer_discardsAllBufferedEventsand now exercisesclearBuffer()in isolation rather than the runtime opt-out path.
⚠️ Test no longer covers the runtime opt-out path it's meant to protect
The renamed clearBuffer_discardsAllBufferedEvents test calls clearBuffer() directly and asserts the buffer empties — but that only proves the new method works in isolation. The behavior that actually matters for GDPR (setting .none at runtime empties the buffer before any flush can transmit) is now untested. The previous none_discardsAlreadyBufferedEvents test drove the scenario through the behavior switch + flush; that end-to-end coverage was lost in the rename.
Technical details
# Test no longer covers the runtime opt-out path
## Affected sites
- `Tests/SuperwallKitTests/Storage/PlacementsQueueTests.swift:173-184` — `clearBuffer_discardsAllBufferedEvents` calls `clearBuffer()` directly, bypassing the setter and any concurrent flush.
## Required outcome
- A test that drives the actual opt-out path: enqueue under `.all`, set `eventTrackingBehavior = .none` (or otherwise exercise the setter + flush ordering), and assert nothing reaches `NetworkMock.sentEvents` even when a flush runs.
## Open questions for the human
- If the flush-time guard is restored (see the inline comment on `Superwall.swift`), a unit test asserting `flushInternal` sends nothing under `.none` would cover the guarantee deterministically without depending on Task ordering.Claude Opus | 𝕏
clearBuffer() and flushInternal() are both dispatched as unordered Tasks on the actor, so flushInternal can drain and send buffered events before clearBuffer gets a turn. The guard makes the behavior check and the drain atomic within the same actor turn, eliminating the window. clearBuffer() at the setter stays as an eager cleanup but the guard is what provides the correctness guarantee. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — this run covers commit 4b645d9, which closes the .none flush race flagged in the prior pullfrog review.
- Restore the flush-time
.noneguard —PlacementsQueue.flushInternalagain early-returns and discardselementswheneventTrackingBehavioris.none, making the discard atomic with the drain. The setter's eagerclearBuffer()Taskremains as cleanup, but the guarantee no longer depends on it winning a race against an in-flight flush (timer tick /willResignActive). - Add deterministic opt-out coverage — new
none_flushAfterOptOutSendsNothingtest enqueues under.all, switches the injectedconfigManager.optionsto.none, callsflushInternal, and asserts nothing reachesNetworkMock.sentEvents— exercising the flush-time guard directly without depending onTaskordering.clearBuffer_discardsAllBufferedEventsis retained for the isolatedclearBuffer()path.
The two earlier open items — the analytics wire-format change (encode emits the string eventTrackingBehavior key and drops the boolean) and the optional doc note on the tri-state→bool round-trip — are untouched by this commit and remain out of scope for this delta.
Claude Opus | 𝕏
Both setupTimer and flushInternal accessed configManager (unowned) from Tasks that can outlive the test's QueueSetup, causing SIGABRT when the unowned reference became dangling. Fix: capture what's needed from configManager synchronously during init (timerInterval, trackingBehavior) and drop the stored reference from all async paths. trackingBehavior is now a local actor property updated via setTrackingBehavior(), which Superwall.shared calls on every eventTrackingBehavior change instead of the former clearBuffer(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ No critical issues — the SIGABRT fix is sound, one stale-comment nit inline.
Reviewed changes — this run covers commit 8e8ae9b, which restructures PlacementsQueue's state ownership to fix a SIGABRT crash.
- Drop the
unowned configManagerfield —PlacementsQueueno longer storesconfigManager; the timer tick andwillResignActiveflush previously dereferenced itunownedfrom async paths after it could be deallocated. The queue now capturestrackingBehaviorandtimerIntervalsynchronously ininitwhileconfigManageris guaranteed alive. - Replace
clearBuffer()withsetTrackingBehavior(_:)— the new actor method updates the cachedtrackingBehaviorand discardselementson.none;trackingAllowed/flushInternalread the cache instead ofconfigManager.options, and theSuperwall.sharedsetter calls it to keep the cache in sync at runtime. - Rework the opt-out test —
none_flushAfterOptOutSendsNothingbecamenone_setTrackingBehaviorDiscardsBufferAndBlocksFlush, drivingsetTrackingBehavior(.none)→flushInternaland asserting nothing is sent;clearBuffer_discardsAllBufferedEventswas removed with the method.
The cached-behavior approach narrows runtime sync to the Superwall.shared.eventTrackingBehavior setter, but since options on Superwall is internal there's no other public runtime mutation path, so the guarantee holds. The two earlier open items — the analytics wire-format change (encode emits the string eventTrackingBehavior key and drops the boolean) and the optional tri-state→bool round-trip doc note — are untouched by this commit and remain out of scope for this delta.
Claude Opus | 𝕏
After the configManager refactor, the queue only holds an unowned reference to network. Update the comment accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| func setTrackingBehavior(_ behavior: EventTrackingBehavior) { | ||
| trackingBehavior = behavior | ||
| if behavior == .none { | ||
| elements.removeAll() | ||
| } | ||
| if placement is InternalSuperwallEvent.TriggerFire | ||
| || placement is InternalSuperwallEvent.UserAttributes | ||
| || placement is UserInitiatedPlacement.Track { | ||
| } |
There was a problem hiding this comment.
Buffered events leak through when switching to
.superwallOnly
setTrackingBehavior clears elements only for .none, but not for .superwallOnly. Any UserInitiatedPlacement.Track, TriggerFire, or UserAttributes events already sitting in the buffer when the caller switches from .all to .superwallOnly will still be transmitted on the very next timer tick or willResignActive flush, because flushInternal's only early-exit guard is trackingBehavior == .none. Since elements stores JSON with no type information, the buffer can't be selectively filtered post-hoc — the pragmatic fix is the same one used for .none: discard the entire buffer on the transition. This sacrifices at most ~50 queued internal events (which will naturally re-appear), but prevents previously-collected user-initiated events from escaping a runtime opt-out.
Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/SuperwallKit/Storage/PlacementsQueue.swift
Line: 86-91
Comment:
**Buffered events leak through when switching to `.superwallOnly`**
`setTrackingBehavior` clears `elements` only for `.none`, but not for `.superwallOnly`. Any `UserInitiatedPlacement.Track`, `TriggerFire`, or `UserAttributes` events already sitting in the buffer when the caller switches from `.all` to `.superwallOnly` will still be transmitted on the very next timer tick or `willResignActive` flush, because `flushInternal`'s only early-exit guard is `trackingBehavior == .none`. Since `elements` stores `JSON` with no type information, the buffer can't be selectively filtered post-hoc — the pragmatic fix is the same one used for `.none`: discard the entire buffer on the transition. This sacrifices at most ~50 queued internal events (which will naturally re-appear), but prevents previously-collected user-initiated events from escaping a runtime opt-out.
How can I resolve this? If you propose a fix, please make it concise.Events already in the buffer when switching to .superwallOnly would still be transmitted on the next flush because the buffer stores raw JSON with no type information and can't be selectively filtered. Clearing on any non-.all transition prevents user-initiated events from escaping a runtime opt-out at the cost of at most maxEventCount internal events, which will naturally reappear. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
ℹ️ The
.superwallOnlybuffer-clear addresses the prior leak, but inherits the same runtime flush race that was fixed for.none.
Reviewed changes — this run covers commit 5e4abbf, which broadens the eager buffer discard so it fires on any transition away from .all, not just .none.
- Discard buffer on
.superwallOnlytoo —PlacementsQueue.setTrackingBehaviornow clearselementswhenever the new behavior is anything other than.all(behavior != .all), so user-initiated events buffered under.allno longer leak through a runtime switch to.superwallOnly. Sinceelementsis untypedJSON, discarding the whole buffer is the pragmatic choice over post-hoc filtering. - Cover the new path —
superwallOnly_setTrackingBehaviorDiscardsBufferenqueues two internal events under.all, switches to.superwallOnly, flushes, and asserts nothing reachesNetworkMock.sentEvents.
Claude Opus | 𝕏
| func setTrackingBehavior(_ behavior: EventTrackingBehavior) { | ||
| trackingBehavior = behavior | ||
| if behavior != .all { | ||
| elements.removeAll() |
There was a problem hiding this comment.
ℹ️ This eager clear closes the .all→.superwallOnly leak, but it inherits the runtime flush race that was fixed for .none in 4b645d9. The Superwall.shared.eventTrackingBehavior setter writes options synchronously and then dispatches setTrackingBehavior via an unordered Task; a flushInternal already scheduled on the actor (timer tick / willResignActive) reads the still-.all cached trackingBehavior, passes the != .none guard, and drains buffered user-initiated events before this clear runs. For .none, flushInternal's trackingBehavior == .none early-return catches that window; .superwallOnly has no analogous flush-time guard, so the eager clear alone can lose the race.
Technical details
# `.superwallOnly` runtime switch has the same flush race that was closed for `.none`
## Affected sites
- `Sources/SuperwallKit/Superwall.swift:78-83` — setter writes `options.eventTrackingBehavior` synchronously, then dispatches `Task { await placementsQueue.setTrackingBehavior(newValue) }` (unordered).
- `Sources/SuperwallKit/Storage/PlacementsQueue.swift:86-91` — `setTrackingBehavior` clears `elements` on `behavior != .all`, but only when the Task actually runs.
- `Sources/SuperwallKit/Storage/PlacementsQueue.swift:109-113` — `flushInternal` only early-returns for `.none`; under a stale `.all` cache it drains and transmits user-initiated events.
## Required outcome
- Under a runtime switch to `.superwallOnly`, user-initiated events buffered before the switch must not be transmitted, regardless of how the `setTrackingBehavior` Task and an in-flight `flushInternal` interleave on the actor.
## Suggested approach
- The new test exercises `setTrackingBehavior` → `flushInternal` in deterministic order, so it does not surface this race. The gap is narrow (requires a flush already scheduled in the window between the option write and the Task) and may be acceptable for the GDPR use case, where `.none` is the consent-decline path and `.superwallOnly` is typically set at init. If runtime `.superwallOnly` opt-out is a supported flow, consider whether `flushInternal` should re-evaluate per-event gating against the current `trackingBehavior` rather than draining whatever was buffered.
## Open questions for the human
- Is runtime switching to `.superwallOnly` (as opposed to init-time) a supported flow that needs the same atomicity guarantee as `.none`? If `.superwallOnly` is only ever set at configure time, this race cannot occur and no change is needed.
Summary
EventTrackingBehaviorenum with.all/.superwallOnly/.nonecases to replace the deprecatedisExternalDataCollectionEnabledboolSuperwallOptions.eventTrackingBehaviorand at runtime viaSuperwall.shared.eventTrackingBehavior(same pattern aslogLevel)SuperwallOptions.isExternalDataCollectionEnabledwith a backwards-compatible computed property:false→.superwallOnly,true→.all.superwallOnlypreserves existing behaviour (blocks user-initiated.track()calls, trigger fires, and user-attribute updates while letting internal SDK events through).noneblocks all events — equivalent to never callingSuperwall.configurefrom an event-tracking perspective, intended for GDPR consent flowsChangelog entry
Added
EventTrackingBehaviorenum andSuperwallOptions.eventTrackingBehaviorproperty. DeprecatedisExternalDataCollectionEnabled.Test plan
PlacementsQueueTestscovering all three enum cases and deprecated API backwards-compatibilityCHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.swiftlintin the main directory and fixed any issues.🤖 Generated with Claude Code
Greptile Summary
This PR introduces
EventTrackingBehavior(.all/.superwallOnly/.none) as a replacement for the deprecatedisExternalDataCollectionEnabledbool, with runtime control viaSuperwall.shared.eventTrackingBehaviorand full backwards-compatibility for the old API.PlacementsQueueis refactored to own a localtrackingBehaviorcopy (removing theconfigManagerreference), withsetTrackingBehaviorproviding a clean actor-safe mutation point called by theSuperwallsetter..nonetransitions eagerly clear the event buffer via bothsetTrackingBehaviorand theflushInternalguard, but the analogous buffer-drain for.superwallOnlyis missing — events queued before a runtime switch from.allto.superwallOnlycan still be flushed..all→.superwallOnlyruntime-switch buffer scenario.Confidence Score: 4/5
Mostly safe to merge; one runtime behavior gap in PlacementsQueue should be addressed before shipping.
When a caller switches from
.allto.superwallOnlyat runtime, events already buffered inPlacementsQueue.elements(user-initiated tracks, trigger fires, user attribute updates) are transmitted on the next flush despite the new behavior explicitly blocking them. The.nonecase received explicit buffer-clearing treatment; the.superwallOnlycase did not. Everything else — the new enum, the deprecated-API shim, the actor refactor, and the unit tests — looks correct.Sources/SuperwallKit/Storage/PlacementsQueue.swift — specifically the setTrackingBehavior and flushInternal pair.
Important Files Changed
.nonetransition but not on.superwallOnly, leaving a window where blocked event types can still be flushed.Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant App participant Superwall participant Options as SuperwallOptions participant Queue as PlacementsQueue participant Network App->>Superwall: "eventTrackingBehavior = .none" Superwall->>Options: set .none (sync) Superwall->>Queue: Task setTrackingBehavior(.none) Queue->>Queue: elements.removeAll() Note over Queue: buffer cleared, future enqueues rejected App->>Superwall: "eventTrackingBehavior = .superwallOnly" Superwall->>Options: set .superwallOnly (sync) Superwall->>Queue: Task setTrackingBehavior(.superwallOnly) Note over Queue: trackingBehavior updated, elements NOT cleared Queue->>Queue: timer fires, flushInternal Note over Queue: flushInternal only guards .none Queue->>Network: sends buffered TriggerFire and UserAttributes events%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant App participant Superwall participant Options as SuperwallOptions participant Queue as PlacementsQueue participant Network App->>Superwall: "eventTrackingBehavior = .none" Superwall->>Options: set .none (sync) Superwall->>Queue: Task setTrackingBehavior(.none) Queue->>Queue: elements.removeAll() Note over Queue: buffer cleared, future enqueues rejected App->>Superwall: "eventTrackingBehavior = .superwallOnly" Superwall->>Options: set .superwallOnly (sync) Superwall->>Queue: Task setTrackingBehavior(.superwallOnly) Note over Queue: trackingBehavior updated, elements NOT cleared Queue->>Queue: timer fires, flushInternal Note over Queue: flushInternal only guards .none Queue->>Network: sends buffered TriggerFire and UserAttributes eventsComments Outside Diff (2)
Sources/SuperwallKit/Storage/PlacementsQueue.swift, line 101-119 (link).nonegateWhen
eventTrackingBehavioris set to.noneat runtime (e.g., after the user declines a GDPR consent prompt), thetrackingAllowedgate prevents new events from enteringelements, but events already buffered inelementsare still sent on the next timer tick orwillResignActiveflush. For the stated GDPR consent-flow use case, this means up tomaxEventCount(50) previously-buffered events can be transmitted after the user has opted out.flushInternalshould skip or discard the buffer when the current behavior is.none.Prompt To Fix With AI
Sources/SuperwallKit/Storage/PlacementsQueue.swift, line 101-119 (link).noneis setflushInternaldrainselementswithout checking the currenteventTrackingBehavior. When a user declines consent and the behavior is switched to.noneat runtime, any events already queued inelements(up tomaxEventCount= 50) are sent on the very next timer tick orwillResignActivenotification — exactly the outcome the GDPR opt-out feature is meant to prevent. Adding a guard at the top offlushInternal(or discardingelementson the.nonepath) would close this gap.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (3): Last reviewed commit: "Update stale QueueSetup comment to refle..." | Re-trigger Greptile