Repository: Kinto/kinto (Python)

Issue:
What should `event.payload["timestamp"]` represent?
Refs #945 and #1731. The documentation describes the timestamp field as representing "the time of the event". However, as far as I can tell, the `timestamp` field in an event represents a collection timestamp calculated relatively early in the request, before the action that triggered event happened. It doesn't represent the time that the event happens, nor the current last_modified / collection_timestamp of anything reflected in the event.

I think I'd like to change the `timestamp` key in the payload to correspond to the updated collection timestamp of the `(parent_id, collection_id)` for which the event was fired.


Discussion:
I would agree with changing that indeed: use the current timestamp before serving the response instead of the one when the request kicks in.

We should still be able to have individual or intermediate timestamps in the `impact_records` attribute I believe.

More generally, we should tackle #945, the semantics of the «global» fields of an event are wrong. 

> Note: if someone reads this, the terms *collection* and *records* in this issue represents the internal notion of `kinto.core`, and applies to buckets, groups, collections, records, accounts... in kinto. See #710 
Thinking about this further, I think that although #1770 is a valid short-term fix, it feels a little odd for me to bless one specific attribute about the resource as "part of the payload". Maybe the right fix is for the payload to include the `model` or `resource` or something so that the recipient of the event can decide what attributes it wants.



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 3bf66cf88..7036010ba 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -10,6 +10,11 @@ This document describes changes between each past release.
 **Bug fixes**
 
 - Fix for adding extra OpenId providers (fixes #1509)
+- Change the meaning of ``event.payload["timestamp"]``. Previously it
+  was ``@reify``\ 'd, which meant that it was calculated from before
+  whatever thing triggered the event. Now we use a "fresh"
+  timestamp. (Fixes #1469.)
+
 
 10.1.0 (2018-09-17)
 -------------------
diff --git a/docs/core/notifications.rst b/docs/core/notifications.rst
index 2a3488e6c..423a19ff5 100644
--- a/docs/core/notifications.rst
+++ b/docs/core/notifications.rst
@@ -110,7 +110,8 @@ Payload
 The :class:`kinto.core.events.ResourceChanged` and :class:`kinto.core.events.AfterResourceChanged`
 events contain a ``payload`` attribute with the following information:
 
-- **timestamp**: the time of the event
+- **timestamp**: the collection timestamp of the ``(parent_id, resource_name)`` pair for
+  which this event was sent
 - **action**: what happened. 'create', 'update' or 'delete'
 - **uri**: the uri of the impacted resource
 - **user_id**: the authenticated user id
diff --git a/kinto/core/resource/__init__.py b/kinto/core/resource/__init__.py
index 03701cb7e..6d2f391b5 100644
--- a/kinto/core/resource/__init__.py
+++ b/kinto/core/resource/__init__.py
@@ -737,8 +737,13 @@ def postprocess(self, result, action=ACTIONS.READ, old=None):
         }
 
         parent_id = self.get_parent_id(self.request)
+        # Use self.model.timestamp() instead of self.timestamp because
+        # self.timestamp is @reify'd relatively early in the request,
+        # so doesn't correspond to any time that is relevant to the
+        # event. See #1769.
+        timestamp = self.model.timestamp()
         self.request.notify_resource_event(parent_id=parent_id,
-                                           timestamp=self.timestamp,
+                                           timestamp=timestamp,
                                            data=result,
                                            action=action,
                                            old=old)


Test patch:
diff --git a/tests/core/resource/test_events.py b/tests/core/resource/test_events.py
index 3d5643b99..0f73c6224 100644
--- a/tests/core/resource/test_events.py
+++ b/tests/core/resource/test_events.py
@@ -118,6 +118,18 @@ def test_put_sends_create_action(self):
         self.assertEqual(self.events[0].payload['action'],
                          ACTIONS.CREATE.value)
 
+    def test_put_event_has_sensible_timestamp(self):
+        body = {**self.body}
+        body['data']['id'] = record_id = str(uuid.uuid4())
+        record_url = self.get_item_url(record_id)
+        resp = self.app.put_json(record_url, body,
+                                 headers=self.headers, status=201)
+        self.assertEqual(len(self.events), 1)
+        self.assertEqual(self.events[0].payload['action'],
+                         ACTIONS.CREATE.value)
+        self.assertEqual(self.events[0].payload['timestamp'],
+                         resp.json['data']['last_modified'])
+
     def test_not_triggered_on_failed_put(self):
         body = {**self.body}
         body['data']['id'] = record_id = str(uuid.uuid4())