Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
* Needs "Countly.UI.WebView2" for displaying content on WPF.
* Fixed a bug where the request queue was not processed when automatic user property saving ("autoSendUserDetails") was disabled, so queued requests (for example from a manual session update) were never uploaded and could cause the SDK to loop indefinitely.
* Fixed a bug where a user-details change that serialized to nothing (empty user details) left the internal "changed" flag set forever, so the SDK kept re-processing user details on every session call and upload-completion checks could wait indefinitely.
* Fixed a crash ("NullReferenceException") when a session-timer tick landed while/after "Halt" cleared the SDK state (an unhandled exception on a timer thread can terminate the host application), and when "SessionUpdate" was called before the first "Init".

## 25.4.3
* ! Minor breaking change ! User properties will now be automatically saved under the following conditions:
Expand Down
25 changes: 25 additions & 0 deletions countlyCommon/TestingRelated/SessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,31 @@ public void Dispose()

}

[Fact]
/// <summary>
/// A session-timer tick that lands after Halt must not throw: Halt nulls the modules while
/// the old Configuration object (here with backend mode on) stays visible to the in-flight tick.
/// </summary>
public void UpdateSession_TimerTickAfterHalt_DoesNotThrow()
{
CountlyConfig cc = TestHelper.GetConfig();
cc.EnableBackendMode();
Countly.Instance.Init(cc).Wait();
Countly.Instance.HaltInternal().Wait();

// the body the session timer runs; used to NRE on the nulled moduleBackendMode
Countly.Instance.UpdateSessionInternal().Wait();
}

[Fact]
/// <summary>Calling SessionUpdate before the first Init (Configuration is null) must not throw.</summary>
public void SessionUpdate_BeforeInit_DoesNotThrow()
{
Countly.Instance.Configuration = null; // what a fresh, never-initialized process looks like

Countly.Instance.SessionUpdate(10).Wait();
}

private void ValidateSessionRequestParams(NameValueCollection collection, string appKey, string deviceId, string deviceIdType)
{
Assert.False(string.IsNullOrEmpty(collection.Get("sdk_version")));
Expand Down
32 changes: 23 additions & 9 deletions countlyCommon/countlyCommon/CountlyBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,24 @@
}
}

protected async Task UpdateSessionInternal(int? elapsedTime = null)
internal async Task UpdateSessionInternal(int? elapsedTime = null)
{
UtilityHelper.CountlyLogging("[CountlyBase] Session Update happening'");
if (Configuration.backendMode) {
moduleBackendMode.OnTimer();
Upload();

// A session-timer tick can land while/after Halt nulls the SDK state. Work on local
// captures and bail out on a halted or never-initialized SDK instead of crashing.
CountlyConfig config = Configuration;
if (config == null || !IsInitialized()) {
UtilityHelper.CountlyLogging("[CountlyBase] UpdateSessionInternal, SDK is not initialized (or halted), ignoring session update");
return;
}

if (config.backendMode) {
ModuleBackendMode backendMode = moduleBackendMode;
if (backendMode != null) {
backendMode.OnTimer();
Upload();

Check warning on line 234 in countlyCommon/countlyCommon/CountlyBase.cs

View workflow job for this annotation

GitHub Actions / Feedback + UI unit tests (net8.0-windows)

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
}
return;
}

Expand Down Expand Up @@ -1522,7 +1534,7 @@
}
if (keptRequests.Count != originalCount) {
StoredRequests = keptRequests;
SaveStoredRequests();

Check warning on line 1537 in countlyCommon/countlyCommon/CountlyBase.cs

View workflow job for this annotation

GitHub Actions / Feedback + UI unit tests (net8.0-windows)

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
}
}
}
Expand Down Expand Up @@ -1565,7 +1577,7 @@

StoredRequests.Enqueue(sr);
if (!Configuration.backendMode) {
SaveStoredRequests();

Check warning on line 1580 in countlyCommon/countlyCommon/CountlyBase.cs

View workflow job for this annotation

GitHub Actions / Feedback + UI unit tests (net8.0-windows)

Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
} else {
UtilityHelper.CountlyLogging("[CountlyBase] AddRequest, Backend mode enabled, request storage disabled");
}
Expand Down Expand Up @@ -1760,14 +1772,16 @@
/// <returns></returns>
public async Task SessionUpdate(int elapsedTimeSeconds)
{
if (Configuration.backendMode) {
UtilityHelper.CountlyLogging("[CountlyBase] SessionUpdate, Backend Mode enabled, returning");
UtilityHelper.CountlyLogging("[CountlyBase] Calling 'SessionUpdate'");
// The initialization check must come first: before the first Init (or after Halt)
// there is no Configuration object to read the backend-mode flag from.
if (!IsInitialized() || Configuration == null) {
UtilityHelper.CountlyLogging("[CountlyBase] SessionUpdate: SDK must initialized before calling 'SessionUpdate'");
return;
}

UtilityHelper.CountlyLogging("[CountlyBase] Calling 'SessionUpdate'");
if (!IsInitialized()) {
UtilityHelper.CountlyLogging("[CountlyBase] SessionUpdate: SDK must initialized before calling 'SessionUpdate'");
if (Configuration.backendMode) {
UtilityHelper.CountlyLogging("[CountlyBase] SessionUpdate, Backend Mode enabled, returning");
return;
}
if (elapsedTimeSeconds < 0) {
Expand Down
Loading