Skip to content

Commit a5c6a22

Browse files
authored
fix: normalize SDK timestamps to UTC (#289)
* fix: normalize SDK timestamps to UTC * fix: allow timestamp super property precedence * test: assert captured timestamps use UTC offset
1 parent 127997c commit a5c6a22

9 files changed

Lines changed: 56 additions & 27 deletions

File tree

.changeset/fuzzy-clocks-convert.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"PostHog": patch
3+
"PostHog.AspNetCore": patch
4+
---
5+
6+
Normalize captured event timestamps to the equivalent UTC instant.

src/PostHog/Api/CapturedEvent.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class CapturedEvent
1515
/// <param name="eventName">The name of the event.</param>
1616
/// <param name="distinctId">The identifier for the user.</param>
1717
/// <param name="properties">The properties to associate with the event.</param>
18-
/// <param name="timestamp">The ISO 8601 timestamp.</param>
18+
/// <param name="timestamp">The ISO 8601 timestamp. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
1919
public CapturedEvent(
2020
string eventName,
2121
string distinctId,
@@ -25,7 +25,7 @@ public CapturedEvent(
2525
Uuid = Guid.NewGuid().ToString();
2626
EventName = eventName;
2727
DistinctId = distinctId;
28-
Timestamp = timestamp;
28+
Timestamp = timestamp.ToUniversalTime();
2929

3030
Properties = properties?.Copy() ?? new Dictionary<string, object>();
3131

src/PostHog/Capture/CaptureExtensions.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public static bool Capture(
7676
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
7777
/// <param name="distinctId">The identifier you use for the user.</param>
7878
/// <param name="eventName">Human friendly name of the event. Recommended format [object] [verb] such as "Project created" or "User signed up".</param>
79-
/// <param name="timestamp">The timestamp when the event occurred.</param>
79+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
8080
/// <returns><c>true</c> if the event was successfully enqueued. Otherwise <c>false</c>.</returns>
8181
public static bool Capture(
8282
this IPostHogClient client,
@@ -97,7 +97,7 @@ public static bool Capture(
9797
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
9898
/// <param name="distinctId">The identifier you use for the user.</param>
9999
/// <param name="eventName">Human friendly name of the event. Recommended format [object] [verb] such as "Project created" or "User signed up".</param>
100-
/// <param name="timestamp">The timestamp when the event occurred.</param>
100+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
101101
/// <param name="properties">Optional: The properties to send along with the event.</param>
102102
/// <returns><c>true</c> if the event was successfully enqueued. Otherwise <c>false</c>.</returns>
103103
public static bool Capture(
@@ -120,7 +120,7 @@ public static bool Capture(
120120
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
121121
/// <param name="distinctId">The identifier you use for the user.</param>
122122
/// <param name="eventName">Human friendly name of the event. Recommended format [object] [verb] such as "Project created" or "User signed up".</param>
123-
/// <param name="timestamp">The timestamp when the event occurred.</param>
123+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
124124
/// <param name="groups">A set of groups to send with the event. The groups are identified by their group_type and group_key.</param>
125125
/// <returns><c>true</c> if the event was successfully enqueued. Otherwise <c>false</c>.</returns>
126126
public static bool Capture(
@@ -143,7 +143,7 @@ public static bool Capture(
143143
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
144144
/// <param name="distinctId">The identifier you use for the user.</param>
145145
/// <param name="eventName">Human friendly name of the event. Recommended format [object] [verb] such as "Project created" or "User signed up".</param>
146-
/// <param name="timestamp">The timestamp when the event occurred.</param>
146+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
147147
/// <param name="properties">Optional: The properties to send along with the event.</param>
148148
/// <param name="groups">Optional: Context of what groups are related to this event, example: { ["company"] = "id:5" }. Can be used to analyze companies instead of users.</param>
149149
/// <returns><c>true</c> if the event was successfully enqueued. Otherwise <c>false</c>.</returns>
@@ -168,7 +168,7 @@ public static bool Capture(
168168
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
169169
/// <param name="distinctId">The identifier you use for the user.</param>
170170
/// <param name="eventName">Human friendly name of the event. Recommended format [object] [verb] such as "Project created" or "User signed up".</param>
171-
/// <param name="timestamp">The timestamp when the event occurred.</param>
171+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
172172
/// <param name="sendFeatureFlags">If <c>true</c>, feature flags are sent with the captured event.</param>
173173
/// <returns><c>true</c> if the event was successfully enqueued. Otherwise <c>false</c>.</returns>
174174
[Obsolete("Prefer Capture(..., flags: snapshot, timestamp: timestamp) using a FeatureFlagEvaluations snapshot from EvaluateFlagsAsync. This overload will be removed in a future major version.", error: false)]
@@ -194,7 +194,7 @@ public static bool Capture(
194194
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
195195
/// <param name="distinctId">The identifier you use for the user.</param>
196196
/// <param name="eventName">Human friendly name of the event. Recommended format [object] [verb] such as "Project created" or "User signed up".</param>
197-
/// <param name="timestamp">The timestamp when the event occurred.</param>
197+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
198198
/// <param name="properties">Optional: The properties to send along with the event.</param>
199199
/// <param name="groups">Optional: Context of what groups are related to this event, example: { ["company"] = "id:5" }. Can be used to analyze companies instead of users.</param>
200200
/// <param name="sendFeatureFlags">Default: <c>false</c>. If <c>true</c>, feature flags are sent with the captured event.</param>

src/PostHog/ErrorTracking/CaptureExceptionExtensions.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public static bool CaptureException(
7676
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
7777
/// <param name="exception">The exception object that you want to capture.</param>
7878
/// <param name="distinctId">The identifier you use for the user.</param>
79-
/// <param name="timestamp">The timestamp when the event occurred.</param>
79+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
8080
/// <returns><c>true</c> if the exception event was successfully enqueued. Otherwise <c>false</c>.</returns>
8181
public static bool CaptureException(
8282
this IPostHogClient client,
@@ -97,7 +97,7 @@ public static bool CaptureException(
9797
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
9898
/// <param name="exception">The exception object that you want to capture.</param>
9999
/// <param name="distinctId">The identifier you use for the user.</param>
100-
/// <param name="timestamp">The timestamp when the event occurred.</param>
100+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
101101
/// <param name="properties">Optional: The properties to send along with the event.</param>
102102
/// <returns><c>true</c> if the exception event was successfully enqueued. Otherwise <c>false</c>.</returns>
103103
public static bool CaptureException(
@@ -120,7 +120,7 @@ public static bool CaptureException(
120120
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
121121
/// <param name="exception">The exception object that you want to capture.</param>
122122
/// <param name="distinctId">The identifier you use for the user.</param>
123-
/// <param name="timestamp">The timestamp when the event occurred.</param>
123+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
124124
/// <param name="groups">A set of groups to send with the event. The groups are identified by their group_type and group_key.</param>
125125
/// <returns><c>true</c> if the exception event was successfully enqueued. Otherwise <c>false</c>.</returns>
126126
public static bool CaptureException(
@@ -143,7 +143,7 @@ public static bool CaptureException(
143143
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
144144
/// <param name="exception">The exception object that you want to capture.</param>
145145
/// <param name="distinctId">The identifier you use for the user.</param>
146-
/// <param name="timestamp">The timestamp when the event occurred.</param>
146+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
147147
/// <param name="properties">Optional: The properties to send along with the event.</param>
148148
/// <param name="groups">Optional: A set of groups to send with the event. The groups are identified by their group_type and group_key.</param>
149149
/// <returns><c>true</c> if the exception event was successfully enqueued. Otherwise <c>false</c>.</returns>
@@ -168,7 +168,7 @@ public static bool CaptureException(
168168
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
169169
/// <param name="exception">The exception object that you want to capture.</param>
170170
/// <param name="distinctId">The identifier you use for the user.</param>
171-
/// <param name="timestamp">The timestamp when the event occurred.</param>
171+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
172172
/// <param name="sendFeatureFlags">Default: <c>false</c>. If <c>true</c>, feature flags are sent with the captured event.</param>
173173
/// <returns><c>true</c> if the exception event was successfully enqueued. Otherwise <c>false</c>.</returns>
174174
[Obsolete("Prefer CaptureException(..., flags: snapshot, timestamp: timestamp) using a FeatureFlagEvaluations snapshot from EvaluateFlagsAsync. This overload will be removed in a future major version.", error: false)]
@@ -194,7 +194,7 @@ public static bool CaptureException(
194194
/// <param name="client">The <see cref="IPostHogClient"/>.</param>
195195
/// <param name="exception">The exception object that you want to capture.</param>
196196
/// <param name="distinctId">The identifier you use for the user.</param>
197-
/// <param name="timestamp">The timestamp when the event occurred.</param>
197+
/// <param name="timestamp">The timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
198198
/// <param name="properties">Optional: The properties to send along with the event.</param>
199199
/// <param name="groups">Optional: A set of groups to send with the event. The groups are identified by their group_type and group_key.</param>
200200
/// <param name="sendFeatureFlags">Default: <c>false</c>. If <c>true</c>, feature flags are sent with the captured event.</param>

src/PostHog/IPostHogClient.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ Task<ApiResult> GroupIdentifyAsync(
9393
/// <param name="properties">Optional: The properties to send along with the event.</param>
9494
/// <param name="groups">Optional: Context of what groups are related to this event, example: { ["company"] = "id:5" }. Can be used to analyze companies instead of users.</param>
9595
/// <param name="sendFeatureFlags">Default: <c>false</c>. If <c>true</c>, feature flags are sent with the captured event.</param>
96-
/// <param name="timestamp">Optional: Custom timestamp when the event occurred. If not provided, uses current time.</param>
96+
/// <param name="timestamp">Optional: Custom timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant. If not provided, uses current time.</param>
9797
/// <returns><c>true</c> if the event was successfully enqueued. Otherwise <c>false</c>.</returns>
9898
[Obsolete("Prefer Capture(..., flags: snapshot, ...) using a FeatureFlagEvaluations snapshot from EvaluateFlagsAsync — same payload, no extra /flags request. This overload will be removed in a future major version.", error: false)]
9999
bool Capture(
@@ -115,7 +115,7 @@ bool Capture(
115115
/// <param name="properties">Optional: The properties to send along with the event.</param>
116116
/// <param name="groups">Optional: Context of what groups are related to this event, example: { ["company"] = "id:5" }. Can be used to analyze companies instead of users.</param>
117117
/// <param name="flags">A snapshot of feature flag evaluations. When non-null, <c>$feature/&lt;key&gt;</c> and <c>$active_feature_flags</c> are attached from the snapshot — no <c>/flags</c> call is made.</param>
118-
/// <param name="timestamp">Optional: Custom timestamp when the event occurred. If not provided, uses current time.</param>
118+
/// <param name="timestamp">Optional: Custom timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant. If not provided, uses current time.</param>
119119
/// <returns><c>true</c> if the event was successfully enqueued. Otherwise <c>false</c>.</returns>
120120
bool Capture(
121121
string distinctId,
@@ -139,7 +139,7 @@ bool Capture(
139139
/// <param name="properties">Optional: The properties to send along with the event.</param>
140140
/// <param name="groups">Optional: Context of what groups are related to this event, example: { ["company"] = "id:5" }. Can be used to analyze companies instead of users.</param>
141141
/// <param name="sendFeatureFlags">Default: <c>false</c>. If <c>true</c>, feature flags are sent with the captured event.</param>
142-
/// <param name="timestamp">Optional: Custom timestamp when the event occurred. If not provided, uses current time</param>
142+
/// <param name="timestamp">Optional: Custom timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant. If not provided, uses current time.</param>
143143
/// <returns><c>true</c> if the exception event was successfully enqueued. Otherwise <c>false</c>.</returns>
144144
[Obsolete("Prefer CaptureException(..., flags: snapshot, ...) using a FeatureFlagEvaluations snapshot from EvaluateFlagsAsync — same payload, no extra /flags request. This overload will be removed in a future major version.", error: false)]
145145
bool CaptureException(
@@ -158,7 +158,7 @@ bool CaptureException(
158158
/// <param name="properties">Optional: The properties to send along with the event.</param>
159159
/// <param name="groups">Optional: Context of what groups are related to this event.</param>
160160
/// <param name="flags">A snapshot of feature flag evaluations. When non-null, <c>$feature/&lt;key&gt;</c> and <c>$active_feature_flags</c> are attached from the snapshot — no <c>/flags</c> call is made.</param>
161-
/// <param name="timestamp">Optional: Custom timestamp when the event occurred.</param>
161+
/// <param name="timestamp">Optional: Custom timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
162162
/// <returns><c>true</c> if the exception event was successfully enqueued. Otherwise <c>false</c>.</returns>
163163
bool CaptureException(
164164
Exception exception,

src/PostHog/PostHogClient.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ bool CaptureCore(
315315
// If custom timestamp provided, add it to properties
316316
if (timestamp.HasValue)
317317
{
318+
timestamp = timestamp.Value.ToUniversalTime();
318319
properties = AddTimestampToProperties(properties, timestamp.Value);
319320
}
320321

src/PostHog/PostHogSdk.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public static bool Capture(string distinctId, string eventName, Dictionary<strin
8989
/// <param name="properties">Optional properties to send along with the event.</param>
9090
/// <param name="groups">Optional groups related to this event.</param>
9191
/// <param name="sendFeatureFlags">Whether to send feature flag data with the event.</param>
92-
/// <param name="timestamp">Optional timestamp when the event occurred.</param>
92+
/// <param name="timestamp">Optional timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
9393
/// <returns><c>true</c> if the event was successfully enqueued; otherwise <c>false</c>.</returns>
9494
#pragma warning disable CS0618
9595
public static bool Capture(
@@ -119,7 +119,7 @@ public static bool CaptureException(Exception exception, string distinctId)
119119
/// <param name="properties">Optional properties to send along with the event.</param>
120120
/// <param name="groups">Optional groups related to this event.</param>
121121
/// <param name="sendFeatureFlags">Whether to send feature flag data with the event.</param>
122-
/// <param name="timestamp">Optional timestamp when the event occurred.</param>
122+
/// <param name="timestamp">Optional timestamp when the event occurred. UTC is preferred; non-UTC input is converted to the equivalent UTC instant.</param>
123123
/// <returns><c>true</c> if the exception event was successfully enqueued; otherwise <c>false</c>.</returns>
124124
#pragma warning disable CS0618
125125
public static bool CaptureException(

tests/UnitTests/Api/CapturedEventTests.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,14 @@ public void SetsEventName()
5353
}
5454

5555
[Fact]
56-
public void SetsTimestamp()
56+
public void ConvertsTimestampToUtcWithoutChangingTheInstant()
5757
{
58-
var timestamp = new DateTimeOffset(2024, 6, 15, 10, 30, 0, TimeSpan.Zero);
58+
var timestamp = new DateTimeOffset(2024, 6, 15, 10, 30, 0, TimeSpan.FromHours(5.5));
5959
var capturedEvent = new CapturedEvent("test-event", "user-1", null, timestamp);
6060

61-
Assert.Equal(timestamp, capturedEvent.Timestamp);
61+
Assert.Equal(new DateTimeOffset(2024, 6, 15, 5, 0, 0, TimeSpan.Zero), capturedEvent.Timestamp);
62+
Assert.Equal(TimeSpan.Zero, capturedEvent.Timestamp.Offset);
63+
Assert.Equal(timestamp.UtcTicks, capturedEvent.Timestamp.UtcTicks);
6264
}
6365

6466
[Fact]

0 commit comments

Comments
 (0)