Skip to content

Add the wide_return_session wide event for app returns - #9516

Merged
YoussefKeyrouz merged 2 commits into
developfrom
feature/youssef/wide_event_return_session
Aug 20, 2026
Merged

Add the wide_return_session wide event for app returns#9516
YoussefKeyrouz merged 2 commits into
developfrom
feature/youssef/wide_event_return_session

Conversation

@YoussefKeyrouz

@YoussefKeyrouz YoussefKeyrouz commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Task/Issue URL: https://app.asana.com/1/137249556945/project/1212810093780571/task/1217573106920334?focus=true
Tech Design URL (if applicable): https://app.asana.com/1/137249556945/project/481882893211075/task/1217382844057496?focus=true
API Proposals URL(s) (if applicable):

Description

This adds wide_return_session. wide_return_session starts after the app came to the foreground.

Data

  • feature.data.ext.after_idle: true only when the inactivity treatment was applied.
  • feature.data.ext.landed_on: ntp, ntp_user_initiated, web, serp, or duck_ai. LUT and
    Specific Page returns are classified by their actual destination.
  • feature.data.ext.time_away_ms_bucketed: lower-bound bucket 0, 60000, 300000, 900000,
    1800000, or 3600000.
  • feature.data.ext.focused: one snapshot of whether the visible landing input had focus immediately
    after the landing fragment resumed. Later focus changes do not alter it.
  • feature.data.ext.page_engaged, back_pressed, opening_screen_changed, close_tab_tapped, and
    burn_tab_tapped: sticky booleans, finalized as true or false.
  • feature.data.ext.session_duration_ms_bucketed and
    feature.data.ext.time_to_first_interaction_ms_bucketed: lower-bound duration bucket 0, 1000,
    5000, 10000, 30000, 60000, 300000, or 600000. Time to first interaction is omitted when
    the only terminal is app backgrounding and no interaction was recorded.

Successful terminals set feature.status=SUCCESS and feature.data.ext.status_reason to search_submitted, url_submitted, ai_prompt_submitted, return_to_page_tapped, tab_switcher_selected, favorite_selected, or chat_selected.

Backgrounding sets feature.status=CANCELLED and feature.data.ext.status_reason=app_backgrounded. If the process dies with an open flow, next-process cleanup emits feature.status=UNKNOWN and feature.data.ext.status_reason=app_terminated.

The event is gated by returnSessionWideEvent FF and the global wideEvents framework flag.

Steps to test this PR

Prerequisites

  • Install an internal build and complete this setup:
    • Set SAMPLING_PROBABILITY = 1.0f in RealReturnSessionWideEvent.kt for testing only. This will allow you to see every pixel and not a sampling of it.
    • In Internal Settings > Feature Flags, make sure wideEvents, enqueueWideEventPixels, sendWideEventsViaPixels, and returnSessionWideEvent.self are ENABLED.
    • Force-stop and relaunch if you change FF for them to take effect.

How to monitor

  • Filter Android studio log cat on Pixel sent: wide_return_session to view all the pixels sent at the end of the session (you won’t see a pixel until you finish the session)
  • You can also filter on RealReturnSessionWideEvent to view individual events as the session record them
  • Alternatively you can run this comment (requires rg installed) in a terminal to view all events
    adb logcat | rg 'RealReturnSessionWideEvent|Pixel sent:.*wide_(return|post_idle)_session_[cd]'.

Instructions

  1. Set Settings > General > After Inactivity to 1 minute, unless a row says Always.
  2. To start with no active session. If the previous case left the app backgrounded, reopen it, submit a
    throwaway search or click on switch tab to finish that session, wait for its pixel confirming the session ended.
  3. When you perform the row's action. Wait for Pixel sent: ... wide_return_session_c; the first event of the
    day also sends wide_return_session_d. Inspect the encoded feature.data.ext.* values.
  4. Short keys in the tables, such as landed_on, mean feature.data.ext.landed_on.
  5. When the instructions say: “Finish the session” it means perform any action that would trigger the session to finish. Easiest ones are submitting a throway search or clicking on the tab switcher.

Landing, focus, submission, and coexistence

  • Run each row with the instructions. Inspect the listed fields
Coverage Setup Action Expected log values
Ordinary NTP and focused input launch app on NTP and leave its input and keyboard open Finish the session after_idle=false, landed_on=ntp_user_initiated, focused=true
Ordinary web, unfocused input, page engagement, and URL submission launch app on a website and touch/scroll in the WebView Touch the WebView, then submit a url (example.com) after_idle=false, landed_on=web, focused=false, page_engaged=true, status_reason=url_submitted
Ordinary SERP and Back Build Back history ending on a DuckDuckGo results page, put app in background and the start it again Press Back once while remaining in the browser, then finish the session after_idle=false, landed_on=serp, back_pressed=true
Ordinary Duck.ai and native AI submission Open app on NTP, Enable open Duck.ai from the menu (hamburger menu) without submitting Submit a real non-empty native prompt after_idle=false, landed_on=duck_ai, status_reason=ai_prompt_submitted
After-idle NTP, search submission use Always > New Tab Page for idle config, with hatch enabled. Trigger a idle return then Submit a search from the address bar Return event: feature.status=SUCCESS, after_idle=true, landed_on=ntp, status_reason=search_submitted.
After-idle LUT Use Always > Last Opened Tab on https://example.com Trigger an idle return then Finish the session after_idle=true, landed_on=web
Changing opening screen Start a session on the browser with Last Opened Tab selected in the idle menu Open the idle menu and Change the option from Last Opened Tab to New Tab Page. Return to browser and submit a search to end the session opening_screen_changed=true

Remaining terminals, negative case, and interaction flags

The after-idle NTP row is the primary feature.status=SUCCESS check. For successful rows below,
inspect only the unique status_reason. Rows ending mechanically use the baseline action without
re-asserting search_submitted.

  • Run each remaining unique case:
Unique case Action Expected log values
Favorite From a fresh NTP return, tap a favorite status_reason=favorite_selected
Existing chat From a fresh Duck.ai return, select an existing chat suggestion without typing status_reason=chat_selected
Tab switcher From any fresh return, tap the Tabs button status_reason=tab_switcher_selected
Return to Page Use Always > New Tab Page from a web tab, then tap Return to Page status_reason=return_to_page_tapped; landing treatment was already verified in the after-idle NTP row
Empty Duck.ai negative From a fresh NTP return, open Duck.ai with empty input but do not submit; return to the NTP and finish the session using the baseline action No event may complete before the baseline finish, especially none with status_reason=ai_prompt_submitted
Background with no interaction Press Home without touching the landing page or input feature.status=CANCELLED, status_reason=app_backgrounded, session_duration_ms_bucketed present, time_to_first_interaction_ms_bucketed absent
Process cleanup While a fresh web return is foregrounded, run adb shell am force-stop <package>, then relaunch Identify the prior flow's cleanup separately from the new return: feature.status=UNKNOWN, status_reason=app_terminated; landing and time-away classifications were already verified above
Close tab Use Always > New Tab Page from a web tab, tap Close Tab, then finish the session using the baseline action close_tab_tapped=true, burn_tab_tapped=false
Burn tab Use Always > New Tab Page from a web tab, tap Burn Tab, then finish the session using the baseline action burn_tab_tapped=true, close_tab_tapped=false

Timing

  • Trigger various sessions and verify that time_to_first_interaction_ms_bucketed, time_away_ms_bucketed, and session_duration_ms_bucketed reflect the correct time buckets.

Feature gate and reset

  • With no active session, turn only returnSessionWideEvent off, force-stop/relaunch,
    open an NTP, press Home, immediately reopen, and finish using any action. Confirm no
    wide_return_session.

UI changes

No UI changes


Note

Medium Risk
Touches first-screen and omnibar submission paths used on every app open. Changes are mostly additive telemetry behind a feature toggle, but misclassifying user vs internal navigation could skew session metrics.

Overview
Adds a return_session wide event that runs from each foreground return until a terminal action (search, URL, AI prompt, hatch actions, background, etc.), capturing landing surface, after-idle treatment, bucketed time away, session duration, and engagement flags.

RealReturnSessionWideEvent starts after FirstScreenHandler resolves landing (after_idle, landed_on as NTP, web, SERP, or Duck.ai). It listens via BrowserInteractionsPlugin / HatchInteractionsPlugin and is gated by ReturnSessionWideEventFeature. ShowOnAppLaunchOptionHandler now returns ShowOnAppLaunchResult so idle NTP/LUT treatments match telemetry.

BrowserTabViewModel splits user vs internal query submission so restoration and error recovery do not fire search/URL classifiers. Landing focus, back, Duck.ai prompts, hatch close/burn, and JS reportMetric submissions feed the same session. NtpEngagementTracker reports NTP body engagement once per app open.

Reviewed by Cursor Bugbot for commit 1d39ae0. Bugbot is set up for automated code reviews on this repo. Configure here.

@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/wide_event_return_session branch from f3431ce to 6d7b522 Compare August 18, 2026 09:53
@github-actions

Copy link
Copy Markdown
Contributor

Privacy Review task: https://app.asana.com/0/69071770703008/1217579681353955

@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/wide_event_return_session branch from 6d7b522 to b7cb6b0 Compare August 18, 2026 20:02
@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/app_return_pixel branch from 784e798 to b707ebc Compare August 18, 2026 20:02
@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/wide_event_return_session branch from b7cb6b0 to c1d0408 Compare August 18, 2026 22:06
@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/app_return_pixel branch from b707ebc to b2fa6f9 Compare August 18, 2026 22:06
@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/app_return_pixel branch from b2fa6f9 to c4fd050 Compare August 18, 2026 23:04
@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/wide_event_return_session branch 2 times, most recently from 0350055 to 8dd0e64 Compare August 19, 2026 03:06
@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/app_return_pixel branch from c4fd050 to 668fc71 Compare August 19, 2026 03:06

@malmstein malmstein 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.

Reviewed statically, stacked PR so no CI APK. CI is green, the three red checks on the PR page are a superseded cancelled run.

Genuinely well built for its size. Session start is deferred until FirstScreenHandler has applied the opening-screen decision, so after_idle and landed_on are authoritative at flowStart rather than guessed. abortIfDisabled() on every entry point aborts rather than skips, with a comment explaining that a session started while enabled would otherwise linger and get force-completed as Unknown after a kill-switch flip. The app_terminated default in the start metadata pairs correctly with OnProcessStart cleanup. Mutations are serialised through a mutex on a single-parallelism scope, matching RealPostIdleSessionWideEvent. The -api additions on BrowserInteractionsPlugin and HatchInteractionsPlugin have default bodies so existing implementers are untouched, and the API Proposal check passes. landed_on matches ReturnSessionLanding exactly, every latency is bucketed, and nothing carries PII.

Two small non-blocking things inline, neither affecting behaviour.

Comment thread PixelDefinitions/pixels/definitions/wide_return_session.json5 Outdated
@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/app_return_pixel branch from 668fc71 to a770375 Compare August 20, 2026 06:51
@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/wide_event_return_session branch from 8dd0e64 to 7f575d2 Compare August 20, 2026 06:51
@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/wide_event_return_session branch from 7f575d2 to 7f795b8 Compare August 20, 2026 09:47

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7f795b8. Configure here.

Comment thread app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt Outdated
@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/wide_event_return_session branch from 7f795b8 to 9ba46aa Compare August 20, 2026 10:02
Base automatically changed from feature/youssef/app_return_pixel to develop August 20, 2026 16:56
@YoussefKeyrouz
YoussefKeyrouz force-pushed the feature/youssef/wide_event_return_session branch from 9ba46aa to 1d39ae0 Compare August 20, 2026 18:12
@YoussefKeyrouz
YoussefKeyrouz added this pull request to the merge queue Aug 20, 2026
Merged via the queue into develop with commit 94d6e20 Aug 20, 2026
29 of 30 checks passed
@YoussefKeyrouz
YoussefKeyrouz deleted the feature/youssef/wide_event_return_session branch August 20, 2026 19:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants