Skip to content

Commit d85a7cb

Browse files
committed
Add support for attaching true timestamp for events (close snowplow#161)
1 parent 043d2de commit d85a7cb

4 files changed

Lines changed: 130 additions & 27 deletions

File tree

snowplow_tracker/emitters.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
logger.setLevel(logging.INFO)
4040

4141
DEFAULT_MAX_LENGTH = 10
42-
PAYLOAD_DATA_SCHEMA = "iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-2"
42+
PAYLOAD_DATA_SCHEMA = "iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4"
4343

4444
new_contract("protocol", lambda x: x == "http" or x == "https")
4545

snowplow_tracker/test/integration/test_integration.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,23 @@
2020
"""
2121

2222
import unittest
23-
import time
2423
import re
25-
import redis
2624
import json
2725
import base64
28-
from snowplow_tracker import tracker, _version, emitters, subject
29-
from snowplow_tracker.self_describing_json import SelfDescribingJson
30-
from httmock import all_requests, HTTMock
31-
3226
try:
3327
from urllib.parse import unquote_plus # Python 3
34-
3528
except ImportError:
3629
from urllib import unquote_plus # Python 2
3730

31+
import redis
32+
from httmock import all_requests, HTTMock
33+
from freezegun import freeze_time
34+
35+
from snowplow_tracker import tracker, _version, emitters, subject
36+
from snowplow_tracker.timestamp import DeviceTimestamp, TrueTimestamp
37+
from snowplow_tracker.self_describing_json import SelfDescribingJson
38+
39+
3840
querystrings = [""]
3941

4042
default_emitter = emitters.Emitter("localhost", protocol="http", port=80)
@@ -81,7 +83,7 @@ def test_integration_page_view(self):
8183
t.track_page_view("http://savethearctic.org", "Save The Arctic", "http://referrer.com")
8284
expected_fields = {"e": "pv", "page": "Save+The+Arctic", "url": "http%3A%2F%2Fsavethearctic.org", "refr": "http%3A%2F%2Freferrer.com"}
8385
for key in expected_fields:
84-
self.assertEquals(from_querystring(key, querystrings[-1]), expected_fields[key])
86+
self.assertEquals(from_querystring(key, querystrings[-1]), expected_fields[key])
8587

8688
def test_integration_ecommerce_transaction_item(self):
8789
t = tracker.Tracker([default_emitter], default_subject)
@@ -95,15 +97,15 @@ def test_integration_ecommerce_transaction(self):
9597
t = tracker.Tracker([default_emitter], default_subject)
9698
with HTTMock(pass_response_content):
9799
t.track_ecommerce_transaction("6a8078be", 35, city="London", currency="GBP", items=
98-
[{
100+
[{
99101
"sku": "pbz0026",
100102
"price": 20,
101103
"quantity": 1
102104
},
103105
{
104106
"sku": "pbz0038",
105107
"price": 15,
106-
"quantity": 1
108+
"quantity": 1
107109
}])
108110

109111
expected_fields = {"e": "tr", "tr_id": "6a8078be", "tr_tt": "35", "tr_ci": "London", "tr_cu": "GBP"}
@@ -125,7 +127,7 @@ def test_integration_screen_view(self):
125127
with HTTMock(pass_response_content):
126128
t.track_screen_view("Game HUD 2", id_="534")
127129
expected_fields = {"e": "ue"}
128-
for key in expected_fields:
130+
for key in expected_fields:
129131
self.assertEquals(from_querystring(key, querystrings[-1]), expected_fields[key])
130132
envelope_string = from_querystring("ue_pr", querystrings[-1])
131133
envelope = json.loads(unquote_plus(envelope_string))
@@ -185,7 +187,7 @@ def test_integration_context_non_base64(self):
185187
"schema": "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-1",
186188
"data":[{"schema": "iglu:com.example/user/jsonschema/2-0-3", "data": {"user_type": "tester"}}]
187189
})
188-
190+
189191
def test_integration_context_base64(self):
190192
t = tracker.Tracker([default_emitter], default_subject, encode_base64=True)
191193
with HTTMock(pass_response_content):
@@ -196,7 +198,7 @@ def test_integration_context_base64(self):
196198
"schema": "iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-1",
197199
"data":[{"schema": "iglu:com.example/user/jsonschema/2-0-3", "data": {"user_type": "tester"}}]
198200
})
199-
201+
200202
def test_integration_standard_nv_pairs(self):
201203
s = subject.Subject()
202204
s.set_platform("mob")
@@ -280,7 +282,7 @@ def test_post_page_view(self):
280282
t.track_page_view("localhost", "local host", None)
281283
expected_fields = {"e": "pv", "page": "local host", "url": "localhost"}
282284
request = querystrings[-1]
283-
self.assertEquals(request["schema"], "iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-2")
285+
self.assertEquals(request["schema"], "iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4")
284286
for key in expected_fields:
285287
self.assertEquals(request["data"][0][key], expected_fields[key])
286288

@@ -292,3 +294,26 @@ def test_post_batched(self):
292294
t.track_struct_event("Test", "B")
293295
self.assertEquals(querystrings[-1]["data"][0]["se_ac"], "A")
294296
self.assertEquals(querystrings[-1]["data"][1]["se_ac"], "B")
297+
298+
def test_timestamps(self):
299+
emitter = emitters.Emitter("localhost", protocol="http", port=80, method='post', buffer_size=4)
300+
t = tracker.Tracker([emitter], default_subject)
301+
with HTTMock(pass_post_response_content):
302+
with freeze_time("2013-01-14 03:21:34"):
303+
t.track_page_view("localhost", "stamp0", None, tstamp=None)
304+
t.track_page_view("localhost", "stamp1", None, tstamp=1358933694000)
305+
t.track_page_view("localhost", "stamp2", None, tstamp=DeviceTimestamp(1458133694000))
306+
t.track_page_view("localhost", "stamp3", None, tstamp=TrueTimestamp(1458033694000))
307+
308+
expected_timestamps = [
309+
{"dtm": "1358133694000", "ttm": None},
310+
{"dtm": "1358933694000", "ttm": None},
311+
{"dtm": "1458133694000", "ttm": None},
312+
{"dtm": None, "ttm": "1458033694000"},
313+
]
314+
request = querystrings[-1]
315+
316+
for i, event in enumerate(expected_timestamps):
317+
self.assertEquals(request["data"][i].get("dtm"), expected_timestamps[i]["dtm"])
318+
self.assertEquals(request["data"][i].get("ttm"), expected_timestamps[i]["ttm"])
319+
self.assertEquals(request["data"][i].get("page"), "stamp" + str(i))

snowplow_tracker/timestamp.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
self_describing_json.py
3+
4+
Copyright (c) 2013-2016 Snowplow Analytics Ltd. All rights reserved.
5+
6+
This program is licensed to you under the Apache License Version 2.0,
7+
and you may not use this file except in compliance with the Apache License
8+
Version 2.0. You may obtain a copy of the Apache License Version 2.0 at
9+
http://www.apache.org/licenses/LICENSE-2.0.
10+
11+
Unless required by applicable law or agreed to in writing,
12+
software distributed under the Apache License Version 2.0 is distributed on
13+
an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
14+
express or implied. See the Apache License Version 2.0 for the specific
15+
language governing permissions and limitations there under.
16+
17+
Authors: Anuj More, Alex Dean, Fred Blundun, Anton Parkhomenko
18+
Copyright: Copyright (c) 2013-2014 Snowplow Analytics Ltd
19+
License: Apache License Version 2.0
20+
"""
21+
22+
from contracts import contract, new_contract
23+
24+
new_contract("ts_type", lambda x: x == "ttm" or x == "dtm")
25+
26+
27+
class Timestamp(object):
28+
@contract
29+
def __init__(self, ts_type, value):
30+
"""
31+
Construct base timestamp type
32+
33+
:param ts_type: one of possible timestamp types, according to
34+
tracker protocol
35+
:type ts_type: ts_type
36+
:param value: timestamp value in milliseconds
37+
:type value: int
38+
"""
39+
self.ts_type = ts_type
40+
self.value = value
41+
42+
43+
class TrueTimestamp(Timestamp):
44+
@contract
45+
def __init__(self, value):
46+
"""
47+
Construct true_timestamp (ttm)
48+
49+
:param value: timestamp value in milliseconds
50+
:type value: int
51+
"""
52+
super(TrueTimestamp, self).__init__("ttm", value)
53+
54+
55+
class DeviceTimestamp(Timestamp):
56+
@contract
57+
def __init__(self, value):
58+
"""
59+
Construct device_timestamp (dtm)
60+
61+
:param value: timestamp value in milliseconds
62+
:type value: int
63+
"""
64+
super(DeviceTimestamp, self).__init__("dtm", value)
65+

snowplow_tracker/tracker.py

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222
import time
2323
import uuid
2424
import six
25+
26+
from contracts import contract, new_contract
27+
2528
from snowplow_tracker import payload, _version, SelfDescribingJson
2629
from snowplow_tracker import subject as _subject
27-
from contracts import contract, new_contract
30+
from snowplow_tracker.timestamp import Timestamp, TrueTimestamp, DeviceTimestamp
2831

2932

3033
"""
@@ -49,6 +52,7 @@
4952
Tracker class
5053
"""
5154

55+
5256
class Tracker:
5357

5458
new_contract("not_none", lambda s: s is not None)
@@ -128,7 +132,7 @@ def get_timestamp(tstamp=None):
128132
"""
129133
if tstamp is None:
130134
return int(time.time() * 1000)
131-
elif isinstance(tstamp, (int, float)):
135+
elif isinstance(tstamp, (int, float, )):
132136
return int(tstamp)
133137

134138

@@ -160,11 +164,18 @@ def complete_payload(self, pb, context, tstamp):
160164
:param context: Custom context for the event
161165
:type context: context_array | None
162166
:param tstamp: Optional user-provided timestamp for the event
163-
:type tstamp: int | float | None
167+
:type tstamp: timestamp | int | float | None
164168
:rtype: tracker
165169
"""
166170
pb.add("eid", Tracker.get_uuid())
167-
pb.add("dtm", Tracker.get_timestamp(tstamp))
171+
172+
if isinstance(tstamp, TrueTimestamp):
173+
pb.add("ttm", tstamp.value)
174+
if isinstance(tstamp, DeviceTimestamp):
175+
pb.add("dtm", Tracker.get_timestamp(tstamp.value))
176+
elif isinstance(tstamp, (int, float, type(None))):
177+
pb.add("dtm", Tracker.get_timestamp(tstamp))
178+
168179
if context is not None:
169180
context_jsons = list(map(lambda c: c.to_json(), context))
170181
context_envelope = SelfDescribingJson(CONTEXT_SCHEMA, context_jsons).to_json()
@@ -187,6 +198,8 @@ def track_page_view(self, page_url, page_title=None, referrer=None, context=None
187198
:type referrer: string_or_none
188199
:param context: Custom context for the event
189200
:type context: context_array | None
201+
:param tstamp: Optional user-provided timestamp for the event
202+
:type tstamp: timestamp | int | float | None
190203
:rtype: tracker
191204
"""
192205
pb = payload.Payload()
@@ -217,7 +230,7 @@ def track_page_ping(self, page_url, page_title=None, referrer=None, min_x=None,
217230
:param context: Custom context for the event
218231
:type context: context_array | None
219232
:param tstamp: Optional user-provided timestamp for the event
220-
:type tstamp: int | float | None
233+
:type tstamp: timestamp | int | float | None
221234
:rtype: tracker
222235
"""
223236
pb = payload.Payload()
@@ -248,7 +261,7 @@ def track_link_click(self, target_url, element_id=None,
248261
:param context: Custom context for the event
249262
:type context: context_array | None
250263
:param tstamp: Optional user-provided timestamp for the event
251-
:type tstamp: int | float | None
264+
:type tstamp: timestamp | int | float | None
252265
:rtype: tracker
253266
"""
254267
properties = {}
@@ -286,7 +299,7 @@ def track_add_to_cart(self, sku, quantity, name=None, category=None,
286299
:param context: Custom context for the event
287300
:type context: context_array | None
288301
:param tstamp: Optional user-provided timestamp for the event
289-
:type tstamp: int | float | None
302+
:type tstamp: timestamp | int | float | None
290303
:rtype: tracker
291304
"""
292305
properties = {}
@@ -325,7 +338,7 @@ def track_remove_from_cart(self, sku, quantity, name=None, category=None,
325338
:param context: Custom context for the event
326339
:type context: context_array | None
327340
:param tstamp: Optional user-provided timestamp for the event
328-
:type tstamp: int | float | None
341+
:type tstamp: timestamp | int | float | None
329342
:rtype: tracker
330343
"""
331344
properties = {}
@@ -363,7 +376,7 @@ def track_form_change(self, form_id, element_id, node_name, value, type_=None,
363376
:param context: Custom context for the event
364377
:type context: context_array | None
365378
:param tstamp: Optional user-provided timestamp for the event
366-
:type tstamp: int | float | None
379+
:type tstamp: timestamp | int | float | None
367380
:rtype: tracker
368381
"""
369382
properties = dict()
@@ -393,7 +406,7 @@ def track_form_submit(self, form_id, form_classes=None, elements=None,
393406
:param context: Custom context for the event
394407
:type context: context_array | None
395408
:param tstamp: Optional user-provided timestamp for the event
396-
:type tstamp: int | float | None
409+
:type tstamp: timestamp | int | float | None
397410
:rtype: tracker
398411
"""
399412

@@ -423,7 +436,7 @@ def track_site_search(self, terms, filters=None, total_results=None,
423436
:param context: Custom context for the event
424437
:type context: context_array | None
425438
:param tstamp: Optional user-provided timestamp for the event
426-
:type tstamp: int | float | None
439+
:type tstamp: timestamp | int | float | None
427440
:rtype: tracker
428441
"""
429442
properties = {}
@@ -595,7 +608,7 @@ def track_unstruct_event(self, event_json, context=None, tstamp=None):
595608
:param context: Custom context for the event
596609
:type context: context_array | None
597610
:param tstamp: User-set timestamp
598-
:type tstamp: int | None
611+
:type tstamp: timestamp | int | None
599612
:rtype: tracker
600613
"""
601614

0 commit comments

Comments
 (0)