Skip to content

Commit 0639b2e

Browse files
authored
feat: add before send callback (#114)
* feat: add before send callback * test: cover before send edge cases
1 parent f4ab337 commit 0639b2e

5 files changed

Lines changed: 127 additions & 1 deletion

File tree

.changeset/bright-owls-filter.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"com.posthog.unity": minor
3+
---
4+
5+
Add a beforeSend callback for modifying or dropping fully enriched events.

api/PostHog.PublicAPI.Shipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ namespace PostHogUnity
103103
{
104104
public PostHogConfig() { }
105105
public string ApiKey { get; set; }
106+
public System.Func<PostHogUnity.PostHogEvent, PostHogUnity.PostHogEvent> BeforeSend { get; set; }
106107
public bool CaptureApplicationLifecycleEvents { get; set; }
107108
public bool CaptureExceptions { get; set; }
108109
public bool CaptureExceptionsInEditor { get; set; }

com.posthog.unity/Runtime/PostHogConfig.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ public class PostHogConfig
106106
/// </summary>
107107
public Action OnFeatureFlagsLoaded { get; set; }
108108

109+
/// <summary>
110+
/// Callback invoked after an event is fully enriched and before it is queued.
111+
/// Return the event (mutated or unchanged) to continue, or null to drop it.
112+
/// If the callback throws, the SDK logs the exception and drops the event.
113+
/// </summary>
114+
public Func<PostHogEvent, PostHogEvent> BeforeSend { get; set; }
115+
109116
/// <summary>
110117
/// Whether to flush events before the application quits.
111118
/// When true, the SDK uses Application.wantsToQuit to delay quitting

com.posthog.unity/Runtime/PostHogSDK.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,8 +414,15 @@ void CaptureInternal(string eventName, Dictionary<string, object> properties)
414414
eventProps["$groups"] = groups;
415415
}
416416

417-
// Create and enqueue the event
417+
// Create the fully enriched event, then give customers a final chance to modify or drop it.
418418
var evt = new PostHogEvent(eventName, _identityManager.DistinctId, eventProps);
419+
evt = RunBeforeSend(evt);
420+
if (evt == null)
421+
{
422+
PostHogLogger.Debug($"Event dropped by beforeSend: {eventName}");
423+
return;
424+
}
425+
419426
_eventQueue.Enqueue(evt);
420427

421428
// Touch session
@@ -424,6 +431,25 @@ void CaptureInternal(string eventName, Dictionary<string, object> properties)
424431
PostHogLogger.Debug($"Captured event: {eventName}");
425432
}
426433

434+
PostHogEvent RunBeforeSend(PostHogEvent evt)
435+
{
436+
var beforeSend = _config.BeforeSend;
437+
if (beforeSend == null)
438+
{
439+
return evt;
440+
}
441+
442+
try
443+
{
444+
return beforeSend(evt);
445+
}
446+
catch (Exception ex)
447+
{
448+
PostHogLogger.Error("beforeSend callback threw", ex);
449+
return null;
450+
}
451+
}
452+
427453
void AddSdkProperties(Dictionary<string, object> properties)
428454
{
429455
properties["$lib"] = SdkInfo.LibraryName;

tests/PostHog.Unity.Tests/PostHogSDKTests.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,93 @@ public void ResetsFeatureFlagCallTracking()
138138
}
139139
}
140140

141+
public class TheBeforeSendCallback
142+
{
143+
[Fact]
144+
public void CanModifyEventBeforeItIsQueued()
145+
{
146+
var config = new PostHogConfig
147+
{
148+
ApiKey = "test-api-key",
149+
BeforeSend = evt =>
150+
{
151+
evt.Properties.Remove("secret");
152+
evt.Properties["before_send"] = true;
153+
return evt;
154+
},
155+
};
156+
var sdk = CreateSdk(config);
157+
var evt = new PostHogEvent(
158+
"before-send-event",
159+
"distinct-id",
160+
new Dictionary<string, object>
161+
{
162+
["$lib"] = "posthog-unity",
163+
["$lib_version"] = "1.0.0",
164+
["$session_id"] = "session-id",
165+
["source"] = "super",
166+
["secret"] = "remove",
167+
}
168+
);
169+
170+
var processed = RunBeforeSend(sdk, evt);
171+
172+
Assert.NotNull(processed);
173+
Assert.True((bool)processed.Properties["before_send"]);
174+
Assert.False(processed.Properties.ContainsKey("secret"));
175+
}
176+
177+
[Theory]
178+
[MemberData(nameof(DropCallbacks))]
179+
public void CanDropEventBeforeItIsQueued(Func<PostHogEvent, PostHogEvent> beforeSend)
180+
{
181+
var config = new PostHogConfig { ApiKey = "test-api-key", BeforeSend = beforeSend };
182+
var sdk = CreateSdk(config);
183+
184+
var processed = RunBeforeSend(sdk, new PostHogEvent("drop-me", "distinct-id"));
185+
186+
Assert.Null(processed);
187+
}
188+
189+
public static IEnumerable<object[]> DropCallbacks()
190+
{
191+
yield return new object[] { (Func<PostHogEvent, PostHogEvent>)(_ => null) };
192+
yield return new object[]
193+
{
194+
(Func<PostHogEvent, PostHogEvent>)(
195+
_ => throw new InvalidOperationException("beforeSend failed")
196+
),
197+
};
198+
}
199+
200+
static PostHogSDK CreateSdk(PostHogConfig config)
201+
{
202+
var sdk = (PostHogSDK)RuntimeHelpers.GetUninitializedObject(typeof(PostHogSDK));
203+
SetField(sdk, "_config", config);
204+
return sdk;
205+
}
206+
207+
static PostHogEvent RunBeforeSend(PostHogSDK sdk, PostHogEvent evt)
208+
{
209+
var method = typeof(PostHogSDK).GetMethod(
210+
"RunBeforeSend",
211+
BindingFlags.Instance | BindingFlags.NonPublic
212+
);
213+
Assert.NotNull(method);
214+
return (PostHogEvent)method.Invoke(sdk, new object[] { evt });
215+
}
216+
217+
static void SetField(PostHogSDK sdk, string name, object value)
218+
{
219+
var field = typeof(PostHogSDK).GetField(
220+
name,
221+
BindingFlags.Instance | BindingFlags.NonPublic
222+
);
223+
Assert.NotNull(field);
224+
field.SetValue(sdk, value);
225+
}
226+
}
227+
141228
[Collection("UnityGlobals")]
142229
public class ThePublicApiMethods
143230
{

0 commit comments

Comments
 (0)