Skip to content

Commit e45ea34

Browse files
bsneedBrandon Sneed
andauthored
Bsneed/timestamps (#876)
* LIB-1656: Added nanosecond timestamps * Actually use the timestamp we’re carrying around. * In case the context is modified, preserve the timestamp. * Bump timestamp nanosecond precision to 9. * Only carry over the timestamp during a modify if there was one to begin with. * Added experimental options to configuration. * Added a second version of 8601 date creation. * Respect experimental value now present in config. * Added nanosecond time test. Co-authored-by: Brandon Sneed <[email protected]>
1 parent 0891ee3 commit e45ea34

File tree

13 files changed

+190
-70
lines changed

13 files changed

+190
-70
lines changed

Analytics.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,6 @@
6262
ReferencedContainer = "container:Analytics.xcodeproj">
6363
</BuildableReference>
6464
</TestableReference>
65-
<TestableReference
66-
skipped = "NO">
67-
<BuildableReference
68-
BuildableIdentifier = "primary"
69-
BlueprintIdentifier = "9D8CE58B23EE014E00197D0C"
70-
BuildableName = "AnalyticsTestsTVOS.xctest"
71-
BlueprintName = "AnalyticsTestsTVOS"
72-
ReferencedContainer = "container:Analytics.xcodeproj">
73-
</BuildableReference>
74-
</TestableReference>
7565
</Testables>
7666
</TestAction>
7767
<LaunchAction

Analytics/Classes/Integrations/SEGIdentifyPayload.m

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#import "SEGIdentifyPayload.h"
22

3+
@interface SEGIdentifyPayload ()
4+
@property (nonatomic, readwrite, nullable) NSString *anonymousId;
5+
@end
36

47
@implementation SEGIdentifyPayload
58

Analytics/Classes/Integrations/SEGIntegrationsManager.m

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,28 @@
3131
static NSString *const SEGCachedSettingsKey = @"analytics.settings.v2.plist";
3232

3333

34-
@interface SEGAnalyticsConfiguration (Private)
34+
@interface SEGIdentifyPayload (AnonymousId)
35+
@property (nonatomic, readwrite, nullable) NSString *anonymousId;
36+
@end
37+
38+
39+
@interface SEGPayload (Options)
40+
@property (readonly) NSDictionary *options;
41+
@end
42+
@implementation SEGPayload (Options)
43+
// Combine context and integrations to form options
44+
- (NSDictionary *)options
45+
{
46+
return @{
47+
@"context" : self.context ?: @{},
48+
@"integrations" : self.integrations ?: @{}
49+
};
50+
}
51+
@end
3552

36-
@property (nonatomic, strong) NSArray *factories;
3753

54+
@interface SEGAnalyticsConfiguration (Private)
55+
@property (nonatomic, strong) NSArray *factories;
3856
@end
3957

4058

@@ -153,10 +171,37 @@ - (NSString *)description
153171

154172
#pragma mark - Analytics API
155173

174+
- (void)identify:(SEGIdentifyPayload *)payload
175+
{
176+
NSCAssert2(payload.userId.length > 0 || payload.traits.count > 0, @"either userId (%@) or traits (%@) must be provided.", payload.userId, payload.traits);
177+
178+
NSString *anonymousId = payload.anonymousId;
179+
if (anonymousId) {
180+
[self saveAnonymousId:anonymousId];
181+
} else {
182+
anonymousId = self.cachedAnonymousId;
183+
}
184+
payload.anonymousId = anonymousId;
185+
186+
[self callIntegrationsWithSelector:NSSelectorFromString(@"identify:")
187+
arguments:@[ payload ]
188+
options:payload.options
189+
sync:false];
190+
}
191+
/*
156192
- (void)identify:(NSString *)userId traits:(NSDictionary *)traits options:(NSDictionary *)options
157193
{
158194
NSCAssert2(userId.length > 0 || traits.count > 0, @"either userId (%@) or traits (%@) must be provided.", userId, traits);
159195
196+
NSDictionary *options;
197+
if (p.anonymousId) {
198+
NSMutableDictionary *mutableOptions = [[NSMutableDictionary alloc] initWithDictionary:p.options];
199+
mutableOptions[@"anonymousId"] = p.anonymousId;
200+
options = [mutableOptions copy];
201+
} else {
202+
options = p.options;
203+
}
204+
160205
NSString *anonymousId = [options objectForKey:@"anonymousId"];
161206
if (anonymousId) {
162207
[self saveAnonymousId:anonymousId];
@@ -174,68 +219,49 @@ - (void)identify:(NSString *)userId traits:(NSDictionary *)traits options:(NSDic
174219
arguments:@[ payload ]
175220
options:options
176221
sync:false];
177-
}
222+
}*/
178223

179224
#pragma mark - Track
180225

181-
- (void)track:(NSString *)event properties:(NSDictionary *)properties options:(NSDictionary *)options
226+
- (void)track:(SEGTrackPayload *)payload
182227
{
183-
NSCAssert1(event.length > 0, @"event (%@) must not be empty.", event);
184-
185-
SEGTrackPayload *payload = [[SEGTrackPayload alloc] initWithEvent:event
186-
properties:SEGCoerceDictionary(properties)
187-
context:SEGCoerceDictionary([options objectForKey:@"context"])
188-
integrations:[options objectForKey:@"integrations"]];
228+
NSCAssert1(payload.event.length > 0, @"event (%@) must not be empty.", payload.event);
189229

190230
[self callIntegrationsWithSelector:NSSelectorFromString(@"track:")
191231
arguments:@[ payload ]
192-
options:options
232+
options:payload.options
193233
sync:false];
194234
}
195235

196236
#pragma mark - Screen
197237

198-
- (void)screen:(NSString *)screenTitle properties:(NSDictionary *)properties options:(NSDictionary *)options
238+
- (void)screen:(SEGScreenPayload *)payload
199239
{
200-
NSCAssert1(screenTitle.length > 0, @"screen name (%@) must not be empty.", screenTitle);
201-
202-
SEGScreenPayload *payload = [[SEGScreenPayload alloc] initWithName:screenTitle
203-
properties:SEGCoerceDictionary(properties)
204-
context:SEGCoerceDictionary([options objectForKey:@"context"])
205-
integrations:[options objectForKey:@"integrations"]];
240+
NSCAssert1(payload.name.length > 0, @"screen name (%@) must not be empty.", payload.name);
206241

207242
[self callIntegrationsWithSelector:NSSelectorFromString(@"screen:")
208243
arguments:@[ payload ]
209-
options:options
244+
options:payload.options
210245
sync:false];
211246
}
212247

213248
#pragma mark - Group
214249

215-
- (void)group:(NSString *)groupId traits:(NSDictionary *)traits options:(NSDictionary *)options
250+
- (void)group:(SEGGroupPayload *)payload
216251
{
217-
SEGGroupPayload *payload = [[SEGGroupPayload alloc] initWithGroupId:groupId
218-
traits:SEGCoerceDictionary(traits)
219-
context:SEGCoerceDictionary([options objectForKey:@"context"])
220-
integrations:[options objectForKey:@"integrations"]];
221-
222252
[self callIntegrationsWithSelector:NSSelectorFromString(@"group:")
223253
arguments:@[ payload ]
224-
options:options
254+
options:payload.options
225255
sync:false];
226256
}
227257

228258
#pragma mark - Alias
229259

230-
- (void)alias:(NSString *)newId options:(NSDictionary *)options
260+
- (void)alias:(SEGAliasPayload *)payload
231261
{
232-
SEGAliasPayload *payload = [[SEGAliasPayload alloc] initWithNewId:newId
233-
context:SEGCoerceDictionary([options objectForKey:@"context"])
234-
integrations:[options objectForKey:@"integrations"]];
235-
236262
[self callIntegrationsWithSelector:NSSelectorFromString(@"alias:")
237263
arguments:@[ payload ]
238-
options:options
264+
options:payload.options
239265
sync:false];
240266
}
241267

@@ -550,32 +576,15 @@ - (void)callIntegrationsWithSelector:(SEL)selector arguments:(NSArray *)argument
550576
@end
551577

552578

553-
@interface SEGPayload (Options)
554-
@property (readonly) NSDictionary *options;
555-
@end
556-
557-
558-
@implementation SEGPayload (Options)
559-
560-
// Combine context and integrations to form options
561-
- (NSDictionary *)options
562-
{
563-
return @{
564-
@"context" : self.context ?: @{},
565-
@"integrations" : self.integrations ?: @{}
566-
};
567-
}
568-
569-
@end
570-
571-
572579
@implementation SEGIntegrationsManager (SEGMiddleware)
573580

574581
- (void)context:(SEGContext *)context next:(void (^_Nonnull)(SEGContext *_Nullable))next
575582
{
576583
switch (context.eventType) {
577584
case SEGEventTypeIdentify: {
578585
SEGIdentifyPayload *p = (SEGIdentifyPayload *)context.payload;
586+
[self identify:p];
587+
/*
579588
NSDictionary *options;
580589
if (p.anonymousId) {
581590
NSMutableDictionary *mutableOptions = [[NSMutableDictionary alloc] initWithDictionary:p.options];
@@ -584,27 +593,27 @@ - (void)context:(SEGContext *)context next:(void (^_Nonnull)(SEGContext *_Nullab
584593
} else {
585594
options = p.options;
586595
}
587-
[self identify:p.userId traits:p.traits options:options];
596+
[self identify:p.userId traits:p.traits options:options];*/
588597
break;
589598
}
590599
case SEGEventTypeTrack: {
591600
SEGTrackPayload *p = (SEGTrackPayload *)context.payload;
592-
[self track:p.event properties:p.properties options:p.options];
601+
[self track:p];
593602
break;
594603
}
595604
case SEGEventTypeScreen: {
596605
SEGScreenPayload *p = (SEGScreenPayload *)context.payload;
597-
[self screen:p.name properties:p.properties options:p.options];
606+
[self screen:p];
598607
break;
599608
}
600609
case SEGEventTypeGroup: {
601610
SEGGroupPayload *p = (SEGGroupPayload *)context.payload;
602-
[self group:p.groupId traits:p.traits options:p.options];
611+
[self group:p];
603612
break;
604613
}
605614
case SEGEventTypeAlias: {
606615
SEGAliasPayload *p = (SEGAliasPayload *)context.payload;
607-
[self alias:p.theNewId options:p.options];
616+
[self alias:p];
608617
break;
609618
}
610619
case SEGEventTypeReset:

Analytics/Classes/Integrations/SEGPayload.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ NS_ASSUME_NONNULL_BEGIN
88

99
@property (nonatomic, readonly) JSON_DICT context;
1010
@property (nonatomic, readonly) JSON_DICT integrations;
11+
@property (nonatomic, strong) NSString *timestamp;
1112

1213
- (instancetype)initWithContext:(JSON_DICT)context integrations:(JSON_DICT)integrations;
1314

Analytics/Classes/Internal/SEGAnalyticsUtils.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ BOOL serializableDictionaryTypes(NSDictionary *dict);
1010

1111
// Date Utils
1212
NSString *iso8601FormattedString(NSDate *date);
13+
NSString *iso8601NanoFormattedString(NSDate *date);
1314

1415
void trimQueue(NSMutableArray *array, NSUInteger size);
1516

Analytics/Classes/Internal/SEGAnalyticsUtils.m

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,45 @@
33

44
static BOOL kAnalyticsLoggerShowLogs = NO;
55

6+
7+
@interface SEGISO8601NanosecondDateFormatter: NSDateFormatter
8+
@end
9+
10+
@implementation SEGISO8601NanosecondDateFormatter
11+
12+
- (id)init
13+
{
14+
self = [super init];
15+
self.dateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSS:'Z'";
16+
self.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
17+
self.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
18+
return self;
19+
}
20+
21+
const __SEG_NANO_MAX_LENGTH = 9;
22+
- (NSString * _Nonnull)stringFromDate:(NSDate *)date
23+
{
24+
NSCalendar *calendar = [NSCalendar currentCalendar];
25+
NSDateComponents *dateComponents = [calendar components:NSCalendarUnitSecond | NSCalendarUnitNanosecond fromDate:date];
26+
NSString *genericDateString = [super stringFromDate:date];
27+
28+
NSMutableArray *stringComponents = [[genericDateString componentsSeparatedByString:@"."] mutableCopy];
29+
NSString *nanoSeconds = [NSString stringWithFormat:@"%li", (long)dateComponents.nanosecond];
30+
31+
if (nanoSeconds.length > __SEG_NANO_MAX_LENGTH) {
32+
nanoSeconds = [nanoSeconds substringToIndex:__SEG_NANO_MAX_LENGTH];
33+
} else {
34+
nanoSeconds = [nanoSeconds stringByPaddingToLength:__SEG_NANO_MAX_LENGTH withString:@"0" startingAtIndex:0];
35+
}
36+
37+
NSString *result = [NSString stringWithFormat:@"%@.%@Z", stringComponents[0], nanoSeconds];
38+
39+
return result;
40+
}
41+
42+
@end
43+
44+
645
NSString *GenerateUUIDString()
746
{
847
CFUUIDRef theUUID = CFUUIDCreate(NULL);
@@ -11,7 +50,18 @@
1150
return UUIDString;
1251
}
1352

53+
1454
// Date Utils
55+
NSString *iso8601NanoFormattedString(NSDate *date)
56+
{
57+
static NSDateFormatter *dateFormatter;
58+
static dispatch_once_t onceToken;
59+
dispatch_once(&onceToken, ^{
60+
dateFormatter = [[SEGISO8601NanosecondDateFormatter alloc] init];
61+
});
62+
return [dateFormatter stringFromDate:date];
63+
}
64+
1565
NSString *iso8601FormattedString(NSDate *date)
1666
{
1767
static NSDateFormatter *dateFormatter;
@@ -25,6 +75,7 @@
2575
return [dateFormatter stringFromDate:date];
2676
}
2777

78+
2879
/** trim the queue so that it contains only upto `max` number of elements. */
2980
void trimQueue(NSMutableArray *queue, NSUInteger max)
3081
{

Analytics/Classes/Internal/SEGSegmentIntegration.m

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ - (void)identify:(SEGIdentifyPayload *)payload
368368

369369
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
370370
[dictionary setValue:payload.traits forKey:@"traits"];
371-
371+
[dictionary setValue:payload.timestamp forKey:@"timestamp"];
372372
[self enqueueAction:@"identify" dictionary:dictionary context:payload.context integrations:payload.integrations];
373373
}
374374

@@ -377,6 +377,7 @@ - (void)track:(SEGTrackPayload *)payload
377377
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
378378
[dictionary setValue:payload.event forKey:@"event"];
379379
[dictionary setValue:payload.properties forKey:@"properties"];
380+
[dictionary setValue:payload.timestamp forKey:@"timestamp"];
380381
[self enqueueAction:@"track" dictionary:dictionary context:payload.context integrations:payload.integrations];
381382
}
382383

@@ -385,6 +386,7 @@ - (void)screen:(SEGScreenPayload *)payload
385386
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
386387
[dictionary setValue:payload.name forKey:@"name"];
387388
[dictionary setValue:payload.properties forKey:@"properties"];
389+
[dictionary setValue:payload.timestamp forKey:@"timestamp"];
388390

389391
[self enqueueAction:@"screen" dictionary:dictionary context:payload.context integrations:payload.integrations];
390392
}
@@ -394,6 +396,7 @@ - (void)group:(SEGGroupPayload *)payload
394396
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
395397
[dictionary setValue:payload.groupId forKey:@"groupId"];
396398
[dictionary setValue:payload.traits forKey:@"traits"];
399+
[dictionary setValue:payload.timestamp forKey:@"timestamp"];
397400

398401
[self enqueueAction:@"group" dictionary:dictionary context:payload.context integrations:payload.integrations];
399402
}
@@ -403,6 +406,7 @@ - (void)alias:(SEGAliasPayload *)payload
403406
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
404407
[dictionary setValue:payload.theNewId forKey:@"userId"];
405408
[dictionary setValue:self.userId ?: [self.analytics getAnonymousId] forKey:@"previousId"];
409+
[dictionary setValue:payload.timestamp forKey:@"timestamp"];
406410

407411
[self enqueueAction:@"alias" dictionary:dictionary context:payload.context integrations:payload.integrations];
408412
}
@@ -457,9 +461,7 @@ - (NSDictionary *)integrationsDictionary:(NSDictionary *)integrations
457461
- (void)enqueueAction:(NSString *)action dictionary:(NSMutableDictionary *)payload context:(NSDictionary *)context integrations:(NSDictionary *)integrations
458462
{
459463
// attach these parts of the payload outside since they are all synchronous
460-
// and the timestamp will be more accurate.
461464
payload[@"type"] = action;
462-
payload[@"timestamp"] = iso8601FormattedString([NSDate date]);
463465
payload[@"messageId"] = GenerateUUIDString();
464466

465467
[self dispatchBackground:^{

Analytics/Classes/Middlewares/SEGContext.m

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ - (SEGContext *_Nonnull)modify:(void (^_Nonnull)(id<SEGMutableContext> _Nonnull
5555
// of immutable data structure without the cost of having to allocate and reallocate
5656
// objects over and over again.
5757
SEGContext *context = self.debug ? [self copy] : self;
58+
NSString *originalTimestamp = context.payload.timestamp;
5859
modify(context);
60+
if (originalTimestamp) {
61+
context.payload.timestamp = originalTimestamp;
62+
}
63+
5964
// TODO: We could probably add some validation here that the newly modified context
6065
// is actualy valid. For example, `eventType` should match `paylaod` class.
6166
// or anonymousId should never be null.

0 commit comments

Comments
 (0)