Skip to content

feat: SDK integration improvements + code-review fixes (U1–U4, P0, P1) - #2

Merged
toarm merged 15 commits into
mainfrom
feature/integration-feedback
Jul 7, 2026
Merged

toarm merged 15 commits into
mainfrom
feature/integration-feedback

Conversation

@toarm

@toarm toarm commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds disable() / clearPendingActions() to the public API for opt-out and GDPR flows
  • Captures per-event URL/referrer for pre-configure buffered events
  • Adds flushAsync() as a non-suspending flush alternative
  • Adds debugRouting config: auto-detect debug builds, force enable/disable, config-time debug token
  • Fixes three P0 issues found in code review: debug routing with enabled=false, immortal replayScope, swallowed replay errors
  • Three P1 hardening changes: atomic context snapshot, replay race guard, ContextProvider.setContext() documented

Changes

SearchCollector:

  • disable() clears pending buffer and discards subsequent calls; configure() re-enables
  • clearPendingActions() clears buffer without changing enabled state
  • setContext() now captures url/referrer per buffered event (not just the latest)
  • flushAsync() fire-and-forget flush
  • SearchCollectorConfig.debugRouting — DebugRoutingSettings(enabled, debugToken, debugEndpoint)
  • activateDebugSession() / deactivateDebugSession() / extractDebugToken() public API
  • cachedUrl/cachedReferrer replaced with AtomicReference<Pair<String,String>> (P1.4)
  • core stays null during replay; set at replay end via pendingCore guard; late arrivals drained (P1.5)

ContextProvider:

  • setContext() documented: custom implementations must override for per-event context to work (P1.6)

Test plan

  • ./gradlew testLibraries — 47 tests, 0 failures
  • New tests cover: disable/clearPendingActions, per-event context, flushAsync, debug routing (auto-detect, force on/off, config token, runtime activate/deactivate), pending cap, replay late arrivals, reset cancels in-flight replay

toarm and others added 7 commits June 26, 2026 16:42
Add disable()/clearPendingActions() lifecycle control with a 3-state
fireAndForget() (disabled→discard, configured→execute, buffered→replay).
Cap pre-configure buffer at maxPendingActions (default 250, drops oldest).
Add setContext(url, referrer) API that propagates to ContextProvider,
captures url/referrer per-PendingAction for accurate per-event attribution
on replay, and restores the current context after replay completes.
Add flushAsync() for non-suspending, fire-and-forget flush from Java/non-suspend callers.
Fix DebugRoutingSettings.enabled=null to auto-detect debug builds via
Build.VERSION.CODENAME rather than always defaulting to false.
reset() now also clears isDisabled and cached context fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ging)

- enabled=false + debugToken no longer activates debug endpoint routing;
  setDebugActive(true) is now gated on enabled != false so the caller's
  explicit intent is respected
- Replace immortal replayScope with newCore.launch so the replay coroutine
  is bound to the core's lifecycle: dispose() on a second configure() or
  reset() cancels it cleanly instead of writing into a disposed core
- Add .onFailure logging to replay's runCatching so pre-configure event
  loss is visible in logs instead of being silently swallowed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P1.4: Replace two @volatile cachedUrl/cachedReferrer fields with a single
AtomicReference<Pair<String,String>> so fireAndForget() always captures
an atomic url+referrer snapshot with no torn-read risk under concurrency.

P1.5: Keep core==null during replay so live events buffer in pendingActions
rather than racing with replay's per-action setContext() calls. core is now
assigned inside the replay coroutine after all replay events are processed.
A second pendingActions drain catches events that arrived during replay.
A pendingCore guard ensures reset()/re-configure() can cancel an in-flight
replay: pendingCore?.dispose() at the top of configure() and reset() cancels
the replay scope, and the if (pendingCore===newCore) check inside the
coroutine prevents a late-running replay from clobbering a reset state.

P1.6: Add KDoc to ContextProvider.setContext() documenting that custom
implementations must override it for per-event context to work; the
default is a no-op.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Section 3: replace old contextProvider.setUrl/setReferrer pattern with the
  simpler SearchCollector.setContext() API; note that pre-configure events
  capture context at queue time
- API Reference: add setContext, flushAsync, disable, clearPendingActions;
  expand reset() description to mention clearStorage parameter
- New "Opt-out / Disable" section documenting disable(), clearPendingActions(),
  and reset(clearStorage=true) for consent and GDPR flows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rd pattern

Captures the concurrency bug where SearchCollector.configure() activated the
new core before the replay coroutine finished mutating the shared contextProvider,
causing live events to record stale URLs/referrers from replayed pre-configure events.

Documents the three coordinated fixes:
- P1.5: pendingCore guard + delayed core activation + late-arrivals drain
- P0: bind replay to newCore.scope instead of an immortal CoroutineScope
- P1.4: AtomicReference<Pair<String,String>> replacing two @volatile fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@toarm
toarm requested a review from rudibatt June 30, 2026 09:42

@rudibatt rudibatt left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Context retrieval and swapping core seems to complex and could be simpler and less error prone from my point of view.

Comment thread library/src/main/kotlin/io/searchhub/collector/SearchCollector.kt Outdated
Comment thread library/src/main/kotlin/io/searchhub/collector/SearchCollector.kt Outdated
Comment thread library/src/main/kotlin/io/searchhub/collector/SearchCollector.kt Outdated
Comment thread docs/solutions/logic-errors/configure-replay-race-pendingcore-guard.md Outdated
Comment thread docs/solutions/logic-errors/configure-replay-race-pendingcore-guard.md Outdated
Comment thread docs/solutions/logic-errors/configure-replay-race-pendingcore-guard.md Outdated
Comment thread docs/solutions/logic-errors/configure-replay-race-pendingcore-guard.md Outdated
Comment thread library/src/main/kotlin/io/searchhub/collector/SearchCollectorCore.kt Outdated
Comment thread library/src/main/kotlin/io/searchhub/collector/SearchCollector.kt Outdated
toarm and others added 4 commits July 3, 2026 08:51
…rovider

ContextProvider previously held URL and referrer as mutable state, which meant
events read the context at async processing time rather than at call time —
causing attribution errors when setContext() was called between events.

URL and referrer are now captured atomically in fireAndForget() and passed
through block parameters to Core. BrowserInfoProvider (renamed from
ContextProvider) only provides static device metadata: user-agent, language,
touch. setNavContext() replaces setContext() on the public API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The method only transformed url/ref/timestamp by wrapping them in a data
class — no actual logic. Replace with getSession() that does the real work
(sessionStore.touch() + session ID lookup) and set timestamp, url, ref, and
channel directly in each event constructor. Removes the CommonProperties
intermediate object.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously, calling configure() a second time hard-cancelled the old
core's scope, losing any events still in its EventQueue. gracefulDispose()
stops the auto-flush timer, flushes the remaining queue on a short-lived
detached scope, and only then cancels the core's scope. reset() keeps
hard dispose — it is intentionally destructive (logout / GDPR erasure).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rudibatt
rudibatt self-requested a review July 3, 2026 08:04
toarm and others added 4 commits July 3, 2026 10:19
logReplayError() was a Core method called exclusively by SearchCollector's
replay logic. SearchCollector now owns the logger directly (filtered by the
configured log level) and logs replay errors inline, removing the
unnecessary indirection through Core.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ssing time

Tracks two events with different nav contexts set between calls, then
asserts each event carries the context that was active when the tracking
method was invoked — not the context at async processing time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The context URL/referrer race was a design mistake (reading ambient state
at processing time instead of capturing at call time), not a complex
concurrency problem worth documenting in detail. The pendingCore guard
and scope-binding are the non-obvious patterns worth keeping. Context
handling is now a short note explaining the call-time snapshot approach.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@toarm
toarm merged commit e6ec79f into main Jul 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants