Skip to content

RUMM-2873 Allow users to stop a RUM session#1356

Merged
fuzzybinary merged 8 commits into
developfrom
jward/RUMM-2873-stop-session
Apr 3, 2023
Merged

RUMM-2873 Allow users to stop a RUM session#1356
fuzzybinary merged 8 commits into
developfrom
jward/RUMM-2873-stop-session

Conversation

@fuzzybinary

Copy link
Copy Markdown
Member

What does this PR do?

This adds a method to manually end a RUM session with stopRumSession. This immediately stops the active view and sets the current session to inactive. A new session starts automatically when a user action is sent (startView or addUserAction).

RUM events now have a new property: session.isActive. This can be null, and is only currently set in RUMViewEvents. The state of this member is pulled from the parent session context.

I've modified RumApplicationScope to support holding multiple RumSessionScopes in a similar way to how RumViewManager can handle multiple Views until all of their events have been processed.

When a RUMStopSession event is received, the Application Scope forwards it to the Session, which marks itself as inactive, then forwards the event to the all views, which mark themselves as inactive and send a ViewUpdate event which captures the session now being inactive.

Provided views have finished processing all of their user actions and resources, the session is removed from available sessions on the ApplicationScope. Otherwise, it sicks around (marked as inactive) until all events are complete.

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

Still need to add tests that ensure that the RUM Context is updated on the core at the proper times and add integration tests.
@fuzzybinary
fuzzybinary requested a review from a team as a code owner March 27, 2023 23:27

@0xnm 0xnm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I've added few comments, most of them are quite minor and not related to the functional part of the change, but some are for clarifying the functional part.

Comment on lines +20 to +31
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/end_session" />

<Button
android:id="@+id/end_session"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="140dp"
android:text="@string/end_session"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

minor: probably we don't need ConstraintLayout with bias 0.5/0.498 (and 0.498/0.501 for another layout). Good old LinearLayout with gravity should work.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Just used to doing it from iOS at this point 🙂

Comment on lines +23 to +24
lateinit var endSessionButton: Button
lateinit var startKioskButton: Button

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

no need to have properties, can be just variables inside method.

Comment on lines 751 to +755
session = ViewEvent.ViewEventSession(
id = rumContext.sessionId,
type = ViewEvent.ViewEventSessionType.USER,
hasReplay = hasReplay
hasReplay = hasReplay,
isActive = rumContext.isSessionActive

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe we are hitting the problem here by relying on the rumContext. Imagine the following situation: session was stopped, but view needs to wait for the child events to complete. But before they complete, another session was started, meaning by the time this view will receive terminal view update, it will be another session in the RumContext (active=true with another session ID), different from the one which view was attributed to originally.

I think the only solution in this case is to take session ID at the time view is constructed and rely on it, instead of relying on the shared RumContext.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll double check, but I think we're good here because this isn't the shared global RUM context. It's created by going up / down the scope tree, so a new session starting will be in a different tree than this.

internal data class RumContext(
val applicationId: String = NULL_UUID,
val sessionId: String = NULL_UUID,
val isSessionActive: Boolean = true,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe it should be false by default, to have a consistency with default sessionId which is NULL_UUID, meaning there is no session.


// Then
val newSession = testedScope.activeSession
check(newSession is RumSessionScope)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I guess check is used instead of assertThat(...).isInstanceOf(...) to benefit from smart case below?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes. I was following other examples but that seems to be the reason it was done there so I kept it 🙂

Comment on lines +130 to +134
sessionIsActive = false
)
)

onView(withId(R.id.end_session)).perform(click())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why do we expect sessionIsActive = false before we click on End Session? shouldn't onView(withId(R.id.end_session)).perform(click()) be above?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is a weird thing about how these tests are written. I need to capture current scope before I actually end the session, so I have to create the expected event before I click the button. The same reason the other tests add the expected "stop view" before actually stopping the view.

Co-authored-by: Nikita Ogorodnikov <[email protected]>
@fuzzybinary
fuzzybinary force-pushed the jward/RUMM-2873-stop-session branch 2 times, most recently from ff44dfb to 6fafbb3 Compare March 28, 2023 17:57
Add more tests checking for session state.
@fuzzybinary
fuzzybinary force-pushed the jward/RUMM-2873-stop-session branch from 6fafbb3 to c6414ac Compare March 28, 2023 18:14
Also add stopSession to untestable APIs
This is used over pulling the RUMViewScope during session stop.
@fuzzybinary
fuzzybinary requested a review from 0xnm March 31, 2023 19:17
@codecov-commenter

codecov-commenter commented Mar 31, 2023

Copy link
Copy Markdown

Codecov Report

Merging #1356 (c08ad2f) into develop (0b7868b) will increase coverage by 0.08%.
The diff coverage is 86.23%.

@@             Coverage Diff             @@
##           develop    #1356      +/-   ##
===========================================
+ Coverage    81.87%   81.95%   +0.08%     
===========================================
  Files          362      363       +1     
  Lines        12903    13014     +111     
  Branches      2164     2187      +23     
===========================================
+ Hits         10564    10665     +101     
- Misses        1679     1688       +9     
- Partials       660      661       +1     
Impacted Files Coverage Δ
.../main/kotlin/com/datadog/android/rum/RumMonitor.kt 79.63% <ø> (ø)
.../android/rum/internal/monitor/DatadogRumMonitor.kt 83.66% <33.33%> (-0.84%) ⬇️
...g/android/rum/internal/domain/scope/RumRawEvent.kt 82.21% <66.67%> (-0.29%) ⬇️
...d/rum/internal/domain/scope/RumApplicationScope.kt 86.08% <84.51%> (-13.92%) ⬇️
...d/rum/internal/domain/scope/RumViewManagerScope.kt 90.70% <85.71%> (-1.34%) ⬇️
...droid/rum/internal/domain/scope/RumSessionScope.kt 91.01% <94.12%> (+1.27%) ⬆️
.../android/rum/internal/domain/scope/RumViewScope.kt 94.31% <94.12%> (-0.03%) ⬇️
.../datadog/android/rum/internal/domain/RumContext.kt 97.44% <100.00%> (+0.07%) ⬆️
...um/internal/domain/scope/RumViewChangedListener.kt 100.00% <100.00%> (ø)

... and 12 files with indirect coverage changes

@0xnm 0xnm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm! I've left some comments, but they are mostly not related to the main functionality.

applicationDisplayed
)

internal val lastActiveViewScope: RumViewScope?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it is not needed anymore?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, good catch.

childScopes.add(newSession)
if (event !is RumRawEvent.StartView) {
lastActiveViewInfo?.let {
it.keyRef.get()?.let { key ->

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should we log the case when key is missing? just to simplify debugging on the customer side.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added a message. Can you review that it makes sense?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yes, totally makes sense!

)

// Then
assertThat(testedScope.childScopes.count()).isEqualTo(1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it will give more meaningful error

Suggested change
assertThat(testedScope.childScopes.count()).isEqualTo(1)
assertThat(testedScope.childScopes).hasSize(1)

}

@Test
fun `M create a new session W handleEvent { no active sessions, user interaction } `(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it seems that name of the test is a bit misleading, here we send the view event and not an action, so I guess it should not be user interaction in the name?

@fuzzybinary
fuzzybinary force-pushed the jward/RUMM-2873-stop-session branch from 788c379 to 57edb19 Compare April 3, 2023 13:51

@0xnm 0xnm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm! I've added a minor comment to help debugging.


val activeSession: RumScope?
get() {
return childScopes.find { it.isActive() }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

just a question out of my head: find will return first element matching the criteria. Normally we shouldn't expect childScopes to contain multiple isActive sessions, but for the debugging do you think it will be useful to log when this invariant (only one active session) is broken? wdyt?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, I actually have a similar telemetry in iOS. Maybe not here, though as it could get noisy. I'll see if I can find a better place to add it.

childScopes.add(newSession)
if (event !is RumRawEvent.StartView) {
lastActiveViewInfo?.let {
it.keyRef.get()?.let { key ->

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yes, totally makes sense!

internalLogger.log(
InternalLogger.Level.WARN,
InternalLogger.Target.USER,
LAST_ACTIVE_VIEW_GONE_WARNING_MESSAGE.format(it.name)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

minor: better to explicitly specify the Locale

Suggested change
LAST_ACTIVE_VIEW_GONE_WARNING_MESSAGE.format(it.name)
LAST_ACTIVE_VIEW_GONE_WARNING_MESSAGE.format(Locale.US, it.name)

@fuzzybinary
fuzzybinary force-pushed the jward/RUMM-2873-stop-session branch from 57edb19 to 61192e1 Compare April 3, 2023 15:34
@fuzzybinary

Copy link
Copy Markdown
Member Author

@0xnm I added telemetry for checking if we create multiple active sessions during session start. I think that should handle the case you're talking about.

@fuzzybinary
fuzzybinary force-pushed the jward/RUMM-2873-stop-session branch from 61192e1 to c08ad2f Compare April 3, 2023 15:38
@fuzzybinary
fuzzybinary merged commit c1fb449 into develop Apr 3, 2023
@fuzzybinary
fuzzybinary deleted the jward/RUMM-2873-stop-session branch April 3, 2023 19:02
@xgouchet xgouchet added this to the 1.19.0 milestone Dec 13, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants