fix(browserstack-service): rebind per-test session after reloadSession (SDK-6767)#8
fix(browserstack-service): rebind per-test session after reloadSession (SDK-6767)#8Bhargavi-BS wants to merge 4 commits into
Conversation
…n (SDK-6767) In the CLI/binary flow the device session id was captured once per worker (KEY_FRAMEWORK_SESSION_ID) and read from that cache for every test's TestHub session event, so tests after browser.reloadSession() stayed stamped with the first session's id — collapsing multiple real device sessions onto one session/video on the Observability dashboard. Fix: in testHubModule.sendTestSessionEvent read the live driver.sessionId (fallback to the cached state) and also (re)send the per-test session binding at TEST/POST so it lands after any beforeTest/in-body reload finalizes the session; retain the onReload CREATE re-fire. Verified end-to-end on App Automate: per-test frameworkSessionId distinct 1 -> 3, each test bound to its own session (in-body and beforeTest-hook reload variants). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n rebind - testHubModule: live driver.sessionId wins over the stale cached id; a TEST/POST observer is registered; onAfterTestSession re-sends the per-test session event (post-reload rebind). - service.onReload: re-fires the CLI session capture when the CLI is running, and does not when it is not. - Update the observer-count assertion (+1 -> +2) for the new TEST/POST registration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- service.onReload: null-guard getAutomationFramework() before calling trackEvent (greptile P1) — avoids a TypeError if it returns null while isRunning() is true. - testHubModule: only re-bind the per-test session at TEST/POST when the live session actually changed during the test (a reloadSession happened), tracked via _lastReportedSessionId (greptile P2) — avoids a redundant session event on the common no-reload path. - tests: add the missing getContext() to the live-read test mock (fixes the failing UT: "instance.getContext is not a function"); cover both the re-send-on-change and skip-on-no-change branches of onAfterTestSession. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
harshit-browserstack
left a comment
There was a problem hiding this comment.
Automated SDK PR Review
Verdict: 🟡 Needs changes before merge
Summary: 1 critical · 2 warnings · 2 suggestions across 4 files reviewed.
Root-cause analysis and repro evidence are solid, and the live-session-id read fix is the right shape. The one blocking concern is reusing the CREATE/POST lifecycle event mid-session to force a refresh — that event may carry side effects beyond session-id capture that shouldn't fire twice.
See inline comments below for full Problem and Suggested Fix detail on each finding.
Generated by Automated SDK PR review.
| // SDK-6767: re-anchor the per-worker session identity to the NEW session after a reload. | ||
| // In the CLI/binary flow the device session id is captured once in before() (the CREATE/POST | ||
| // trackEvent below) into KEY_FRAMEWORK_SESSION_ID and never refreshed; without this, every | ||
| // post-reload test's TestHub event is stamped with the FIRST session's hashed_id, collapsing | ||
| // multiple real device sessions onto one session/video on the dashboard. Re-firing CREATE/POST | ||
| // re-invokes WebdriverIOModule.onDriverCreated, which overwrites the stored session id (and the | ||
| // new device's capabilities) with the live post-reload browser.sessionId. | ||
| if (BrowserstackCLI.getInstance().isRunning()) { | ||
| const automationFramework = BrowserstackCLI.getInstance().getAutomationFramework() | ||
| if (automationFramework) { | ||
| await automationFramework.trackEvent(AutomationFrameworkState.CREATE, HookState.POST, { browser: this._browser, hubUrl: this._config.hostname }) | ||
| } | ||
| } |
There was a problem hiding this comment.
🔴 Critical — [CORRECTNESS] Re-firing CREATE/POST mid-session may trigger side effects beyond session-id capture
Problem
onReload now re-invokes the full CREATE/POST lifecycle event (automationFramework.trackEvent(AutomationFrameworkState.CREATE, HookState.POST, ...)) purely to force WebdriverIOModule.onDriverCreated to re-run and refresh KEY_FRAMEWORK_SESSION_ID. That event was designed to fire exactly once, at real session creation.
If onDriverCreated (or anything else observing CREATE/POST) does more than write session state — e.g. emits a "session created" log/telemetry event, registers listeners, increments a session counter, or notifies the dashboard — all of that now fires a second time on every reloadSession() call within the same worker. The diff and tests only assert that trackEvent was called with the right arguments; nothing verifies that re-invoking it is side-effect-safe for a session that's already running.
Suggested Fix
Verify WebdriverIOModule.onDriverCreated's full body is idempotent for repeated calls within one worker lifetime (i.e. it only ever touches state that's safe to overwrite, with no "fire once" side effects). If it isn't, extract a narrower "refresh session id/capabilities" path instead of reusing the full CREATE/POST lifecycle event for this refresh.
Confidence: 🟡 Subjective/contextual — depends on onDriverCreated's full implementation, which isn't visible in this diff; needs verification against the actual method body.
| if (BrowserstackCLI.getInstance().isRunning()) { | ||
| const automationFramework = BrowserstackCLI.getInstance().getAutomationFramework() | ||
| if (automationFramework) { | ||
| await automationFramework.trackEvent(AutomationFrameworkState.CREATE, HookState.POST, { browser: this._browser, hubUrl: this._config.hostname }) |
There was a problem hiding this comment.
⚠️ Warning — [ERROR-HANDLING] Unguarded await on trackEvent inside onReload
Problem
await automationFramework.trackEvent(AutomationFrameworkState.CREATE, HookState.POST, { browser: this._browser, hubUrl: this._config.hostname })This call has no try/catch. If it throws (CLI hiccup, binary temporarily unavailable, network issue), the exception propagates out of onReload and the rest of the method — including session name/status handling below — never runs. That fails the reload/test outright due to an instrumentation error, which conflicts with the general BrowserStack SDK principle that instrumentation failures must not break the customer's test run.
Suggested Fix
Wrap the call in a try/catch, log at debug/warn on failure, and let onReload continue regardless:
if (BrowserstackCLI.getInstance().isRunning()) {
const automationFramework = BrowserstackCLI.getInstance().getAutomationFramework()
if (automationFramework) {
try {
await automationFramework.trackEvent(AutomationFrameworkState.CREATE, HookState.POST, { browser: this._browser, hubUrl: this._config.hostname })
} catch (error) {
this._bsLogger?.debug?.(`onReload: failed to re-anchor session identity: ${error}`)
}
}
}Confidence: 🟡 Subjective/contextual — depends on the team's policy for how strictly instrumentation calls must be isolated from test-affecting control flow, but matches the SDK's stated graceful-degradation principle.
| // SDK-6767: session id sent in the most recent session event (per worker). Used at TEST/POST to | ||
| // detect whether browser.reloadSession() changed the session during the test, so the post-reload | ||
| // re-bind only fires when it is actually needed (not for every test). | ||
| private _lastReportedSessionId?: string |
There was a problem hiding this comment.
⚠️ Warning — [CORRECTNESS] _lastReportedSessionId is a single scalar, not keyed per instance
Problem
private _lastReportedSessionId?: stringTestHubModule appears to be a single shared module instance per worker. _lastReportedSessionId holds only the last session bound across whichever autoInstance most recently called sendTestSessionEvent. In a multiremote setup (multiple concurrent browser instances within one worker), one instance's session id can overwrite this field, causing onAfterTestSession's "did the session change" comparison to be evaluated against the wrong instance's last-known session — leading to either a spurious re-bind or, worse, a skipped re-bind for a genuinely reloaded session.
Suggested Fix
If multiremote is a supported scenario for this module, key _lastReportedSessionId by the instance/session ref (e.g. Map<string, string> keyed by autoInstance.getRef()) instead of a single scalar.
Confidence: 🟡 Subjective/contextual — depends on whether this module is ever used with multiremote/multiple concurrent instances per worker; not verifiable from this diff alone.
| // No reload => the PRE binding already carries the correct session; skip the extra event. | ||
| if (!liveSessionId || liveSessionId === this._lastReportedSessionId) { | ||
| return | ||
| } |
There was a problem hiding this comment.
⚠️ Warning — [CORRECTNESS] Silent skip when live session read fails, no fallback to cached state
Problem
if (!liveSessionId || liveSessionId === this._lastReportedSessionId) {
return
}If the live driver.sessionId read fails or is unavailable, liveSessionId is '' (caught above) and this unconditionally returns — there's no fallback to the cached KEY_FRAMEWORK_SESSION_ID state the way sendTestSessionEvent does. If a reload genuinely happened but the live read transiently fails for this particular test, the stale PRE binding for that test is never corrected, silently reproducing part of the original bug for that one test.
Suggested Fix
On a failed/empty live read, fall back to the cached state (mirroring the fallback already used in sendTestSessionEvent) and still compare against _lastReportedSessionId before deciding whether to skip:
if (!liveSessionId) {
liveSessionId = (AutomationFramework.getState(autoInstance, AutomationFrameworkConstants.KEY_FRAMEWORK_SESSION_ID)?.toString() ?? '')
}
if (!liveSessionId || liveSessionId === this._lastReportedSessionId) {
return
}Confidence: 🟡 Subjective/contextual — the failure window this covers is narrow (live read failing specifically on a test where a reload also happened), but the current code has no fallback at all for it.
|
|
||
| let liveSessionId = '' | ||
| try { | ||
| liveSessionId = (AutomationFramework.getDriver(autoInstance) as WebdriverIO.Browser | undefined)?.sessionId?.toString() ?? '' |
There was a problem hiding this comment.
💡 Suggestion — [MAINTAINABILITY] Duplicated “live driver session, fallback to cached state” logic
Problem
The pattern of trying the live driver.sessionId via getDriver() and falling back to the cached KEY_FRAMEWORK_SESSION_ID state on failure/empty appears near-identically in both onAfterTestSession (here) and sendTestSessionEvent further down this file. As the fix evolves (e.g. addressing the fallback gap noted in the other comment), the two copies are likely to drift out of sync.
Suggested Fix
Extract a small shared helper, e.g. getLiveOrCachedSessionId(autoInstance): string, and call it from both sites.
- Remove the onReload CREATE/POST re-fire. That lifecycle event has six observers (Percy, Accessibility, Observability, CustomTags, WebdriverIO, ...), so re-firing it on every reloadSession() risked re-running all of them, and the await was unguarded. The o11y session fix does not need it: sendTestSessionEvent already reads the live driver session directly. (addresses the "unexpected side effects" + "unguarded await" comments) - Key _lastReportedSessionId per automation-instance ref (Map) so concurrent multiremote instances in one worker no longer clobber each other's last-known session. (addresses the "single scalar" comment) - Extract getLiveOrCachedSessionId() (live driver -> cached-state fallback) and use it in both sendTestSessionEvent and onAfterTestSession, so the TEST/POST path now has the cached fallback it was missing and the two sites can't drift. (addresses the "silent skip / no fallback" + "duplicated logic" comments) - tests: drop the removed onReload re-fire tests; add per-ref keying and cached-fallback coverage. Full wdio-browserstack-service suite green (39 files, 1020 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Problem
WDIO + Mocha on App Automate: within one spec, tests after
browser.reloadSession()(called in a beforeTest hook or in-body) all report under the FIRST device session — one video shows for all of them. Customer (Circle, SDK-6767): 7 tests ofposts-actions.spec.tsall carrysession_id 79b8e560…across 6 udids; build-wide 56 udids collapse onto 28 hashed_ids.Root cause
The CLI/binary flow captures the device session id once per worker into
KEY_FRAMEWORK_SESSION_ID(webdriverIOModule.tsonDriverCreated, fired once fromservice.ts before()) and each test's TestHub event reads that cached value (testHubModule.tssendTestSessionEvent).onReloadnever refreshes it (grep -rn reload cli/= 0). Present in both 9.23.1 and 9.29.1 — upgrading does not fix it.Fix
testHubModule.sendTestSessionEvent: read the livedriver.sessionId(via the already-presentgetDriver()) instead of the cached id; fall back to the cached state for LTS local-Selenium instances.TEST/POSTbinding so the per-test session event is (re)sent after the reload finalizes the session; the binary does last-write-wins pertest_run.uuid.onReloadCREATE re-fire.Verification
Tier-1 App Automate repro (WikipediaSample, pipe device-pool,
reloadSession()between tests): per-testframeworkSessionIddistinct 1 → 3, each test bound to its own session. Confirmed for both in-body and beforeTest-hook reload patterns (fixed buildsc51d4754,50c5cd10) vs broken baseline (988f1517, distinct=1).Follow-on (not in this PR)
A separate App Automate session-NAMING path (
setSessionName/_update(oldSessionId)) shows a similar off-by-one; distinct from the Observability session-binding fixed here.🤖 Generated with Claude Code