diff --git a/Agent/Analytics/NRMAAnalytics.mm b/Agent/Analytics/NRMAAnalytics.mm index 9d66dc48..6e7a66e3 100644 --- a/Agent/Analytics/NRMAAnalytics.mm +++ b/Agent/Analytics/NRMAAnalytics.mm @@ -1127,10 +1127,12 @@ - (void) clearLastSessionsAnalytics { if([NRMAFlags shouldEnableNewEventSystem]){ [_sessionAttributeManager removeAllSessionAttributes]; [_eventManager empty]; + [_eventManager resetTimestamp]; } else { try { _analyticsController->clearAttributesDuplicationStore(); _analyticsController->clearEventsDuplicationStore(); + _analyticsController->resetEventTimestamp(); } catch (std::exception& e) { NRLOG_AGENT_VERBOSE(@"Failed to clear last sessions' analytcs, %s",e.what()); } catch (...) { diff --git a/Agent/Analytics/NRMAEventManager.h b/Agent/Analytics/NRMAEventManager.h index 670dbd1a..935d2d56 100644 --- a/Agent/Analytics/NRMAEventManager.h +++ b/Agent/Analytics/NRMAEventManager.h @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)didReachMaxQueueTime:(NSTimeInterval)currentTimeMilliseconds; - (BOOL)addEvent:(id)event; - (void)empty; +- (void)resetTimestamp; - (nullable NSString *)getEventJSONStringWithError:(NSError *__autoreleasing *)error clearEvents:(BOOL)clearEvents; + (nullable NSString *)getLastSessionEventsFromFilename:(NSString *)filename; diff --git a/Agent/Analytics/NRMAEventManager.m b/Agent/Analytics/NRMAEventManager.m index 4aecf73f..ec29f6e1 100644 --- a/Agent/Analytics/NRMAEventManager.m +++ b/Agent/Analytics/NRMAEventManager.m @@ -18,6 +18,7 @@ static const NSUInteger kDefaultBufferTimeSeconds = 60; // 60 seconds static const NSUInteger kMinBufferTimeSeconds = 60; // 60 seconds static const NSUInteger kBufferTimeSecondsLeeway = 60; // 60 seconds +static const double kMillisecondsPerSecond = 1000.0; // milliseconds to seconds conversion // Event Key Format String: TimeStamp|SessionElapsedTime|EventType static NSString* const eventKeyFormat = @"%f|%f|%@"; @@ -74,9 +75,9 @@ - (BOOL)didReachMaxQueueTime:(NSTimeInterval)currentTimeMilliseconds { if(oldestEventTimestamp == 0) { return false; } - + NSTimeInterval oldestEventAge = currentTimeMilliseconds - oldestEventTimestamp; - return (oldestEventAge / kDefaultBufferSize) + kBufferTimeSecondsLeeway >= maxBufferTimeSeconds; + return (oldestEventAge / kMillisecondsPerSecond) + kBufferTimeSecondsLeeway >= maxBufferTimeSeconds; } - (NSUInteger)getEvictionIndex { @@ -122,11 +123,14 @@ - (void)empty { @synchronized (events) { [events removeAllObjects]; [_persistentStore clearAll]; - oldestEventTimestamp = 0; totalAttemptedInserts = 0; } } +- (void)resetTimestamp { + oldestEventTimestamp = 0; +} + - (nullable NSString *)getEventJSONStringWithError:(NSError *__autoreleasing *)error clearEvents:(BOOL)clearEvents { NSString *eventJsonString = nil; @synchronized (events) { diff --git a/Agent/SessionReplay/SessionReplayReporter.swift b/Agent/SessionReplay/SessionReplayReporter.swift index 1d31545b..c581d9fa 100644 --- a/Agent/SessionReplay/SessionReplayReporter.swift +++ b/Agent/SessionReplay/SessionReplayReporter.swift @@ -166,9 +166,25 @@ public class SessionReplayReporter: NSObject { "replay.lastTimestamp": String(Int(lastTimestamp)), "appVersion": appVersion, "instrumentation.provider": "mobile", - "instrumentation.name": NewRelicInternalUtils.osName(), - "instrumentation.version": NewRelicInternalUtils.agentVersion(), - "collector.name": NewRelicInternalUtils.osName() + "instrumentation.name": { + guard let connectionInfo = NRMAAgentConfiguration.connectionInformation(), + let deviceInfo = connectionInfo.deviceInformation else { + return NewRelicInternalUtils.agentName() + } + let platform = deviceInfo.platform + return platform.rawValue == 0 // NRMAPlatform_Native + ? NewRelicInternalUtils.agentName() + : NewRelicInternalUtils.string(from: platform) + }(), + "instrumentation.version": { + guard let connectionInfo = NRMAAgentConfiguration.connectionInformation(), + let deviceInfo = connectionInfo.deviceInformation, + let platformVersion = deviceInfo.platformVersion as String? else { + return NewRelicInternalUtils.agentVersion() + } + return platformVersion + }(), + "collector.name": NewRelicInternalUtils.agentName() ] if isGZipped { attributes["content_encoding"] = "gzip" diff --git a/Agent/Utilities/NRLogger.m b/Agent/Utilities/NRLogger.m index 00554521..a07464c6 100644 --- a/Agent/Utilities/NRLogger.m +++ b/Agent/Utilities/NRLogger.m @@ -335,7 +335,7 @@ - (NSMutableDictionary*) commonBlockDict { if (NRSessionId) [commonAttributes setObject:NRSessionId forKey:NRLogMessageSessionIdKey]; [commonAttributes setObject:NRLogMessageMobileValue forKey:NRLogMessageInstrumentationProviderKey]; if (name) [commonAttributes setObject:name forKey:NRLogMessageInstrumentationNameKey]; - [commonAttributes setObject:[NRMAAgentConfiguration connectionInformation].deviceInformation.agentVersion forKey:NRLogMessageInstrumentationVersionKey]; + [commonAttributes setObject:[NRMAAgentConfiguration connectionInformation].deviceInformation.platformVersion ?: [NRMAAgentConfiguration connectionInformation].deviceInformation.agentVersion forKey:NRLogMessageInstrumentationVersionKey]; if (nativePlatform) [commonAttributes setObject:nativePlatform forKey:NRLogMessageInstrumentationCollectorKey]; if (nrAppId) [commonAttributes setObject:nrAppId forKey:NRLogMessageAppIdKey]; diff --git a/Tests/Unit-Tests/NewRelicAgentTests/Analytics-Tests/TestIntegratedEventManager.m b/Tests/Unit-Tests/NewRelicAgentTests/Analytics-Tests/TestIntegratedEventManager.m index 5ec7f712..2739b4ba 100644 --- a/Tests/Unit-Tests/NewRelicAgentTests/Analytics-Tests/TestIntegratedEventManager.m +++ b/Tests/Unit-Tests/NewRelicAgentTests/Analytics-Tests/TestIntegratedEventManager.m @@ -187,32 +187,65 @@ - (void)testEmptyEvents { XCTAssertEqual(emptyDecode.count, 0); } -- (void)testEmptyEventsResetOldestEventTime { +- (void)testEmptyEventsDoesNotResetOldestEventTime { // Given NRMACustomEvent *customEventOne = [[NRMACustomEvent alloc] initWithEventType:@"Custom Event 1" timestamp:1000 sessionElapsedTimeInSeconds:20 withAttributeValidator:agreeableAttributeValidator]; - + NRMACustomEvent *customEventTwo = [[NRMACustomEvent alloc] initWithEventType:@"Custom Event 2" timestamp:1000 sessionElapsedTimeInSeconds:20 withAttributeValidator:agreeableAttributeValidator]; - + NRMACustomEvent *customEventThree = [[NRMACustomEvent alloc] initWithEventType:@"Custom Event 3" timestamp:1000 sessionElapsedTimeInSeconds:20 withAttributeValidator:agreeableAttributeValidator]; [sut setMaxEventBufferTimeInSeconds:1]; - + [sut addEvent:customEventOne]; [sut addEvent:customEventTwo]; [sut addEvent:customEventThree]; - + XCTAssertTrue([sut didReachMaxQueueTime:2000]); + // When - empty() is called (during normal harvest) [sut empty]; + // Then - timestamp should persist (matching Android behavior) + XCTAssertTrue([sut didReachMaxQueueTime:2000]); +} + +- (void)testResetTimestampResetsOldestEventTime { + // Given + NRMACustomEvent *customEventOne = [[NRMACustomEvent alloc] initWithEventType:@"Custom Event 1" + timestamp:1000 + sessionElapsedTimeInSeconds:20 + withAttributeValidator:agreeableAttributeValidator]; + + NRMACustomEvent *customEventTwo = [[NRMACustomEvent alloc] initWithEventType:@"Custom Event 2" + timestamp:1000 + sessionElapsedTimeInSeconds:20 + withAttributeValidator:agreeableAttributeValidator]; + + NRMACustomEvent *customEventThree = [[NRMACustomEvent alloc] initWithEventType:@"Custom Event 3" + timestamp:1000 + sessionElapsedTimeInSeconds:20 + withAttributeValidator:agreeableAttributeValidator]; + [sut setMaxEventBufferTimeInSeconds:1]; + + [sut addEvent:customEventOne]; + [sut addEvent:customEventTwo]; + [sut addEvent:customEventThree]; + + XCTAssertTrue([sut didReachMaxQueueTime:2000]); + + // When - resetTimestamp() is explicitly called (during session clear) + [sut resetTimestamp]; + + // Then - timestamp should be reset XCTAssertFalse([sut didReachMaxQueueTime:2000]); } diff --git a/libMobileAgent/src/Analytics/include/Analytics/AnalyticsController.hpp b/libMobileAgent/src/Analytics/include/Analytics/AnalyticsController.hpp index a93de064..78bde21c 100644 --- a/libMobileAgent/src/Analytics/include/Analytics/AnalyticsController.hpp +++ b/libMobileAgent/src/Analytics/include/Analytics/AnalyticsController.hpp @@ -218,6 +218,8 @@ namespace NewRelic { void clearAttributesDuplicationStore(); + void resetEventTimestamp(); + static std::shared_ptr fetchDuplicatedEvents( PersistentStore &eventStore, bool shouldClearStore); diff --git a/libMobileAgent/src/Analytics/include/Analytics/EventManager.hpp b/libMobileAgent/src/Analytics/include/Analytics/EventManager.hpp index 3f6e6770..dde9d9bb 100644 --- a/libMobileAgent/src/Analytics/include/Analytics/EventManager.hpp +++ b/libMobileAgent/src/Analytics/include/Analytics/EventManager.hpp @@ -102,6 +102,7 @@ namespace NewRelic { void setMaxBufferSize(unsigned int size); //sets max buffer size bool didReachMaxQueueTime(unsigned long long currentTimestamp_ms); //checks if oldest event timestamp exceededs max queue time void empty(); //removes all events in _events; + void resetTimestamp(); //resets _oldest_event_timestamp_ms to 0 (for session clear) }; } #endif diff --git a/libMobileAgent/src/Analytics/src/AnalyticsController.cxx b/libMobileAgent/src/Analytics/src/AnalyticsController.cxx index 06a4091e..825ae41e 100644 --- a/libMobileAgent/src/Analytics/src/AnalyticsController.cxx +++ b/libMobileAgent/src/Analytics/src/AnalyticsController.cxx @@ -1002,6 +1002,10 @@ namespace NewRelic { _attributeDuplicationStore.clear(); } + void AnalyticsController::resetEventTimestamp() { + _eventManager.resetTimestamp(); + } + const char *AnalyticsController::getPersistentAttributeStoreName() { return ATTRIBUTE_STORE_DB_FILENAME; } diff --git a/libMobileAgent/src/Analytics/src/EventManager.cxx b/libMobileAgent/src/Analytics/src/EventManager.cxx index 56260ce2..cd2f71c8 100644 --- a/libMobileAgent/src/Analytics/src/EventManager.cxx +++ b/libMobileAgent/src/Analytics/src/EventManager.cxx @@ -41,11 +41,14 @@ void EventManager::empty() { lock1.lock(); _events.clear(); _eventDuplicationStore.clear(); - _oldest_event_timestamp_ms = 0; //we're empty so let's reset the total number of attempted inserts. _total_attempted_inserts = 0; } +void EventManager::resetTimestamp() { + _oldest_event_timestamp_ms = 0; +} + std::string EventManager::createKey(std::shared_ptr event) { std::stringstream ss; ss << *event; diff --git a/libMobileAgent/test/AnalyticsTest/EventManager_test.cxx b/libMobileAgent/test/AnalyticsTest/EventManager_test.cxx index b1d4bc85..46996bf9 100644 --- a/libMobileAgent/test/AnalyticsTest/EventManager_test.cxx +++ b/libMobileAgent/test/AnalyticsTest/EventManager_test.cxx @@ -286,5 +286,55 @@ TEST_F(EventManagerTest, testBadSerializedEvent) { ASSERT_THROW(manager.newEvent(strs2), std::runtime_error); } + +TEST_F(EventManagerTest, testEmptyDoesNotResetTimestamp) { + EventManager manager{store}; + + // Set max buffer time to 1 second + manager.setMaxBufferTime(1); + + // Add events at timestamp 1000ms + auto event1 = manager.newCustomMobileEvent("custom1", 1000, 1, validator); + auto event2 = manager.newCustomMobileEvent("custom2", 1000, 1, validator); + auto event3 = manager.newCustomMobileEvent("custom3", 1000, 1, validator); + + manager.addEvent(event1); + manager.addEvent(event2); + manager.addEvent(event3); + + // Check at 2000ms - should have reached max queue time (1 second elapsed) + ASSERT_TRUE(manager.didReachMaxQueueTime(2000)); + + // Call empty() (simulating normal harvest) + manager.empty(); + + // Timestamp should persist (matching Android behavior) + ASSERT_TRUE(manager.didReachMaxQueueTime(2000)); +} + +TEST_F(EventManagerTest, testResetTimestampResetsTimestamp) { + EventManager manager{store}; + + // Set max buffer time to 1 second + manager.setMaxBufferTime(1); + + // Add events at timestamp 1000ms + auto event1 = manager.newCustomMobileEvent("custom1", 1000, 1, validator); + auto event2 = manager.newCustomMobileEvent("custom2", 1000, 1, validator); + auto event3 = manager.newCustomMobileEvent("custom3", 1000, 1, validator); + + manager.addEvent(event1); + manager.addEvent(event2); + manager.addEvent(event3); + + // Check at 2000ms - should have reached max queue time (1 second elapsed) + ASSERT_TRUE(manager.didReachMaxQueueTime(2000)); + + // Call resetTimestamp() (simulating session clear) + manager.resetTimestamp(); + + // Timestamp should be reset + ASSERT_FALSE(manager.didReachMaxQueueTime(2000)); +} } // namespace NewRelic