Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ import Foundation
distinctId: crashDistinctId,
properties: finalProperties,
timestamp: crashTimestamp,
options: savedContext["options"] as? [String: Any],
skipBuildProperties: true
)

Expand Down
3 changes: 3 additions & 0 deletions PostHog/ErrorTracking/PostHogMemoryExceptionReporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ import Foundation
)

var distinctId: String?
var options: [String: Any]?
if let pid = report.environment.pid,
let saved = contextStore.takeContext(pid: pid, notAfter: report.timeRange.end)
{
distinctId = saved["distinct_id"] as? String
options = saved["options"] as? [String: Any]
let eventProperties = saved["event_properties"] as? [String: Any] ?? [:]
properties = eventProperties.merging(properties) { _, new in new }
if let steps = saved[PostHogExceptionStepFields.stepsKey] as? [[String: Any]], !steps.isEmpty {
Expand All @@ -76,6 +78,7 @@ import Foundation
distinctId: distinctId,
properties: properties,
timestamp: report.timeRange.end,
options: options,
skipBuildProperties: true
)
hedgeLog("Memory exception report processed")
Expand Down
7 changes: 4 additions & 3 deletions PostHog/PostHogApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,12 @@ class PostHogApi {

if let statusCode, 200 ... 299 ~= statusCode {
// A 2xx whose results can't be read, including a transfer that failed
// after the status arrived, counts as delivered: the server already
// stored its ok events, and a resend would store them again.
// after the status arrived, fails the whole batch. It isn't retried:
// the server may have stored its ok events, and a resend would store them again.
let results = data.flatMap { fromJSONData($0) }?["results"] as? [String: Any]
if results == nil {
hedgeLog("Capture returned \(statusCode) without readable per-event results, treating \(uuids.count) events as delivered.")
dropped = uuids.count
hedgeLog("Capture returned \(statusCode) without readable per-event results, dropping \(dropped) events.")
}
// Events missing from the results count as delivered. A repeated UUID takes its
// first copy's result; capture counts the later copies it dropped in `duplicates_dropped`.
Expand Down
17 changes: 7 additions & 10 deletions PostHog/PostHogSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -787,14 +787,6 @@ let maxRetryDelay = 30.0
props["$is_identified"] = isIdentified
}

// Person processing is sent as the `process_person_profile` option, only when it's off
// (see `personProcessingOptions`). A registered or caller `$process_person_profile` wins
// over it when the send hoists the property. The crash-context snapshot stores only
// properties, so it keeps the property, which the send hoists.
if readOnlySession, !hasPersonProcessing() {
props["$process_person_profile"] = false
}

if config.disableGeoIp {
props["$geoip_disable"] = true
}
Expand Down Expand Up @@ -1866,7 +1858,8 @@ let maxRetryDelay = 30.0
finalProperties[PostHogExceptionStepFields.stepsKey] = steps
}

// Snapshots and pre-built crash events don't carry the SDK's person-processing option.
// Snapshots don't carry the SDK's person-processing option; pre-built crash events carry the
// one saved with their crash context.
let eventOptions = skipBuildProperties || isSnapshotEvent
? sanitizeDictionary(options)
: personProcessingOptions(sanitizeDictionary(options), properties: finalProperties)
Expand Down Expand Up @@ -3871,10 +3864,14 @@ let maxRetryDelay = 30.0

// Build crash context with identity info + event properties
// This structure allows crash reporting to reconstruct events with crash-time data
let context: [String: Any] = [
var context: [String: Any] = [
"distinct_id": distinctId,
"event_properties": eventProperties,
]
// Person processing is an option, not a property (see `personProcessingOptions`).
if let options = personProcessingOptions(nil, properties: eventProperties) {
context["options"] = options
}

onEventContextChanged.invoke(context)
}
Expand Down
43 changes: 27 additions & 16 deletions PostHogTests/PostHogCaptureV1Test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,26 @@ final class PostHogCaptureV1Test {
#expect(result.retryAfter == 2)
}

@Test("treats a 200 without a results map as delivered")
func malformedSuccessIsDelivered() async {
server.captureV1ResponseHandler = { _, _ in
HTTPStubsResponse(data: Data("not json".utf8), statusCode: 200, headers: nil)
}
@Test("drops the whole batch on a 200 without a results map, without retrying it")
func malformedSuccessDropsBatch() throws {
let wasLogging = hedgeLogEnabled
hedgeLogEnabled = true
defer { hedgeLogEnabled = wasLogging }
let uuids = [UUID().postHogUuidString, UUID().postHogUuidString]
let identity = CaptureV1RequestIdentity(uuids: Set(uuids), requestId: "request", attempt: 1, createdAt: "now")

let result = await send(makeApi(), [PostHogEvent(event: "test", distinctId: "user")])
var result: PostHogUploadInfo?
let output = try captureStdout {
result = makeApi().captureV1Result(
PostHogUploadInfo(statusCode: 200, error: nil), path: PostHogApi.captureV1AnalyticsPath,
data: Data("not json".utf8), uuids: uuids, identity: identity
)
}

#expect(result.statusCode == 200)
#expect(result.retryRecordIds?.isEmpty ?? true)
#expect(output.contains("Capture: 2 dropped, 0 duplicates dropped, 0 to retry."))
#expect(!output.contains("not json"))
#expect(result?.statusCode == 200)
#expect(result?.retryRecordIds?.isEmpty ?? true)
}

@Test("logs each dropped event's name, UUID and reason, without its properties")
Expand Down Expand Up @@ -209,8 +219,8 @@ final class PostHogCaptureV1Test {
#expect(output.contains("Capture: 0 dropped, 2 duplicates dropped, 0 to retry."))
}

@Test("treats a 2xx whose transfer then failed as delivered, so its ok events aren't stored twice")
func failedTransferAfterSuccessIsDelivered() {
@Test("doesn't retry a 2xx whose transfer then failed, so its ok events aren't stored twice")
func failedTransferAfterSuccessIsNotRetried() {
let uuids = [UUID().postHogUuidString, UUID().postHogUuidString]
let identity = CaptureV1RequestIdentity(uuids: Set(uuids), requestId: "request", attempt: 1, createdAt: "now")
let info = PostHogUploadInfo(statusCode: 200, error: URLError(.networkConnectionLost))
Expand Down Expand Up @@ -631,16 +641,17 @@ final class PostHogCaptureV1OptionsTest {
#expect((sent["properties"] as? [String: Any])?["$process_person_profile"] == nil)
}

@Test("the crash context keeps $process_person_profile false only when person processing is off")
func crashContextKeepsPersonlessProperty() {
for (personProfiles, expected) in [(PostHogPersonProfiles.never, false as Bool?), (.always, nil)] {
@Test("the crash context saves process_person_profile false as an option only when person processing is off")
func crashContextSavesPersonlessOption() {
for (personProfiles, expected) in [(PostHogPersonProfiles.never, ["process_person_profile": false] as [String: Bool]?), (.always, nil)] {
let sut = makeSDK(personProfiles: personProfiles)
var eventProperties: [String: Any]?
let token = sut.onEventContextChanged.subscribe { eventProperties = $0["event_properties"] as? [String: Any] }
var context: [String: Any]?
let token = sut.onEventContextChanged.subscribe { context = $0 }
sut.notifyContextDidChange()
withExtendedLifetime(token) {}
sut.close()
#expect(eventProperties?["$process_person_profile"] as? Bool == expected)
#expect(context?["options"] as? [String: Bool] == expected)
#expect((context?["event_properties"] as? [String: Any])?["$process_person_profile"] == nil)
}
}

Expand Down
2 changes: 2 additions & 0 deletions PostHogTests/PostHogMemoryExceptionReporterTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import Testing
store.write(try JSONSerialization.data(withJSONObject: [
"distinct_id": "killed-user",
"event_properties": ["$session_id": "killed-session", "$app_version": "9.9"],
"options": ["process_person_profile": false],
PostHogExceptionStepFields.stepsKey: [[PostHogExceptionStepFields.message: "opened gallery"]],
]))

Expand All @@ -112,6 +113,7 @@ import Testing
#expect(event.distinctId == "killed-user")
#expect(event.properties["$session_id"] as? String == "killed-session")
#expect(event.properties["$exception_level"] as? String == "fatal")
#expect(event.options["process_person_profile"] as? Bool == false)

let steps = event.properties[PostHogExceptionStepFields.stepsKey] as? [[String: Any]]
#expect(steps?.first?[PostHogExceptionStepFields.message] as? String == "opened gallery")
Expand Down
Loading