Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve end session event #124

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
13 changes: 3 additions & 10 deletions Amplitude/AMPDatabaseHelper.m
Original file line number Diff line number Diff line change
Expand Up @@ -386,16 +386,9 @@ - (NSMutableArray*)getEventsFromTable:(NSString*) table upToId:(long long) upToI
continue;
}
NSString *eventString = [NSString stringWithUTF8String:rawEventString];
if ([AMPUtils isEmptyString:eventString]) {
AMPLITUDE_LOG(@"Ignoring empty event string for event id %lld from table %@", eventId, table);
continue;
}

NSData *eventData = [eventString dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id eventImmutable = [NSJSONSerialization JSONObjectWithData:eventData options:0 error:&error];
if (error != nil) {
AMPLITUDE_LOG(@"Error JSON deserialization of event id %lld from table %@: %@", eventId, table, error);
NSDictionary *eventImmutable = [AMPUtils deserializeEventString:eventString];
if (eventImmutable == nil) {
AMPLITUDE_LOG(@"Failed to deserialize event from table %@", table);
continue;
}

Expand Down
1 change: 1 addition & 0 deletions Amplitude/AMPUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
+ (id) makeJSONSerializable:(id) obj;
+ (BOOL) isEmptyString:(NSString*) str;
+ (NSDictionary*) validateGroups:(NSDictionary*) obj;
+ (NSDictionary*) deserializeEventString:(NSString*) eventString;

@end
16 changes: 16 additions & 0 deletions Amplitude/AMPUtils.m
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,20 @@ + (NSDictionary *) validateGroups:(NSDictionary *) obj
return [NSDictionary dictionaryWithDictionary:dict];
}

+ (NSDictionary *) deserializeEventString:(NSString *)eventString
{
if ([self isEmptyString:eventString]) {
return nil;
}

NSData *eventData = [eventString dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
id event = [NSJSONSerialization JSONObjectWithData:eventData options:0 error:&error];
if (error != nil) {
AMPLITUDE_LOG(@"Error JSON deserialization of event: %@", error);
return nil;
}
return event;
}

@end
43 changes: 31 additions & 12 deletions Amplitude/Amplitude.m
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,8 @@ - (void)logEvent:(NSString*) eventType withEventProperties:(NSDictionary*) event
}
if ([eventType isEqualToString:IDENTIFY_EVENT]) {
(void) [self.dbHelper addIdentify:jsonString];
} else if ([eventType isEqualToString:kAMPSessionEndEvent]) {
(void) [self.dbHelper insertOrReplaceKeyValue:kAMPSessionEndEvent value:jsonString];
} else {
(void) [self.dbHelper addEvent:jsonString];
}
Expand Down Expand Up @@ -1102,6 +1104,7 @@ - (void)enterBackground
[self runOnBackgroundQueue:^{
_inForeground = NO;
[self refreshSessionTime:now];
[self sendSessionEvent:kAMPSessionEndEvent timestamp:now];
[self uploadEventsWithLimit:0];
}];
}
Expand Down Expand Up @@ -1151,29 +1154,45 @@ - (BOOL)startOrContinueSession:(NSNumber*) timestamp
- (void)startNewSession:(NSNumber*) timestamp
{
if (_trackingSessionEvents) {
[self sendSessionEvent:kAMPSessionEndEvent];

// try to load saved end session event from key value table, else fall back to re-logging
NSNumber *lastEventTime = [self lastEventTime];
NSString *endSessionEventString = [self.dbHelper getValue:kAMPSessionEndEvent];
if ([AMPUtils isEmptyString:endSessionEventString]) {
[self sendSessionEvent:kAMPSessionEndEvent timestamp:lastEventTime];
} else {
// sanity check the event
NSNumber *endSessionTimestamp = nil;
NSDictionary *endSessionEvent = [AMPUtils deserializeEventString:endSessionEventString];
if (endSessionEvent != nil) {
endSessionTimestamp = [endSessionEvent objectForKey:@"timestamp"];
}
if (endSessionEvent == nil || endSessionTimestamp == nil || [endSessionTimestamp longLongValue] != [lastEventTime longLongValue]) {
[self sendSessionEvent:kAMPSessionEndEvent timestamp:lastEventTime];
}
}

// transfer end session event from key value table into the event queue
endSessionEventString = [self.dbHelper getValue:kAMPSessionEndEvent];
if (![AMPUtils isEmptyString:endSessionEventString]) {
[self.dbHelper addEvent:endSessionEventString];
[self.dbHelper insertOrReplaceKeyValue:kAMPSessionEndEvent value:nil];
}
}
[self setSessionId:[timestamp longLongValue]];
[self refreshSessionTime:timestamp];
if (_trackingSessionEvents) {
[self sendSessionEvent:kAMPSessionStartEvent];
[self sendSessionEvent:kAMPSessionStartEvent timestamp:timestamp];
}
}

- (void)sendSessionEvent:(NSString*) sessionEvent
- (void)sendSessionEvent:(NSString*) sessionEvent timestamp:(NSNumber *) timestamp
{
if (_apiKey == nil) {
AMPLITUDE_ERROR(@"ERROR: apiKey cannot be nil or empty, set apiKey with initializeApiKey: before sending session event");
return;
}

if (![self inSession]) {
if (_apiKey == nil || ![self inSession]) {
return;
}

NSMutableDictionary *apiProperties = [NSMutableDictionary dictionary];
[apiProperties setValue:sessionEvent forKey:@"special"];
NSNumber* timestamp = [self lastEventTime];
NSDictionary *apiProperties = [NSDictionary dictionaryWithObject:sessionEvent forKey:@"special"];
[self logEvent:sessionEvent withEventProperties:nil withApiProperties:apiProperties withUserProperties:nil withGroups:nil withTimestamp:timestamp outOfSession:NO];
}

Expand Down
80 changes: 80 additions & 0 deletions AmplitudeTests/SessionTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#import "Amplitude.h"
#import "Amplitude+Test.h"
#import "BaseTestCase.h"
#import "AMPUtils.h"

@interface SessionTests : BaseTestCase

Expand Down Expand Up @@ -150,6 +151,8 @@ - (void)testEnterBackgroundDoesNotTrackEvent {
}

- (void)testTrackSessionEvents {
AMPDatabaseHelper *dbHelper = [AMPDatabaseHelper getDatabaseHelper];

id mockAmplitude = [OCMockObject partialMockForObject:self.amplitude];
NSDate *date = [NSDate dateWithTimeIntervalSince1970:1000];
[[[mockAmplitude expect] andReturnValue:OCMOCK_VALUE(date)] currentTime];
Expand Down Expand Up @@ -201,6 +204,83 @@ - (void)testTrackSessionEvents {
[mockAmplitude identify:identify outOfSession:YES];
[mockAmplitude flushQueue];
XCTAssertEqual([mockAmplitude queuedEventCount], 5); // does not trigger session events

// test new end session logic -> go to background
NSDate *date5 = [NSDate dateWithTimeIntervalSince1970:1000 + 4 * self.amplitude.minTimeBetweenSessionsMillis];
[[[mockAmplitude expect] andReturnValue:OCMOCK_VALUE(date5)] currentTime];
[mockAmplitude enterBackground]; // simulate app entering background
[mockAmplitude flushQueue];
XCTAssertEqual([mockAmplitude queuedEventCount], 5); // no actual events logged

// verify end session event is added to key value table
NSString *endSessionEventString = [dbHelper getValue:kAMPSessionEndEvent];
XCTAssertFalse([AMPUtils isEmptyString:endSessionEventString]);
NSDictionary *endSessionEvent = [AMPUtils deserializeEventString:endSessionEventString];
XCTAssertNotNil(endSessionEvent);
NSNumber *endSessionTimestamp = [endSessionEvent objectForKey:@"timestamp"];
NSNumber *expectedTimestamp = [NSNumber numberWithLongLong:[date5 timeIntervalSince1970] * 1000];
XCTAssertEqualObjects(expectedTimestamp, endSessionTimestamp);

// verify that the end session event sent is loaded from key value table
// we can modify the value in the database and verify the modified event is the one that is sent
// adding a value for version_name
endSessionEventString = @"{\"uuid\":\"CC83932C-7520-4AAD-BC95-8E59D30057D6\",\"device_manufacturer\":\"Apple\",\"library\":{\"name\":\"amplitude-ios\",\"version\":\"3.11.1\"},\"event_type\":\"session_end\",\"os_name\":\"ios\",\"sequence_number\":10,\"timestamp\":1201000000,\"event_properties\":{},\"api_properties\":{\"special\":\"session_end\",\"ios_idfv\":\"AFD750D7-1A23-44AB-A091-673A6229647A\"},\"groups\":{},\"user_properties\":{},\"platform\":\"iOS\",\"language\":\"English\",\"device_id\":\"AFD750D7-1A23-44AB-A091-673A6229647A\",\"os_version\":\"10.1\",\"session_id\":601000000,\"device_model\":\"Simulator\",\"country\":\"United States\",\"version_name\":\"test_version\"}";
[dbHelper insertOrReplaceKeyValue:kAMPSessionEndEvent value:endSessionEventString];

// force app to foreground and verify new session logic
NSDate *date6 = [NSDate dateWithTimeIntervalSince1970:1000 + 5 * self.amplitude.minTimeBetweenSessionsMillis];
[[[mockAmplitude expect] andReturnValue:OCMOCK_VALUE(date6)] currentTime];
[mockAmplitude enterForeground];
[mockAmplitude flushQueue];
XCTAssertEqual([mockAmplitude queuedEventCount], 7); // end and start session events logged

NSArray *events = [dbHelper getEvents:-1 limit:-1];
XCTAssertEqual([events count], 7);
endSessionEvent = [events objectAtIndex:5];
XCTAssertEqualObjects([endSessionEvent objectForKey:@"event_type"], kAMPSessionEndEvent);
XCTAssertEqualObjects([endSessionEvent objectForKey:@"timestamp"], expectedTimestamp);
XCTAssertEqualObjects([endSessionEvent objectForKey:@"version_name"], @"test_version");
XCTAssertNil([dbHelper getValue:kAMPSessionEndEvent]);

// the start session event should be missing the version_name
expectedTimestamp = [NSNumber numberWithLongLong:[date6 timeIntervalSince1970] * 1000];
NSDictionary *startSessionEvent = [events objectAtIndex:6];
XCTAssertEqualObjects([startSessionEvent objectForKey:@"event_type"], kAMPSessionStartEvent);
XCTAssertEqualObjects([startSessionEvent objectForKey:@"timestamp"], expectedTimestamp);
XCTAssertNil([startSessionEvent objectForKey:@"version_name"]);

// test new session logic -> verify saved end session event is discarded if the timestamp does not match
// the start session event logged at date6 should have updated the lastEventTime
[[[mockAmplitude expect] andReturnValue:OCMOCK_VALUE(date6)] currentTime];
[mockAmplitude enterBackground]; // simulate app entering background
[mockAmplitude flushQueue];
XCTAssertEqual([mockAmplitude queuedEventCount], 7); // no actual events logged

// if we try to re-send our modified endSessionEventString with date5, it should discard it and log a new one
endSessionEventString = @"{\"uuid\":\"CC83932C-7520-4AAD-BC95-8E59D30057D6\",\"device_manufacturer\":\"Apple\",\"library\":{\"name\":\"amplitude-ios\",\"version\":\"3.11.1\"},\"event_type\":\"session_end\",\"os_name\":\"ios\",\"sequence_number\":10,\"timestamp\":1201000000,\"event_properties\":{},\"api_properties\":{\"special\":\"session_end\",\"ios_idfv\":\"AFD750D7-1A23-44AB-A091-673A6229647A\"},\"groups\":{},\"user_properties\":{},\"platform\":\"iOS\",\"language\":\"English\",\"device_id\":\"AFD750D7-1A23-44AB-A091-673A6229647A\",\"os_version\":\"10.1\",\"session_id\":601000000,\"device_model\":\"Simulator\",\"country\":\"United States\",\"version_name\":\"test_version\"}";
[dbHelper insertOrReplaceKeyValue:kAMPSessionEndEvent value:endSessionEventString];

// force app to foreground and verify new session logic
NSDate *date7 = [NSDate dateWithTimeIntervalSince1970:1000 + 6 * self.amplitude.minTimeBetweenSessionsMillis];
[[[mockAmplitude expect] andReturnValue:OCMOCK_VALUE(date7)] currentTime];
[mockAmplitude enterForeground];
[mockAmplitude flushQueue];
XCTAssertEqual([mockAmplitude queuedEventCount], 9); // end and start session events logged

events = [dbHelper getEvents:-1 limit:-1];
XCTAssertEqual([events count], 9);
endSessionEvent = [events objectAtIndex:7];
XCTAssertEqualObjects([endSessionEvent objectForKey:@"event_type"], kAMPSessionEndEvent);
XCTAssertEqualObjects([endSessionEvent objectForKey:@"timestamp"], expectedTimestamp);
XCTAssertNil([endSessionEvent objectForKey:@"version_name"]); // should be missing the version_name since a brand new end session event is logged
XCTAssertNil([dbHelper getValue:kAMPSessionEndEvent]);

// the start session event should be missing the version_name
startSessionEvent = [events objectAtIndex:8];
XCTAssertEqualObjects([startSessionEvent objectForKey:@"event_type"], kAMPSessionStartEvent);
XCTAssertEqualObjects([startSessionEvent objectForKey:@"timestamp"], [NSNumber numberWithLongLong:[date7 timeIntervalSince1970] * 1000]);
XCTAssertNil([startSessionEvent objectForKey:@"version_name"]);

}

- (void)testSessionEventsOn32BitDevices {
Expand Down