From 6bc7c9c2d662d7a7d4e4e6ae79804a8565be46da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 11:13:00 +0200 Subject: [PATCH 01/86] chore(deps): bump codecov/codecov-action from 4.1.1 to 4.2.0 (#3841) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4.1.1 to 4.2.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/c16abc29c95fcf9174b58eb7e1abf4c866893bc8...7afa10ed9b269c561c2336fd862446844e0cbf71) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9995aed4d3c..bead6123b98 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -246,7 +246,7 @@ jobs: # We don't upload codecov for scheduled runs as CodeCov only accepts a limited amount of uploads per commit. - name: Push code coverage to codecov id: codecov_1 - uses: codecov/codecov-action@c16abc29c95fcf9174b58eb7e1abf4c866893bc8 # pin@v4.1.1 + uses: codecov/codecov-action@7afa10ed9b269c561c2336fd862446844e0cbf71 # pin@v4.2.0 if: ${{ contains(matrix.platform, 'iOS') && !contains(github.ref, 'release') && github.event.schedule == '' }} with: # Although public repos should not have to specify a token there seems to be a bug with the Codecov GH action, which can @@ -258,7 +258,7 @@ jobs: # Sometimes codecov uploads etc can fail. Retry one time to rule out e.g. intermittent network failures. - name: Push code coverage to codecov id: codecov_2 - uses: codecov/codecov-action@c16abc29c95fcf9174b58eb7e1abf4c866893bc8 # pin@v4.1.1 + uses: codecov/codecov-action@7afa10ed9b269c561c2336fd862446844e0cbf71 # pin@v4.2.0 if: ${{ steps.codecov_1.outcome == 'failure' && contains(matrix.platform, 'iOS') && !contains(github.ref, 'release') && github.event.schedule == '' }} with: token: ${{ secrets.CODECOV_TOKEN }} From c0f08e718b7ca8828429d95d382d548b9f300de7 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Tue, 9 Apr 2024 11:54:29 +0200 Subject: [PATCH 02/86] feat(metrics): Add rate limits (#3838) This adds the handling of rate limits for the new metric_bucket category including handling the metric namespace. Fixes GH-3805 --- CHANGELOG.md | 1 + Sources/Sentry/SentryDataCategoryMapper.m | 14 +++--- Sources/Sentry/SentryRateLimitParser.m | 20 ++++++++- Sources/Sentry/include/SentryDataCategory.h | 18 +------- .../Sentry/include/SentryDataCategoryMapper.h | 2 +- .../SentryDefaultRateLimitsTests.swift | 43 +++++++++++++++++++ .../SentryDataCategoryMapperTests.swift | 9 ++-- .../Networking/SentryHttpTransportTests.swift | 11 +++++ 8 files changed, 88 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d63aae1f398..8a7b9cf5335 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Add timing API for Metrics (#3812): +- Add [rate limiting](https://develop.sentry.dev/sdk/rate-limiting/) for Metrics (#3838) ## 8.23.0 diff --git a/Sources/Sentry/SentryDataCategoryMapper.m b/Sources/Sentry/SentryDataCategoryMapper.m index 68a2724b642..b971bf19502 100644 --- a/Sources/Sentry/SentryDataCategoryMapper.m +++ b/Sources/Sentry/SentryDataCategoryMapper.m @@ -11,7 +11,7 @@ NSString *const kSentryDataCategoryNameAttachment = @"attachment"; NSString *const kSentryDataCategoryNameUserFeedback = @"user_report"; NSString *const kSentryDataCategoryNameProfile = @"profile"; -NSString *const kSentryDataCategoryNameStatsd = @"statsd"; +NSString *const kSentryDataCategoryNameMetricBucket = @"metric_bucket"; NSString *const kSentryDataCategoryNameUnknown = @"unknown"; NS_ASSUME_NONNULL_BEGIN @@ -34,8 +34,10 @@ if ([itemType isEqualToString:SentryEnvelopeItemTypeProfile]) { return kSentryDataCategoryProfile; } + // The envelope item type used for metrics is statsd whereas the client report category for + // discarded events is metric_bucket. if ([itemType isEqualToString:SentryEnvelopeItemTypeStatsd]) { - return kSentryDataCategoryStatsd; + return kSentryDataCategoryMetricBucket; } return kSentryDataCategoryDefault; } @@ -77,8 +79,8 @@ if ([value isEqualToString:kSentryDataCategoryNameProfile]) { return kSentryDataCategoryProfile; } - if ([value isEqualToString:kSentryDataCategoryNameStatsd]) { - return kSentryDataCategoryStatsd; + if ([value isEqualToString:kSentryDataCategoryNameMetricBucket]) { + return kSentryDataCategoryMetricBucket; } return kSentryDataCategoryUnknown; @@ -108,8 +110,8 @@ return kSentryDataCategoryNameUserFeedback; case kSentryDataCategoryProfile: return kSentryDataCategoryNameProfile; - case kSentryDataCategoryStatsd: - return kSentryDataCategoryNameStatsd; + case kSentryDataCategoryMetricBucket: + return kSentryDataCategoryNameMetricBucket; case kSentryDataCategoryUnknown: return kSentryDataCategoryNameUnknown; } diff --git a/Sources/Sentry/SentryRateLimitParser.m b/Sources/Sentry/SentryRateLimitParser.m index 92b22708961..df2931e6617 100644 --- a/Sources/Sentry/SentryRateLimitParser.m +++ b/Sources/Sentry/SentryRateLimitParser.m @@ -45,8 +45,24 @@ - (instancetype)initWithCurrentDateProvider:(SentryCurrentDateProvider *)current } for (NSNumber *category in [self parseCategories:parameters[1]]) { - rateLimits[category] = [self getLongerRateLimit:rateLimits[category] - andRateLimitInSeconds:rateLimitInSeconds]; + SentryDataCategory dataCategory + = sentryDataCategoryForNSUInteger(category.integerValue); + + // Namespaces should only be available for MetricBucket + if (dataCategory == kSentryDataCategoryMetricBucket && parameters.count > 4) { + NSString *namespacesAsString = parameters[4]; + + NSArray *namespaces = + [namespacesAsString componentsSeparatedByString:@";"]; + if (namespacesAsString.length == 0 || [namespaces containsObject:@"custom"]) { + rateLimits[category] = [self getLongerRateLimit:rateLimits[category] + andRateLimitInSeconds:rateLimitInSeconds]; + } + + } else { + rateLimits[category] = [self getLongerRateLimit:rateLimits[category] + andRateLimitInSeconds:rateLimitInSeconds]; + } } } diff --git a/Sources/Sentry/include/SentryDataCategory.h b/Sources/Sentry/include/SentryDataCategory.h index 9760944f3b8..67da2b9ff49 100644 --- a/Sources/Sentry/include/SentryDataCategory.h +++ b/Sources/Sentry/include/SentryDataCategory.h @@ -14,22 +14,6 @@ typedef NS_ENUM(NSUInteger, SentryDataCategory) { kSentryDataCategoryAttachment = 5, kSentryDataCategoryUserFeedback = 6, kSentryDataCategoryProfile = 7, - kSentryDataCategoryStatsd = 8, + kSentryDataCategoryMetricBucket = 8, kSentryDataCategoryUnknown = 9 }; - -static DEPRECATED_MSG_ATTRIBUTE( - "Use one of the functions to convert between literals and enum cases in " - "SentryDataCategoryMapper instead.") NSString *_Nonnull const SentryDataCategoryNames[] - = { - @"", // empty on purpose - @"default", - @"error", - @"session", - @"transaction", - @"attachment", - @"user_report", - @"profile", - @"statsd", - @"unkown", - }; diff --git a/Sources/Sentry/include/SentryDataCategoryMapper.h b/Sources/Sentry/include/SentryDataCategoryMapper.h index cfe39dd0bd3..021f99e71e1 100644 --- a/Sources/Sentry/include/SentryDataCategoryMapper.h +++ b/Sources/Sentry/include/SentryDataCategoryMapper.h @@ -11,7 +11,7 @@ FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameTransaction; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameAttachment; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameUserFeedback; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameProfile; -FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameStatsd; +FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameMetricBucket; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameUnknown; SentryDataCategory sentryDataCategoryForNSUInteger(NSUInteger value); diff --git a/Tests/SentryTests/Networking/RateLimits/SentryDefaultRateLimitsTests.swift b/Tests/SentryTests/Networking/RateLimits/SentryDefaultRateLimitsTests.swift index 65a0a103c49..d87b9ca964f 100644 --- a/Tests/SentryTests/Networking/RateLimits/SentryDefaultRateLimitsTests.swift +++ b/Tests/SentryTests/Networking/RateLimits/SentryDefaultRateLimitsTests.swift @@ -1,3 +1,4 @@ +import Nimble @testable import Sentry import SentryTestUtils import XCTest @@ -168,4 +169,46 @@ class SentryDefaultRateLimitsTests: XCTestCase { XCTAssertFalse(sut.isRateLimitActive(SentryDataCategory.transaction)) XCTAssertFalse(sut.isRateLimitActive(SentryDataCategory.attachment)) } + + func testMetricBucket() { + let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:metric_bucket:::custom") + + sut.update(response) + expect(self.sut.isRateLimitActive(SentryDataCategory.metricBucket)) == true + } + + func testMetricBucket_NoNamespace() { + let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:metric_bucket::") + + sut.update(response) + expect(self.sut.isRateLimitActive(SentryDataCategory.metricBucket)) == true + } + + func testMetricBucket_EmptyNamespace() { + let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:metric_bucket:::") + + sut.update(response) + expect(self.sut.isRateLimitActive(SentryDataCategory.metricBucket)) == true + } + + func testMetricBucket_NamespaceExclusivelyThanOtherCustom() { + let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:metric_bucket:organization:quota_exceeded:customs;cust") + + sut.update(response) + expect(self.sut.isRateLimitActive(SentryDataCategory.metricBucket)) == false + } + + func testMetricBucket_EmptyNamespaces() { + let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:metric_bucket:::;") + + sut.update(response) + expect(self.sut.isRateLimitActive(SentryDataCategory.metricBucket)) == false + } + + func testIgnoreNamespaceForNonMetricBucket() { + let response = TestResponseFactory.createRateLimitResponse(headerValue: "1:error:::customs;cust") + + sut.update(response) + expect(self.sut.isRateLimitActive(SentryDataCategory.error)) == true + } } diff --git a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift index c90430f103b..35b05344e2f 100644 --- a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift +++ b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift @@ -3,13 +3,14 @@ import Nimble import XCTest class SentryDataCategoryMapperTests: XCTestCase { + func testEnvelopeItemType() { expect(sentryDataCategoryForEnvelopItemType("event")) == .error expect(sentryDataCategoryForEnvelopItemType("session")) == .session expect(sentryDataCategoryForEnvelopItemType("transaction")) == .transaction expect(sentryDataCategoryForEnvelopItemType("attachment")) == .attachment expect(sentryDataCategoryForEnvelopItemType("profile")) == .profile - expect(sentryDataCategoryForEnvelopItemType("statsd")) == .statsd + expect(sentryDataCategoryForEnvelopItemType("statsd")) == .metricBucket expect(sentryDataCategoryForEnvelopItemType("unknown item type")) == .default } @@ -22,7 +23,7 @@ class SentryDataCategoryMapperTests: XCTestCase { expect(sentryDataCategoryForNSUInteger(5)) == .attachment expect(sentryDataCategoryForNSUInteger(6)) == .userFeedback expect(sentryDataCategoryForNSUInteger(7)) == .profile - expect(sentryDataCategoryForNSUInteger(8)) == .statsd + expect(sentryDataCategoryForNSUInteger(8)) == .metricBucket expect(sentryDataCategoryForNSUInteger(9)) == .unknown XCTAssertEqual(.unknown, sentryDataCategoryForNSUInteger(10), "Failed to map unknown category number to case .unknown") @@ -37,7 +38,7 @@ class SentryDataCategoryMapperTests: XCTestCase { expect(sentryDataCategoryForString(kSentryDataCategoryNameAttachment)) == .attachment expect(sentryDataCategoryForString(kSentryDataCategoryNameUserFeedback)) == .userFeedback expect(sentryDataCategoryForString(kSentryDataCategoryNameProfile)) == .profile - expect(sentryDataCategoryForString(kSentryDataCategoryNameStatsd)) == .statsd + expect(sentryDataCategoryForString(kSentryDataCategoryNameMetricBucket)) == .metricBucket expect(sentryDataCategoryForString(kSentryDataCategoryNameUnknown)) == .unknown XCTAssertEqual(.unknown, sentryDataCategoryForString("gdfagdfsa"), "Failed to map unknown category name to case .unknown") @@ -52,7 +53,7 @@ class SentryDataCategoryMapperTests: XCTestCase { expect(nameForSentryDataCategory(.attachment)) == kSentryDataCategoryNameAttachment expect(nameForSentryDataCategory(.userFeedback)) == kSentryDataCategoryNameUserFeedback expect(nameForSentryDataCategory(.profile)) == kSentryDataCategoryNameProfile - expect(nameForSentryDataCategory(.statsd)) == kSentryDataCategoryNameStatsd + expect(nameForSentryDataCategory(.metricBucket)) == kSentryDataCategoryNameMetricBucket expect(nameForSentryDataCategory(.unknown)) == kSentryDataCategoryNameUnknown } } diff --git a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift index 2cedc999834..291692a16f0 100644 --- a/Tests/SentryTests/Networking/SentryHttpTransportTests.swift +++ b/Tests/SentryTests/Networking/SentryHttpTransportTests.swift @@ -344,6 +344,17 @@ class SentryHttpTransportTests: XCTestCase { assertRateLimitUpdated(response: response) assertClientReportStoredInMemory() } + + func testSendEventWithMetricBucketRateLimitResponse() { + fixture.requestManager.nextError = NSError(domain: "something", code: 12) + + let response = givenRateLimitResponse(forCategory: SentryEnvelopeItemTypeSession) + + sendEvent() + + assertRateLimitUpdated(response: response) + assertClientReportStoredInMemory() + } func testSendEnvelopeWithRetryAfterResponse() { let response = givenRetryAfterResponse() From e8114e44bce4a12403c809dbf8440d48c9d9d644 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Wed, 10 Apr 2024 12:23:43 +0200 Subject: [PATCH 03/86] fix(metrics): Data Normalization (#3843) Update metrics data normalization to spec. Fixes GH-3829 --- CHANGELOG.md | 1 + Sources/Swift/Metrics/EncodeMetrics.swift | 33 ++++++++++++++----- .../Swift/Metrics/EncodeMetricTests.swift | 18 +++++++--- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a7b9cf5335..a84e2bc090b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add timing API for Metrics (#3812): - Add [rate limiting](https://develop.sentry.dev/sdk/rate-limiting/) for Metrics (#3838) +- Data normalization for Metrics (#3843) ## 8.23.0 diff --git a/Sources/Swift/Metrics/EncodeMetrics.swift b/Sources/Swift/Metrics/EncodeMetrics.swift index c66fc335c69..83b9762c115 100644 --- a/Sources/Swift/Metrics/EncodeMetrics.swift +++ b/Sources/Swift/Metrics/EncodeMetrics.swift @@ -10,10 +10,10 @@ func encodeToStatsd(flushableBuckets: [BucketTimestamp: [Metric]]) -> Data { let buckets = bucket.value for metric in buckets { - statsdString.append(sanitize(key: metric.key)) + statsdString.append(sanitize(metricKey: metric.key)) statsdString.append("@") - statsdString.append(metric.unit.unit) + statsdString.append(sanitize(metricUnit: metric.unit.unit)) for serializedValue in metric.serialize() { statsdString.append(":\(serializedValue)") @@ -24,7 +24,7 @@ func encodeToStatsd(flushableBuckets: [BucketTimestamp: [Metric]]) -> Data { var firstTag = true for (tagKey, tagValue) in metric.tags { - let sanitizedTagKey = sanitize(key: tagKey) + let sanitizedTagKey = sanitize(tagKey: tagKey) if firstTag { statsdString.append("|#") @@ -34,7 +34,7 @@ func encodeToStatsd(flushableBuckets: [BucketTimestamp: [Metric]]) -> Data { } statsdString.append("\(sanitizedTagKey):") - statsdString.append(sanitize(value: tagValue)) + statsdString.append(replaceTagValueCharacters(tagValue: tagValue)) } statsdString.append("|T") @@ -46,10 +46,27 @@ func encodeToStatsd(flushableBuckets: [BucketTimestamp: [Metric]]) -> Data { return statsdString.data(using: .utf8) ?? Data() } -private func sanitize(key: String) -> String { - return key.replacingOccurrences(of: "[^a-zA-Z0-9_/.-]+", with: "_", options: .regularExpression) +private func sanitize(metricUnit: String) -> String { + // We can't use \w because it includes chars like ä on Swift + return metricUnit.replacingOccurrences(of: "[^a-zA-Z0-9_]", with: "", options: .regularExpression) } -private func sanitize(value: String) -> String { - return value.replacingOccurrences(of: "[^\\w\\d\\s_:/@\\.\\{\\}\\[\\]$-]+", with: "", options: .regularExpression) +private func sanitize(metricKey: String) -> String { + // We can't use \w because it includes chars like ä on Swift + return metricKey.replacingOccurrences(of: "[^a-zA-Z0-9_.-]+", with: "_", options: .regularExpression) +} + +private func sanitize(tagKey: String) -> String { + // We can't use \w because it includes chars like ä on Swift + return tagKey.replacingOccurrences(of: "[^a-zA-Z0-9_/.-]+", with: "", options: .regularExpression) +} + +private func replaceTagValueCharacters(tagValue: String) -> String { + var result = tagValue.replacingOccurrences(of: "\\", with: #"\\\\"#) + result = result.replacingOccurrences(of: "\n", with: #"\\n"#) + result = result.replacingOccurrences(of: "\r", with: #"\\r"#) + result = result.replacingOccurrences(of: "\t", with: #"\\t"#) + result = result.replacingOccurrences(of: "|", with: #"\\u{7c}"#) + return result.replacingOccurrences(of: ",", with: #"\\u{2c}"#) + } diff --git a/Tests/SentryTests/Swift/Metrics/EncodeMetricTests.swift b/Tests/SentryTests/Swift/Metrics/EncodeMetricTests.swift index ae8e26de535..825c0ea576e 100644 --- a/Tests/SentryTests/Swift/Metrics/EncodeMetricTests.swift +++ b/Tests/SentryTests/Swift/Metrics/EncodeMetricTests.swift @@ -83,23 +83,31 @@ final class EncodeMetricTests: XCTestCase { let data = encodeToStatsd(flushableBuckets: [10_234: [counterMetric]]) - expect(data.decodeStatsd()) == "abyzABYZ09_/.-_a_a@second:10.1|c|T10234\n" + expect(data.decodeStatsd()) == "abyzABYZ09__.-_a_a@second:10.1|c|T10234\n" } func testEncodeCounterMetricWithTagKeyToSanitize() { - let counterMetric = CounterMetric(first: 10.1, key: "app.start", unit: MeasurementUnitDuration.second, tags: ["abyzABYZ09_/.-!@a#$Äa": "value"]) + let counterMetric = CounterMetric(first: 10.1, key: "app.start", unit: MeasurementUnitDuration.second, tags: ["abcABC123_-./äöü$%&abcABC123": "value"]) let data = encodeToStatsd(flushableBuckets: [10_234: [counterMetric]]) - expect(data.decodeStatsd()) == "app.start@second:10.1|c|#abyzABYZ09_/.-_a_a:value|T10234\n" + expect(data.decodeStatsd()) == "app.start@second:10.1|c|#abcABC123_-./abcABC123:value|T10234\n" } func testEncodeCounterMetricWithTagValueToSanitize() { - let counterMetric = CounterMetric(first: 10.1, key: "app.start", unit: MeasurementUnitDuration.second, tags: ["key": #"azAZ1 _:/@.{}[]$\%^&a*"#]) + let counterMetric = CounterMetric(first: 10.1, key: "app.start", unit: MeasurementUnitDuration.second, tags: ["key": "abc\n\r\t|,\\123"]) let data = encodeToStatsd(flushableBuckets: [10_234: [counterMetric]]) - expect(data.decodeStatsd()) == "app.start@second:10.1|c|#key:azAZ1 _:/@.{}[]$a|T10234\n" + expect(data.decodeStatsd()).to(contain(#"abc\\n\\r\\t\\u{7c}\\u{2c}\\\\123"#)) + } + + func testEncodeCounterMetricWithUnitToSanitize() { + let counterMetric = CounterMetric(first: 10.1, key: "app.start", unit: MeasurementUnit(unit: "abyzABYZ09_/.ä"), tags: [:]) + + let data = encodeToStatsd(flushableBuckets: [10_234: [counterMetric]]) + + expect(data.decodeStatsd()) == "app.start@abyzABYZ09_:10.1|c|T10234\n" } } From 3de9971d99752d282bfbb4b3d977295306275540 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Wed, 10 Apr 2024 12:04:02 -0800 Subject: [PATCH 04/86] ref(profiling): prepare for new public continuous API (#3833) --- Sentry.xcodeproj/project.pbxproj | 10 +- Sources/Sentry/PrivateSentrySDKOnly.mm | 2 +- .../Sentry/Profiling/SentryLaunchProfiling.m | 2 +- Sources/Sentry/SentryFramesTracker.m | 2 +- Sources/Sentry/SentryTracer.m | 2 +- .../Sentry/include/SentryProfiler+Private.h | 102 +++++++++++++++--- Sources/Sentry/include/SentryProfiler.h | 98 ----------------- .../SentryProfilerTests.mm | 2 +- Tests/SentryTests/SentryProfiler+Test.h | 15 ++- .../Transaction/SentryTracerObjCTests.m | 2 +- 10 files changed, 107 insertions(+), 130 deletions(-) delete mode 100644 Sources/Sentry/include/SentryProfiler.h diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 8014fa5f642..e40b0ce161b 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -11,7 +11,6 @@ 03BCC38A27E1BF49003232C7 /* SentryTime.h in Headers */ = {isa = PBXBuildFile; fileRef = 03BCC38927E1BF49003232C7 /* SentryTime.h */; }; 03BCC38C27E1C01A003232C7 /* SentryTime.mm in Sources */ = {isa = PBXBuildFile; fileRef = 03BCC38B27E1C01A003232C7 /* SentryTime.mm */; }; 03BCC38E27E2A377003232C7 /* SentryProfilingConditionals.h in Headers */ = {isa = PBXBuildFile; fileRef = 03BCC38D27E2A377003232C7 /* SentryProfilingConditionals.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 03F84D1D27DD414C008FE43F /* SentryProfiler.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F84D1127DD414C008FE43F /* SentryProfiler.h */; }; 03F84D1E27DD414C008FE43F /* SentryBacktrace.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 03F84D1227DD414C008FE43F /* SentryBacktrace.hpp */; }; 03F84D1F27DD414C008FE43F /* SentryAsyncSafeLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = 03F84D1327DD414C008FE43F /* SentryAsyncSafeLogging.h */; }; 03F84D2027DD414C008FE43F /* SentryStackBounds.hpp in Headers */ = {isa = PBXBuildFile; fileRef = 03F84D1427DD414C008FE43F /* SentryStackBounds.hpp */; }; @@ -672,7 +671,6 @@ 8489B8882A5F7905009A055A /* SentryThreadWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8489B8872A5F7905009A055A /* SentryThreadWrapperTests.swift */; }; 849AC40029E0C1FF00889C16 /* SentryFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849AC3FF29E0C1FF00889C16 /* SentryFormatterTests.swift */; }; 84A5D75B29D5170700388BFA /* TimeInterval+Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A5D75A29D5170700388BFA /* TimeInterval+Sentry.swift */; }; - 84A888FD28D9B11700C51DFD /* SentryProfiler+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 84A888FC28D9B11700C51DFD /* SentryProfiler+Private.h */; }; 84A8891C28DBD28900C51DFD /* SentryDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 84A8891A28DBD28900C51DFD /* SentryDevice.h */; }; 84A8891D28DBD28900C51DFD /* SentryDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 84A8891B28DBD28900C51DFD /* SentryDevice.mm */; }; 84A8892128DBD8D600C51DFD /* SentryDeviceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 84A8892028DBD8D600C51DFD /* SentryDeviceTests.mm */; }; @@ -926,7 +924,6 @@ 03BCC38927E1BF49003232C7 /* SentryTime.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryTime.h; path = Sources/Sentry/include/SentryTime.h; sourceTree = SOURCE_ROOT; }; 03BCC38B27E1C01A003232C7 /* SentryTime.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = SentryTime.mm; path = Sources/Sentry/SentryTime.mm; sourceTree = SOURCE_ROOT; }; 03BCC38D27E2A377003232C7 /* SentryProfilingConditionals.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryProfilingConditionals.h; path = ../Public/SentryProfilingConditionals.h; sourceTree = ""; }; - 03F84D1127DD414C008FE43F /* SentryProfiler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryProfiler.h; path = Sources/Sentry/include/SentryProfiler.h; sourceTree = SOURCE_ROOT; }; 03F84D1227DD414C008FE43F /* SentryBacktrace.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = SentryBacktrace.hpp; path = Sources/Sentry/include/SentryBacktrace.hpp; sourceTree = SOURCE_ROOT; }; 03F84D1327DD414C008FE43F /* SentryAsyncSafeLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryAsyncSafeLogging.h; path = Sources/Sentry/include/SentryAsyncSafeLogging.h; sourceTree = SOURCE_ROOT; }; 03F84D1427DD414C008FE43F /* SentryStackBounds.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = SentryStackBounds.hpp; path = Sources/Sentry/include/SentryStackBounds.hpp; sourceTree = SOURCE_ROOT; }; @@ -1605,6 +1602,7 @@ 840B7EEE2BBF2B23008B8120 /* .ruby-version */ = {isa = PBXFileReference; lastKnownFileType = text; path = ".ruby-version"; sourceTree = ""; }; 840B7EEF2BBF2B2B008B8120 /* .spi.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .spi.yml; sourceTree = ""; }; 840B7EF02BBF2B5F008B8120 /* MIGRATION.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = MIGRATION.md; sourceTree = ""; }; + 840B7EF22BBF83DF008B8120 /* SentryProfiler+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryProfiler+Private.h"; path = "Sources/Sentry/include/SentryProfiler+Private.h"; sourceTree = SOURCE_ROOT; }; 8419C0C328C1889D001C8259 /* SentryProfilerSwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryProfilerSwiftTests.swift; sourceTree = ""; }; 84281C422A578E5600EE88F2 /* SentryProfilerState.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryProfilerState.mm; sourceTree = ""; }; 84281C442A57905700EE88F2 /* SentrySample.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySample.h; path = ../include/SentrySample.h; sourceTree = ""; }; @@ -1663,7 +1661,6 @@ 849472842971C41A002603DE /* SentryNSTimerFactoryTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSTimerFactoryTest.swift; sourceTree = ""; }; 849AC3FF29E0C1FF00889C16 /* SentryFormatterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryFormatterTests.swift; sourceTree = ""; }; 84A5D75A29D5170700388BFA /* TimeInterval+Sentry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Sentry.swift"; sourceTree = ""; }; - 84A888FC28D9B11700C51DFD /* SentryProfiler+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SentryProfiler+Private.h"; path = "Sources/Sentry/include/SentryProfiler+Private.h"; sourceTree = SOURCE_ROOT; }; 84A8891A28DBD28900C51DFD /* SentryDevice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryDevice.h; path = include/SentryDevice.h; sourceTree = ""; }; 84A8891B28DBD28900C51DFD /* SentryDevice.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryDevice.mm; sourceTree = ""; }; 84A8892028DBD8D600C51DFD /* SentryDeviceTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SentryDeviceTests.mm; sourceTree = ""; }; @@ -3286,9 +3283,8 @@ 8454CF8B293EAF9A006AC140 /* SentryMetricProfiler.mm */, 84AF45A429A7FFA500FBB177 /* SentryProfiledTracerConcurrency.h */, 84AF45A529A7FFA500FBB177 /* SentryProfiledTracerConcurrency.mm */, - 03F84D1127DD414C008FE43F /* SentryProfiler.h */, + 840B7EF22BBF83DF008B8120 /* SentryProfiler+Private.h */, 03F84D2B27DD4191008FE43F /* SentryProfiler.mm */, - 84A888FC28D9B11700C51DFD /* SentryProfiler+Private.h */, 0354A22A2A134D9C003C3A04 /* SentryProfilerState.h */, 84281C422A578E5600EE88F2 /* SentryProfilerState.mm */, 84281C642A57D36100EE88F2 /* SentryProfilerState+ObjCpp.h */, @@ -3647,7 +3643,6 @@ 8E4E7C7425DAAB49006AB9E2 /* SentrySpanProtocol.h in Headers */, 8EC4CF4A25C38DAA0093DEE9 /* SentrySpanStatus.h in Headers */, 8ECC673D25C23996000E2BF6 /* SentrySpanId.h in Headers */, - 03F84D1D27DD414C008FE43F /* SentryProfiler.h in Headers */, 03F84D2227DD414C008FE43F /* SentryStackFrame.hpp in Headers */, 8ECC673E25C23996000E2BF6 /* SentrySpanContext.h in Headers */, 8ECC674025C23996000E2BF6 /* SentryTransactionContext.h in Headers */, @@ -3712,7 +3707,6 @@ 62862B1C2B1DDBC8009B16E3 /* SentryDelayedFrame.h in Headers */, 627E7589299F6FE40085504D /* SentryInternalDefines.h in Headers */, 7BE3C77B2446111500A38442 /* SentryRateLimitParser.h in Headers */, - 84A888FD28D9B11700C51DFD /* SentryProfiler+Private.h in Headers */, 7D0637032382B34300B30749 /* SentryScope.h in Headers */, 03F84D2727DD414C008FE43F /* SentryMachLogging.hpp in Headers */, 63295AF51EF3C7DB002D4490 /* SentryNSDictionarySanitize.h in Headers */, diff --git a/Sources/Sentry/PrivateSentrySDKOnly.mm b/Sources/Sentry/PrivateSentrySDKOnly.mm index f5a00f8707d..07d68dfdf70 100644 --- a/Sources/Sentry/PrivateSentrySDKOnly.mm +++ b/Sources/Sentry/PrivateSentrySDKOnly.mm @@ -10,7 +10,7 @@ #import "SentryMeta.h" #import "SentryOptions.h" #import "SentryProfiledTracerConcurrency.h" -#import "SentryProfiler.h" +#import "SentryProfiler+Private.h" #import "SentrySDK+Private.h" #import "SentrySerialization.h" #import "SentrySwift.h" diff --git a/Sources/Sentry/Profiling/SentryLaunchProfiling.m b/Sources/Sentry/Profiling/SentryLaunchProfiling.m index a526ab64ea2..8fbf006db56 100644 --- a/Sources/Sentry/Profiling/SentryLaunchProfiling.m +++ b/Sources/Sentry/Profiling/SentryLaunchProfiling.m @@ -8,7 +8,7 @@ # import "SentryInternalDefines.h" # import "SentryLog.h" # import "SentryOptions.h" -# import "SentryProfiler.h" +# import "SentryProfiler+Private.h" # import "SentryRandom.h" # import "SentrySamplerDecision.h" # import "SentrySampling.h" diff --git a/Sources/Sentry/SentryFramesTracker.m b/Sources/Sentry/SentryFramesTracker.m index be580efa019..ab68258d1ea 100644 --- a/Sources/Sentry/SentryFramesTracker.m +++ b/Sources/Sentry/SentryFramesTracker.m @@ -8,7 +8,7 @@ # import "SentryDispatchQueueWrapper.h" # import "SentryDisplayLinkWrapper.h" # import "SentryLog.h" -# import "SentryProfiler.h" +# import "SentryProfiler+Private.h" # import "SentryProfilingConditionals.h" # import "SentrySwift.h" # import "SentryTime.h" diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index b8714fb649b..1ee47db2095 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -36,7 +36,7 @@ #if SENTRY_TARGET_PROFILING_SUPPORTED # import "SentryLaunchProfiling.h" # import "SentryProfiledTracerConcurrency.h" -# import "SentryProfiler.h" +# import "SentryProfiler+Private.h" #endif // SENTRY_TARGET_PROFILING_SUPPORTED #if SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentryProfiler+Private.h b/Sources/Sentry/include/SentryProfiler+Private.h index 5eaee6d21d0..41235225837 100644 --- a/Sources/Sentry/include/SentryProfiler+Private.h +++ b/Sources/Sentry/include/SentryProfiler+Private.h @@ -1,38 +1,106 @@ -#import "SentryProfiler.h" #import "SentryProfilingConditionals.h" #if SENTRY_TARGET_PROFILING_SUPPORTED -@class SentryDebugMeta; -@class SentryId; -@class SentryProfilerState; -@class SentrySample; +# import "SentryCompiler.h" +# import "SentrySpan.h" +# import + +@class SentryEnvelopeItem; @class SentryHub; +@class SentryProfilerState; +@class SentryTransaction; + # if SENTRY_HAS_UIKIT +@class SentryFramesTracker; @class SentryScreenFrames; # endif // SENTRY_HAS_UIKIT -@class SentryTransaction; + +typedef NS_ENUM(NSUInteger, SentryProfilerTruncationReason) { + SentryProfilerTruncationReasonNormal, + SentryProfilerTruncationReasonTimeout, + SentryProfilerTruncationReasonAppMovedToBackground, +}; NS_ASSUME_NONNULL_BEGIN -NSMutableDictionary *serializedProfileData( - NSDictionary *profileData, uint64_t startSystemTime, uint64_t endSystemTime, - NSString *truncationReason, NSDictionary *serializedMetrics, - NSArray *debugMeta, SentryHub *hub -# if SENTRY_HAS_UIKIT - , - SentryScreenFrames *gpuData -# endif // SENTRY_HAS_UIKIT -); +SENTRY_EXTERN const int kSentryProfilerFrequencyHz; + +SENTRY_EXTERN NSString *const kSentryProfilerSerializationKeySlowFrameRenders; +SENTRY_EXTERN NSString *const kSentryProfilerSerializationKeyFrozenFrameRenders; +SENTRY_EXTERN NSString *const kSentryProfilerSerializationKeyFrameRates; + +SENTRY_EXTERN_C_BEGIN -@interface -SentryProfiler () +/** + * Disable profiling when running with TSAN because it produces a TSAN false positive, similar to + * the situation described here: https://github.com/envoyproxy/envoy/issues/2561 + */ +BOOL threadSanitizerIsPresent(void); + +NSString *profilerTruncationReasonName(SentryProfilerTruncationReason reason); + +SENTRY_EXTERN_C_END + +/** + * A wrapper around the low-level components used to gather sampled backtrace profiles. + * @warning A main assumption is that profile start/stop must be contained within range of time of + * the first concurrent transaction's start time and last one's end time. + */ +@interface SentryProfiler : NSObject + +@property (strong, nonatomic) SentryId *profilerId; @property (strong, nonatomic) SentryProfilerState *_state; + # if SENTRY_HAS_UIKIT @property (strong, nonatomic) SentryScreenFrames *_screenFrameData; # endif // SENTRY_HAS_UIKIT +/** + * Start a profiler, if one isn't already running. + */ ++ (BOOL)startWithTracer:(SentryId *)traceId; + +/** + * Stop the profiler if it is running. + */ +- (void)stopForReason:(SentryProfilerTruncationReason)reason; + +/** + * Whether the profiler instance is currently running. If not, then it probably timed out or aborted + * due to app backgrounding and is being kept alive while its associated transactions finish so they + * can query for its profile data. */ +- (BOOL)isRunning; + +/** + * Whether there is any profiler that is currently running. A convenience method to query for this + * information from other SDK components that don't have access to specific @c SentryProfiler + * instances. + */ ++ (BOOL)isCurrentlyProfiling; + +/** + * Immediately record a sample of profiling metrics. Helps get full coverage of concurrent spans + * when they're ended. + */ ++ (void)recordMetrics; + +/** + * Given a transaction, return an envelope item containing any corresponding profile data to be + * attached to the transaction envelope. + * */ ++ (nullable SentryEnvelopeItem *)createProfilingEnvelopeItemForTransaction: + (SentryTransaction *)transaction + startTimestamp:startTimestamp; + +/** + * Collect profile data corresponding with the given traceId and time period. + * */ ++ (nullable NSMutableDictionary *)collectProfileBetween:(uint64_t)startSystemTime + and:(uint64_t)endSystemTime + forTrace:(SentryId *)traceId + onHub:(SentryHub *)hub; @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryProfiler.h b/Sources/Sentry/include/SentryProfiler.h deleted file mode 100644 index cfd976d02bc..00000000000 --- a/Sources/Sentry/include/SentryProfiler.h +++ /dev/null @@ -1,98 +0,0 @@ -#import "SentryCompiler.h" -#import "SentryProfilingConditionals.h" -#import "SentrySpan.h" -#import - -@class SentryEnvelopeItem; -#if SENTRY_HAS_UIKIT -@class SentryFramesTracker; -#endif // SENTRY_HAS_UIKIT -@class SentryTransaction; -@class SentryHub; - -#if SENTRY_TARGET_PROFILING_SUPPORTED - -typedef NS_ENUM(NSUInteger, SentryProfilerTruncationReason) { - SentryProfilerTruncationReasonNormal, - SentryProfilerTruncationReasonTimeout, - SentryProfilerTruncationReasonAppMovedToBackground, -}; - -NS_ASSUME_NONNULL_BEGIN - -SENTRY_EXTERN const int kSentryProfilerFrequencyHz; - -SENTRY_EXTERN NSString *const kSentryProfilerSerializationKeySlowFrameRenders; -SENTRY_EXTERN NSString *const kSentryProfilerSerializationKeyFrozenFrameRenders; -SENTRY_EXTERN NSString *const kSentryProfilerSerializationKeyFrameRates; - -SENTRY_EXTERN_C_BEGIN - -/** - * Disable profiling when running with TSAN because it produces a TSAN false positive, similar to - * the situation described here: https://github.com/envoyproxy/envoy/issues/2561 - */ -BOOL threadSanitizerIsPresent(void); - -NSString *profilerTruncationReasonName(SentryProfilerTruncationReason reason); - -SENTRY_EXTERN_C_END - -/** - * A wrapper around the low-level components used to gather sampled backtrace profiles. - * @warning A main assumption is that profile start/stop must be contained within range of time of - * the first concurrent transaction's start time and last one's end time. - */ -@interface SentryProfiler : NSObject - -@property (strong, nonatomic) SentryId *profilerId; - -/** - * Start a profiler, if one isn't already running. - */ -+ (BOOL)startWithTracer:(SentryId *)traceId; - -/** - * Stop the profiler if it is running. - */ -- (void)stopForReason:(SentryProfilerTruncationReason)reason; - -/** - * Whether the profiler instance is currently running. If not, then it probably timed out or aborted - * due to app backgrounding and is being kept alive while its associated transactions finish so they - * can query for its profile data. */ -- (BOOL)isRunning; - -/** - * Whether there is any profiler that is currently running. A convenience method to query for this - * information from other SDK components that don't have access to specific @c SentryProfiler - * instances. - */ -+ (BOOL)isCurrentlyProfiling; - -/** - * Immediately record a sample of profiling metrics. Helps get full coverage of concurrent spans - * when they're ended. - */ -+ (void)recordMetrics; - -/** - * Given a transaction, return an envelope item containing any corresponding profile data to be - * attached to the transaction envelope. - * */ -+ (nullable SentryEnvelopeItem *)createProfilingEnvelopeItemForTransaction: - (SentryTransaction *)transaction - startTimestamp:startTimestamp; - -/** - * Collect profile data corresponding with the given traceId and time period. - * */ -+ (nullable NSMutableDictionary *)collectProfileBetween:(uint64_t)startSystemTime - and:(uint64_t)endSystemTime - forTrace:(SentryId *)traceId - onHub:(SentryHub *)hub; -@end - -NS_ASSUME_NONNULL_END - -#endif // SENTRY_TARGET_PROFILING_SUPPORTED diff --git a/Tests/SentryProfilerTests/SentryProfilerTests.mm b/Tests/SentryProfilerTests/SentryProfilerTests.mm index 05d1fe53209..40ee42289eb 100644 --- a/Tests/SentryProfilerTests/SentryProfilerTests.mm +++ b/Tests/SentryProfilerTests/SentryProfilerTests.mm @@ -15,7 +15,7 @@ using namespace sentry::profiling; -# import "SentryProfiler.h" +# import "SentryProfiler+Private.h" # import # import diff --git a/Tests/SentryTests/SentryProfiler+Test.h b/Tests/SentryTests/SentryProfiler+Test.h index 1d94abfc7e1..6f57f368659 100644 --- a/Tests/SentryTests/SentryProfiler+Test.h +++ b/Tests/SentryTests/SentryProfiler+Test.h @@ -1,13 +1,26 @@ -#import "SentryProfiler.h" #import "SentryProfilingConditionals.h" #if SENTRY_TARGET_PROFILING_SUPPORTED +# import "SentryProfiler+Private.h" + +@class SentryDebugMeta; + NS_ASSUME_NONNULL_BEGIN @interface SentryProfiler () +NSMutableDictionary *serializedProfileData( + NSDictionary *profileData, uint64_t startSystemTime, uint64_t endSystemTime, + NSString *truncationReason, NSDictionary *serializedMetrics, + NSArray *debugMeta, SentryHub *hub +# if SENTRY_HAS_UIKIT + , + SentryScreenFrames *gpuData +# endif // SENTRY_HAS_UIKIT +); + + (SentryProfiler *)getCurrentProfiler; + (void)resetConcurrencyTracking; diff --git a/Tests/SentryTests/Transaction/SentryTracerObjCTests.m b/Tests/SentryTests/Transaction/SentryTracerObjCTests.m index 3ddf1741033..df979ff337c 100644 --- a/Tests/SentryTests/Transaction/SentryTracerObjCTests.m +++ b/Tests/SentryTests/Transaction/SentryTracerObjCTests.m @@ -9,7 +9,7 @@ #import #if SENTRY_TARGET_PROFILING_SUPPORTED -# import "SentryProfiler.h" +# import "SentryProfiler+Private.h" #endif // SENTRY_TARGET_PROFILING_SUPPORTED @interface SentryTracerObjCTests : XCTestCase From ef4fec9dfb8dd5027b09a4a5c9362feafd118e1a Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 11 Apr 2024 09:09:14 +0000 Subject: [PATCH 05/86] release: 8.24.0 --- .github/last-release-runid | 2 +- CHANGELOG.md | 2 +- Package.swift | 8 ++++---- Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj | 8 ++++---- Sentry.podspec | 2 +- SentryPrivate.podspec | 2 +- SentrySwiftUI.podspec | 4 ++-- Sources/Configuration/Sentry.xcconfig | 2 +- Sources/Sentry/SentryMeta.m | 2 +- Tests/HybridSDKTest/HybridPod.podspec | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/last-release-runid b/.github/last-release-runid index eb163fc51a4..291227d752d 100644 --- a/.github/last-release-runid +++ b/.github/last-release-runid @@ -1 +1 @@ -8538785668 +8644123911 diff --git a/CHANGELOG.md b/CHANGELOG.md index a84e2bc090b..1c065daa139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## 8.24.0 ### Features diff --git a/Package.swift b/Package.swift index 17a4539bba3..ff68d443c9f 100644 --- a/Package.swift +++ b/Package.swift @@ -12,13 +12,13 @@ let package = Package( targets: [ .binaryTarget( name: "Sentry", - url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.23.0/Sentry.xcframework.zip", - checksum: "f6d5e846ee979671211ff526fe7600f7d7b6348940314b2b76e5b64901165e26" //Sentry-Static + url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.24.0/Sentry.xcframework.zip", + checksum: "e0348b8e112bcc3864831e7a631478c2028335615f4d88ef5a77c6423b34c7f5" //Sentry-Static ), .binaryTarget( name: "Sentry-Dynamic", - url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.23.0/Sentry-Dynamic.xcframework.zip", - checksum: "33ed13e177056530d3fb4fdecf48d573a631c776b08952b839cc4d5a7157f327" //Sentry-Dynamic + url: "https://github.com/getsentry/sentry-cocoa/releases/download/8.24.0/Sentry-Dynamic.xcframework.zip", + checksum: "1dea62b0c53fc4ca2fd475808a46b1978ad75f19c4a4b8f56fa3c564c000d33e" //Sentry-Dynamic ), .target ( name: "SentrySwiftUI", dependencies: ["Sentry", "SentryInternal"], diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj index 634538f37df..a918156e5a5 100644 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj +++ b/Samples/iOS-Swift/iOS-Swift.xcodeproj/project.pbxproj @@ -1248,7 +1248,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 8.23.0; + MARKETING_VERSION = 8.24.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift"; @@ -1277,7 +1277,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 8.23.0; + MARKETING_VERSION = 8.24.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.sentry.sample.iOS-Swift"; @@ -1926,7 +1926,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 8.23.0; + MARKETING_VERSION = 8.24.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match Development io.sentry.sample.iOS-Swift.Clip"; @@ -1961,7 +1961,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 8.23.0; + MARKETING_VERSION = 8.24.0; PRODUCT_BUNDLE_IDENTIFIER = "io.sentry.sample.iOS-Swift.Clip"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.sentry.sample.iOS-Swift.Clip"; diff --git a/Sentry.podspec b/Sentry.podspec index 7fcd496f89c..cb279f41b67 100644 --- a/Sentry.podspec +++ b/Sentry.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Sentry" - s.version = "8.23.0" + s.version = "8.24.0" s.summary = "Sentry client for cocoa" s.homepage = "https://github.com/getsentry/sentry-cocoa" s.license = "mit" diff --git a/SentryPrivate.podspec b/SentryPrivate.podspec index 54343fff10d..3ffa29113ae 100644 --- a/SentryPrivate.podspec +++ b/SentryPrivate.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SentryPrivate" - s.version = "8.23.0" + s.version = "8.24.0" s.summary = "Sentry Private Library." s.homepage = "https://github.com/getsentry/sentry-cocoa" s.license = "mit" diff --git a/SentrySwiftUI.podspec b/SentrySwiftUI.podspec index d417c3edbf2..efdbee5e131 100644 --- a/SentrySwiftUI.podspec +++ b/SentrySwiftUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SentrySwiftUI" - s.version = "8.23.0" + s.version = "8.24.0" s.summary = "Sentry client for SwiftUI" s.homepage = "https://github.com/getsentry/sentry-cocoa" s.license = "mit" @@ -19,5 +19,5 @@ Pod::Spec.new do |s| s.watchos.framework = 'WatchKit' s.source_files = "Sources/SentrySwiftUI/**/*.{swift,h,m}" - s.dependency 'Sentry', "8.23.0" + s.dependency 'Sentry', "8.24.0" end diff --git a/Sources/Configuration/Sentry.xcconfig b/Sources/Configuration/Sentry.xcconfig index fbfa8dda8f8..df757a8fe2c 100644 --- a/Sources/Configuration/Sentry.xcconfig +++ b/Sources/Configuration/Sentry.xcconfig @@ -2,7 +2,7 @@ PRODUCT_NAME = Sentry INFOPLIST_FILE = Sources/Resources/Info.plist PRODUCT_BUNDLE_IDENTIFIER = io.sentry.Sentry -CURRENT_PROJECT_VERSION = 8.23.0 +CURRENT_PROJECT_VERSION = 8.24.0 MODULEMAP_FILE = $(SRCROOT)/Sources/Resources/Sentry.modulemap diff --git a/Sources/Sentry/SentryMeta.m b/Sources/Sentry/SentryMeta.m index 6e6672cf548..4a1be9e7e39 100644 --- a/Sources/Sentry/SentryMeta.m +++ b/Sources/Sentry/SentryMeta.m @@ -5,7 +5,7 @@ @implementation SentryMeta // Don't remove the static keyword. If you do the compiler adds the constant name to the global // symbol table and it might clash with other constants. When keeping the static keyword the // compiler replaces all occurrences with the value. -static NSString *versionString = @"8.23.0"; +static NSString *versionString = @"8.24.0"; static NSString *sdkName = @"sentry.cocoa"; + (NSString *)versionString diff --git a/Tests/HybridSDKTest/HybridPod.podspec b/Tests/HybridSDKTest/HybridPod.podspec index c14101b3c4e..106fca65440 100644 --- a/Tests/HybridSDKTest/HybridPod.podspec +++ b/Tests/HybridSDKTest/HybridPod.podspec @@ -13,6 +13,6 @@ Pod::Spec.new do |s| s.requires_arc = true s.frameworks = 'Foundation' s.swift_versions = "5.5" - s.dependency "Sentry/HybridSDK", "8.23.0" + s.dependency "Sentry/HybridSDK", "8.24.0" s.source_files = "HybridTest.swift" end From 6ac15ddb95065f8841ad86a2fcc4467b7d7827b9 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 11 Apr 2024 15:24:36 +0200 Subject: [PATCH 06/86] chore: Organize project (#3847) - Organized files into groups. - Moved most used files to a better position in the project tree. Before and After ![image](https://github.com/getsentry/sentry-cocoa/assets/28265868/08e6d587-f9b3-4ff9-a705-70ccebbe8417) ![image](https://github.com/getsentry/sentry-cocoa/assets/28265868/6c438c84-53e9-4705-bfde-01521ff57f5a) So much clean when working ![image](https://github.com/getsentry/sentry-cocoa/assets/28265868/f9d62a0a-9be4-42b8-9e52-e70bc572e77f) _#skip-changelog_ --- .../xcschemes/iOS-ObjectiveC.xcscheme | 94 ---------------- .../xcschemes/iOS-SwiftClip.xcscheme | 85 -------------- .../xcschemes/iOS-SwiftUITests.xcscheme | 52 --------- Sentry.xcodeproj/project.pbxproj | 104 ++++++++++-------- 4 files changed, 59 insertions(+), 276 deletions(-) delete mode 100644 Samples/iOS-ObjectiveC/iOS-ObjectiveC.xcodeproj/xcshareddata/xcschemes/iOS-ObjectiveC.xcscheme delete mode 100644 Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-SwiftClip.xcscheme delete mode 100644 Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-SwiftUITests.xcscheme diff --git a/Samples/iOS-ObjectiveC/iOS-ObjectiveC.xcodeproj/xcshareddata/xcschemes/iOS-ObjectiveC.xcscheme b/Samples/iOS-ObjectiveC/iOS-ObjectiveC.xcodeproj/xcshareddata/xcschemes/iOS-ObjectiveC.xcscheme deleted file mode 100644 index f08e9a04e02..00000000000 --- a/Samples/iOS-ObjectiveC/iOS-ObjectiveC.xcodeproj/xcshareddata/xcschemes/iOS-ObjectiveC.xcscheme +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-SwiftClip.xcscheme b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-SwiftClip.xcscheme deleted file mode 100644 index 76a81269831..00000000000 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-SwiftClip.xcscheme +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-SwiftUITests.xcscheme b/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-SwiftUITests.xcscheme deleted file mode 100644 index 37a4ca1e81a..00000000000 --- a/Samples/iOS-Swift/iOS-Swift.xcodeproj/xcshareddata/xcschemes/iOS-SwiftUITests.xcscheme +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index e40b0ce161b..b871048fd73 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -1043,7 +1043,6 @@ 62E146D12BAAF55B00ED34FD /* LocalMetricsAggregatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalMetricsAggregatorTests.swift; sourceTree = ""; }; 62F226B629A37C120038080D /* SentryBooleanSerialization.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBooleanSerialization.m; sourceTree = ""; }; 62F226B829A37C270038080D /* SentryBooleanSerialization.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryBooleanSerialization.h; sourceTree = ""; }; - 62F605422B9A099100582E47 /* SentryCurrentDateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCurrentDateProvider.swift; sourceTree = ""; }; 630435FC1EBCA9D900C4D3FA /* SentryNSURLRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSURLRequest.h; path = include/SentryNSURLRequest.h; sourceTree = ""; }; 630435FD1EBCA9D900C4D3FA /* SentryNSURLRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSURLRequest.m; sourceTree = ""; }; 630436081EC0595B00C4D3FA /* SentryNSDataUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSDataUtils.h; path = include/SentryNSDataUtils.h; sourceTree = ""; }; @@ -1768,7 +1767,6 @@ D8199DCF29376FF40074249E /* SentrySwiftUI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SentrySwiftUI.xcconfig; sourceTree = ""; }; D8199DD029377C130074249E /* SentrySwiftUI.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SentrySwiftUI.podspec; sourceTree = ""; }; D81A346B291AECC7005A27A9 /* PrivateSentrySDKOnly.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PrivateSentrySDKOnly.h; path = include/HybridPublic/PrivateSentrySDKOnly.h; sourceTree = ""; }; - D81A349F291D5568005A27A9 /* SentryPrivate.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SentryPrivate.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; D81FDF10280EA0080045E0E4 /* SentryScreenShotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenShotTests.swift; sourceTree = ""; }; D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderSanitizer.swift; sourceTree = ""; }; D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; @@ -1809,6 +1807,7 @@ D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSURLSessionTaskSearchTests.swift; sourceTree = ""; }; D8757D142A209F7300BFEFCC /* SentrySampleDecision+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySampleDecision+Private.h"; path = "include/SentrySampleDecision+Private.h"; sourceTree = ""; }; D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryNSDataTrackerTests.swift; sourceTree = ""; }; + D878C6C02BC8048A0039D6A3 /* SentryPrivate.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SentryPrivate.podspec; sourceTree = ""; }; D880E3A628573E87008A90DB /* SentryBaggageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBaggageTests.swift; sourceTree = ""; }; D880E3B02860A5A0008A90DB /* SentryEvent+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryEvent+Private.h"; path = "include/SentryEvent+Private.h"; sourceTree = ""; }; D884A20327C80F2700074664 /* SentryCoreDataTrackerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackerTest.swift; sourceTree = ""; }; @@ -2154,43 +2153,16 @@ 6327C5C91EB8A783004E799B = { isa = PBXGroup; children = ( - 843BD6282AD8752300B0098F /* .clang-format */, - 84C47B2B2A09239100DAEB8A /* .codecov.yml */, - 844DA80628246D5000E6B62E /* .craft.yml */, - 840B7EEC2BBF2AFE008B8120 /* .gitattributes */, - 844A34C3282B278500C6D1DF /* .github */, - 844DA81628246D5000E6B62E /* .gitignore */, - 844DA80828246D5000E6B62E /* .gitmodules */, - 844DA80328246D5000E6B62E /* .oclint */, - 840B7EED2BBF2B16008B8120 /* .pre-commit-config.yaml */, - 840B7EEE2BBF2B23008B8120 /* .ruby-version */, - 844A3563282B3C9F00C6D1DF /* .sauce */, - 840B7EEA2BBF2ABA008B8120 /* .slather.yml */, - 840B7EEF2BBF2B2B008B8120 /* .spi.yml */, - 844DA80A28246D5000E6B62E /* .swiftlint.yml */, - 844DA80B28246D5000E6B62E /* Brewfile */, 844DA80C28246D5000E6B62E /* CHANGELOG.md */, - 844DA81028246D5000E6B62E /* CONTRIBUTING.md */, - 844DA81D28246DAE00E6B62E /* develop-docs */, - 844DA81E28246DB900E6B62E /* fastlane */, - 6304360C1EC05CEF00C4D3FA /* Frameworks */, - 844DA80728246D5000E6B62E /* Gemfile */, - 844DA81C28246D9300E6B62E /* Gemfile.lock */, - 844DA80E28246D5000E6B62E /* LICENSE.md */, - 844DA80428246D5000E6B62E /* Makefile */, - 840B7EF02BBF2B5F008B8120 /* MIGRATION.md */, - 844DA80D28246D5000E6B62E /* Package.swift */, - 6327C5D41EB8A783004E799B /* Products */, - 844DA80F28246D5000E6B62E /* README.md */, - D8105B37297A86B800299F03 /* Recovered References */, - 844DA81F28246DE300E6B62E /* scripts */, - 844DA80528246D5000E6B62E /* Sentry.podspec */, - D81A349F291D5568005A27A9 /* SentryPrivate.podspec */, - D8199DD029377C130074249E /* SentrySwiftUI.podspec */, - 8431F00B29B284F200D8DC56 /* SentryTestUtils */, - D84DAD4E2B17428D003CF120 /* SentryTestUtilsDynamic */, 63AA756E1EB8AEDB00D153DE /* Sources */, 63AA75921EB8AEDB00D153DE /* Tests */, + D878C6BF2BC803440039D6A3 /* Aux */, + D878C6C22BC8066D0039D6A3 /* Docs */, + D878C6C32BC807250039D6A3 /* Distribution */, + 8431F00B29B284F200D8DC56 /* SentryTestUtils */, + D84DAD4E2B17428D003CF120 /* SentryTestUtilsDynamic */, + 6304360C1EC05CEF00C4D3FA /* Frameworks */, + 6327C5D41EB8A783004E799B /* Products */, 7D826E3C2390840E00EED93D /* Utils */, ); indentWidth = 4; @@ -2352,10 +2324,10 @@ 63AA756E1EB8AEDB00D153DE /* Sources */ = { isa = PBXGroup; children = ( - D8199DB329376ECC0074249E /* SentrySwiftUI */, 63AA75C61EB8B06100D153DE /* Sentry */, D800942328F82E8D005D3943 /* Swift */, 63FE6FB920DA4C1000CDBAE8 /* SentryCrash */, + D8199DB329376ECC0074249E /* SentrySwiftUI */, 63AA75A31EB8AFDF00D153DE /* Configuration */, D8B0542F2A7D35F10056BAF6 /* Resources */, ); @@ -3461,14 +3433,6 @@ path = UIEvents; sourceTree = ""; }; - D8105B37297A86B800299F03 /* Recovered References */ = { - isa = PBXGroup; - children = ( - 62F605422B9A099100582E47 /* SentryCurrentDateProvider.swift */, - ); - name = "Recovered References"; - sourceTree = ""; - }; D8199DB329376ECC0074249E /* SentrySwiftUI */ = { isa = PBXGroup; children = ( @@ -3557,6 +3521,56 @@ path = IO; sourceTree = ""; }; + D878C6BF2BC803440039D6A3 /* Aux */ = { + isa = PBXGroup; + children = ( + 844DA80428246D5000E6B62E /* Makefile */, + 844DA81F28246DE300E6B62E /* scripts */, + 844DA81E28246DB900E6B62E /* fastlane */, + 844A34C3282B278500C6D1DF /* .github */, + 844DA80728246D5000E6B62E /* Gemfile */, + 844DA81C28246D9300E6B62E /* Gemfile.lock */, + 84C47B2B2A09239100DAEB8A /* .codecov.yml */, + 844DA80628246D5000E6B62E /* .craft.yml */, + 840B7EEC2BBF2AFE008B8120 /* .gitattributes */, + 844DA81628246D5000E6B62E /* .gitignore */, + 844DA80828246D5000E6B62E /* .gitmodules */, + 844DA80328246D5000E6B62E /* .oclint */, + 840B7EED2BBF2B16008B8120 /* .pre-commit-config.yaml */, + 840B7EEE2BBF2B23008B8120 /* .ruby-version */, + 844A3563282B3C9F00C6D1DF /* .sauce */, + 840B7EEA2BBF2ABA008B8120 /* .slather.yml */, + 840B7EEF2BBF2B2B008B8120 /* .spi.yml */, + 844DA80A28246D5000E6B62E /* .swiftlint.yml */, + 843BD6282AD8752300B0098F /* .clang-format */, + 844DA80B28246D5000E6B62E /* Brewfile */, + ); + name = Aux; + sourceTree = ""; + }; + D878C6C22BC8066D0039D6A3 /* Docs */ = { + isa = PBXGroup; + children = ( + 844DA81028246D5000E6B62E /* CONTRIBUTING.md */, + 844DA80E28246D5000E6B62E /* LICENSE.md */, + 840B7EF02BBF2B5F008B8120 /* MIGRATION.md */, + 844DA80F28246D5000E6B62E /* README.md */, + 844DA81D28246DAE00E6B62E /* develop-docs */, + ); + name = Docs; + sourceTree = ""; + }; + D878C6C32BC807250039D6A3 /* Distribution */ = { + isa = PBXGroup; + children = ( + 844DA80D28246D5000E6B62E /* Package.swift */, + 844DA80528246D5000E6B62E /* Sentry.podspec */, + D878C6C02BC8048A0039D6A3 /* SentryPrivate.podspec */, + D8199DD029377C130074249E /* SentrySwiftUI.podspec */, + ); + name = Distribution; + sourceTree = ""; + }; D884A20227C80F1300074664 /* CoreData */ = { isa = PBXGroup; children = ( From 1a86fb26a5ca6bd309d33bf541bcfacc75e451c9 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Thu, 11 Apr 2024 16:38:08 +0200 Subject: [PATCH 07/86] feat: Session Replay (#3625) Added session replay Co-authored-by: Philipp Hofmann --- CHANGELOG.md | 6 + Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 10 +- Sentry.podspec | 6 +- Sentry.xcodeproj/project.pbxproj | 150 +++++++++- SentryTestUtils/TestCurrentDateProvider.swift | 4 + SentryTestUtils/TestTransport.swift | 1 + Sources/Configuration/SentryNoUI.xcconfig | 5 + Sources/Sentry/Public/SentryOptions.h | 11 +- Sources/Sentry/SentryBaseIntegration.m | 14 + Sources/Sentry/SentryClient.m | 56 +++- Sources/Sentry/SentryCoreGraphicsHelper.m | 18 ++ Sources/Sentry/SentryDataCategoryMapper.m | 9 + Sources/Sentry/SentryDateUtil.m | 5 + Sources/Sentry/SentryEnvelope.m | 40 +++ Sources/Sentry/SentryHub.m | 10 + Sources/Sentry/SentryMsgPackSerializer.m | 110 +++++++ Sources/Sentry/SentryOptions.m | 18 +- Sources/Sentry/SentryReplayEvent.m | 41 +++ Sources/Sentry/SentryReplayRecording.m | 75 +++++ Sources/Sentry/SentryReplayType.m | 14 + Sources/Sentry/SentrySerialization.m | 17 +- Sources/Sentry/SentrySessionReplay.m | 276 ++++++++++++++++++ .../Sentry/SentrySessionReplayIntegration.m | 129 ++++++++ .../include/HybridPublic/SentryEnvelope.h | 2 + .../HybridPublic/SentryEnvelopeItemType.h | 1 + .../Sentry/include/SentryBaseIntegration.h | 1 + Sources/Sentry/include/SentryClient+Private.h | 7 +- .../Sentry/include/SentryCoreGraphicsHelper.h | 13 + Sources/Sentry/include/SentryDataCategory.h | 3 +- .../Sentry/include/SentryDataCategoryMapper.h | 1 + Sources/Sentry/include/SentryDateUtil.h | 2 + .../Sentry/include/SentryEnvelope+Private.h | 6 + Sources/Sentry/include/SentryHub+Private.h | 6 + .../Sentry/include/SentryMsgPackSerializer.h | 33 +++ Sources/Sentry/include/SentryPrivate.h | 8 +- Sources/Sentry/include/SentryReplayEvent.h | 40 +++ .../Sentry/include/SentryReplayRecording.h | 45 +++ Sources/Sentry/include/SentryReplayType.h | 16 + Sources/Sentry/include/SentrySerialization.h | 6 +- Sources/Sentry/include/SentrySessionReplay.h | 65 +++++ .../include/SentrySessionReplayIntegration.h | 12 + .../SessionReplay/SentryOnDemandReplay.swift | 188 ++++++++++++ .../SessionReplay/SentryPixelBuffer.swift | 49 ++++ .../SessionReplay/SentryReplayOptions.swift | 101 +++++++ .../SessionReplay/SentryVideoInfo.swift | 28 ++ .../Swift/Protocol/SentryRedactOptions.swift | 7 + Sources/Swift/SentryExperimentalOptions.swift | 18 ++ .../Swift/Tools/SentryViewPhotographer.swift | 115 ++++++++ .../Helper/SentryDateUtilTests.swift | 8 + .../Helper/SentrySerializationTests.swift | 17 ++ .../SentryReplayEventTests.swift | 30 ++ .../SentryReplayRecordingTests.swift | 41 +++ .../SentrySessionReplayIntegrationTests.swift | 73 +++++ .../SentrySessionReplayTests.swift | 210 +++++++++++++ .../SentryDataCategoryMapperTests.swift | 6 +- .../Protocol/SentryEnvelopeTests.swift | 6 + Tests/SentryTests/SentryClientTests.swift | 84 ++++++ Tests/SentryTests/SentryHubTests.swift | 28 ++ .../SentryMsgPackSerializerTests.m | 103 +++++++ Tests/SentryTests/SentryOptionsTest.m | 24 ++ .../SentryTests/SentryTests-Bridging-Header.h | 17 ++ 61 files changed, 2407 insertions(+), 38 deletions(-) create mode 100644 Sources/Configuration/SentryNoUI.xcconfig create mode 100644 Sources/Sentry/SentryCoreGraphicsHelper.m create mode 100644 Sources/Sentry/SentryMsgPackSerializer.m create mode 100644 Sources/Sentry/SentryReplayEvent.m create mode 100644 Sources/Sentry/SentryReplayRecording.m create mode 100644 Sources/Sentry/SentryReplayType.m create mode 100644 Sources/Sentry/SentrySessionReplay.m create mode 100644 Sources/Sentry/SentrySessionReplayIntegration.m create mode 100644 Sources/Sentry/include/SentryCoreGraphicsHelper.h create mode 100644 Sources/Sentry/include/SentryMsgPackSerializer.h create mode 100644 Sources/Sentry/include/SentryReplayEvent.h create mode 100644 Sources/Sentry/include/SentryReplayRecording.h create mode 100644 Sources/Sentry/include/SentryReplayType.h create mode 100644 Sources/Sentry/include/SentrySessionReplay.h create mode 100644 Sources/Sentry/include/SentrySessionReplayIntegration.h create mode 100644 Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift create mode 100644 Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift create mode 100644 Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift create mode 100644 Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift create mode 100644 Sources/Swift/Protocol/SentryRedactOptions.swift create mode 100644 Sources/Swift/SentryExperimentalOptions.swift create mode 100644 Sources/Swift/Tools/SentryViewPhotographer.swift create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentryReplayRecordingTests.swift create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift create mode 100644 Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift create mode 100644 Tests/SentryTests/SentryMsgPackSerializerTests.m diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c065daa139..f0ddfc61d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Add Session Replay, which is **still experimental**. (#3625) + ## 8.24.0 ### Features diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 5bf093e78b7..7125eb93257 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -17,13 +17,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let dsn = DSNStorage.shared.getDSN() ?? AppDelegate.defaultDSN DSNStorage.shared.saveDSN(dsn: dsn) - SentrySDK.start { options in + SentrySDK.start(configureOptions: { options in options.dsn = dsn options.beforeSend = { event in return event } options.debug = true + if #available(iOS 16.0, *) { + options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1, redactAllText: false, redactAllImages: true) + } + if #available(iOS 15.0, *) { options.enableMetricKit = true } @@ -60,7 +64,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.sessionTrackingIntervalMillis = 5_000 options.attachScreenshot = true options.attachViewHierarchy = true - + #if targetEnvironment(simulator) options.enableSpotlight = true options.environment = "test-app" @@ -130,7 +134,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } return scope } - } + }) SentrySDK.metrics.increment(key: "app.start", value: 1.0, tags: ["view": "app-delegate"]) diff --git a/Sentry.podspec b/Sentry.podspec index cb279f41b67..2f4e5eb6851 100644 --- a/Sentry.podspec +++ b/Sentry.podspec @@ -32,7 +32,8 @@ Pod::Spec.new do |s| s.subspec 'Core' do |sp| sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", - "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}", + "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}" + sp.preserve_path = "Sources/Sentry/include/module.modulemap" sp.public_header_files = "Sources/Sentry/Public/*.h" @@ -43,7 +44,8 @@ Pod::Spec.new do |s| s.subspec 'HybridSDK' do |sp| sp.source_files = "Sources/Sentry/**/*.{h,hpp,m,mm,c,cpp}", "Sources/SentryCrash/**/*.{h,hpp,m,mm,c,cpp}", "Sources/Swift/**/*.{swift,h,hpp,m,mm,c,cpp}" - + + sp.preserve_path = "Sources/Sentry/include/module.modulemap" sp.public_header_files = "Sources/Sentry/Public/*.h", "Sources/Sentry/include/HybridPublic/*.h" diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index b871048fd73..278402352b3 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -763,6 +763,12 @@ A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B2D2901765900990B25 /* SentryRequest.m */; }; A8F17B342902870300990B25 /* SentryHttpStatusCodeRange.m in Sources */ = {isa = PBXBuildFile; fileRef = A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */; }; D8019910286B089000C277F0 /* SentryCrashReportSinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */; }; + D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */; }; + D80299502BA83A88000F0081 /* SentryPixelBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */; }; + D80694C42B7CC9AE00B820E6 /* SentryReplayEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */; }; + D80694C72B7CD22B00B820E6 /* SentryReplayRecordingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */; }; + D80694CD2B7E0A3E00B820E6 /* SentryReplayType.h in Headers */ = {isa = PBXBuildFile; fileRef = D80694CB2B7E0A3E00B820E6 /* SentryReplayType.h */; }; + D80694CE2B7E0A3E00B820E6 /* SentryReplayType.m in Sources */ = {isa = PBXBuildFile; fileRef = D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */; }; D808FB88281AB33C009A2A33 /* SentryUIEventTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB86281AB31D009A2A33 /* SentryUIEventTrackerTests.swift */; }; D808FB8B281BCE96009A2A33 /* TestSentrySwizzleWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB89281BCE46009A2A33 /* TestSentrySwizzleWrapper.swift */; }; D808FB92281BF6EC009A2A33 /* SentryUIEventTrackingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D808FB90281BF6E9009A2A33 /* SentryUIEventTrackingIntegrationTests.swift */; }; @@ -779,9 +785,16 @@ D8199DC229376FC10074249E /* Sentry.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63AA759B1EB8AEF500D153DE /* Sentry.framework */; }; D81A346C291AECC7005A27A9 /* PrivateSentrySDKOnly.h in Headers */ = {isa = PBXBuildFile; fileRef = D81A346B291AECC7005A27A9 /* PrivateSentrySDKOnly.h */; settings = {ATTRIBUTES = (Private, ); }; }; D81FDF12280EA1060045E0E4 /* SentryScreenShotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D81FDF10280EA0080045E0E4 /* SentryScreenShotTests.swift */; }; + D820CDB32BB1886100BA339D /* SentrySessionReplay.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CDB22BB1886100BA339D /* SentrySessionReplay.m */; }; + D820CDB42BB1886100BA339D /* SentrySessionReplay.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB12BB1886100BA339D /* SentrySessionReplay.h */; }; + D820CDB72BB1895F00BA339D /* SentrySessionReplayIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */; }; + D820CDB82BB1895F00BA339D /* SentrySessionReplayIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */; }; + D820CE132BB2F13C00BA339D /* SentryCoreGraphicsHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */; }; D8292D7D2A39A027009872F7 /* UrlSanitizedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */; }; D8370B6A273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */; }; D8370B6C273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */; }; + D83D079B2B7F9D1C00CC9674 /* SentryMsgPackSerializer.h in Headers */ = {isa = PBXBuildFile; fileRef = D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */; }; + D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */ = {isa = PBXBuildFile; fileRef = D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */; }; D84541182A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */; }; D84793262788737D00BE8E99 /* SentryByteCountFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = D84793242788737D00BE8E99 /* SentryByteCountFormatter.m */; }; D8479328278873A100BE8E99 /* SentryByteCountFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = D8479327278873A100BE8E99 /* SentryByteCountFormatter.h */; }; @@ -806,15 +819,22 @@ D85D3BEA278DF63D001B2889 /* SentryByteCountFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D85D3BE9278DF63D001B2889 /* SentryByteCountFormatterTests.swift */; }; D8603DD6284F8497000E1227 /* SentryBaggage.m in Sources */ = {isa = PBXBuildFile; fileRef = D8603DD4284F8497000E1227 /* SentryBaggage.m */; }; D8603DD8284F894C000E1227 /* SentryBaggage.h in Headers */ = {isa = PBXBuildFile; fileRef = D8603DD7284F894C000E1227 /* SentryBaggage.h */; }; + D86130122BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86130112BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift */; }; + D861301C2BB5A267004C0F5E /* SentrySessionReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D861301B2BB5A267004C0F5E /* SentrySessionReplayTests.swift */; }; D865892F29D6ECA7000BE151 /* SentryCrashBinaryImageCache.h in Headers */ = {isa = PBXBuildFile; fileRef = D865892D29D6ECA7000BE151 /* SentryCrashBinaryImageCache.h */; }; D865893029D6ECA7000BE151 /* SentryCrashBinaryImageCache.c in Sources */ = {isa = PBXBuildFile; fileRef = D865892E29D6ECA7000BE151 /* SentryCrashBinaryImageCache.c */; }; D867063D27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063A27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h */; }; D867063E27C3BC2400048851 /* SentryCoreDataSwizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063B27C3BC2400048851 /* SentryCoreDataSwizzling.h */; }; D867063F27C3BC2400048851 /* SentryCoreDataTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = D867063C27C3BC2400048851 /* SentryCoreDataTracker.h */; }; D86B6835294348A400B8B1FC /* SentryAttachment+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */; }; + D86B7B5C2B7A529C0017E8D9 /* SentryReplayEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = D86B7B5A2B7A529C0017E8D9 /* SentryReplayEvent.h */; }; + D86B7B5D2B7A529C0017E8D9 /* SentryReplayEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = D86B7B5B2B7A529C0017E8D9 /* SentryReplayEvent.m */; }; D86F419827C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */; }; D8751FA5274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */; }; D875ED0B276CC84700422FAC /* SentryNSDataTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */; }; + D878C6A82BC7F01C0039D6A3 /* SentryCoreGraphicsHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */; }; + D87C89032BC43C9C0086C7DF /* SentryRedactOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */; }; + D87C892B2BC67BC20086C7DF /* SentryExperimentalOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */; }; D880E3A728573E87008A90DB /* SentryBaggageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D880E3A628573E87008A90DB /* SentryBaggageTests.swift */; }; D884A20527C80F6300074664 /* SentryCoreDataTrackerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D884A20327C80F2700074664 /* SentryCoreDataTrackerTest.swift */; }; D885266427739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D885266327739D01001269FC /* SentryFileIOTrackingIntegrationTests.swift */; }; @@ -822,6 +842,8 @@ D88817D826D7149100BF2251 /* SentryTraceContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D88817D626D7149100BF2251 /* SentryTraceContext.m */; }; D88817DA26D72AB800BF2251 /* SentryTraceContext.h in Headers */ = {isa = PBXBuildFile; fileRef = D88817D926D72AB800BF2251 /* SentryTraceContext.h */; settings = {ATTRIBUTES = (Private, ); }; }; D88817DD26D72BA500BF2251 /* SentryTraceStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */; }; + D88D6C1D2B7B5A8800C8C633 /* SentryReplayRecording.h in Headers */ = {isa = PBXBuildFile; fileRef = D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */; }; + D88D6C1E2B7B5A8800C8C633 /* SentryReplayRecording.m in Sources */ = {isa = PBXBuildFile; fileRef = D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */; }; D8918B222849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */; }; D8AB40DB2806EC1900E5E9F7 /* SentryScreenshotIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */; }; D8ACE3C72762187200F5A213 /* SentryNSDataSwizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */; }; @@ -846,7 +868,10 @@ D8C66A372A77B1F70015696A /* SentryPropagationContext.m in Sources */ = {isa = PBXBuildFile; fileRef = D8C66A352A77B1F70015696A /* SentryPropagationContext.m */; }; D8C67E9B28000E24007E326E /* SentryUIApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = D8C67E9928000E23007E326E /* SentryUIApplication.h */; }; D8C67E9C28000E24007E326E /* SentryScreenshot.h in Headers */ = {isa = PBXBuildFile; fileRef = D8C67E9A28000E23007E326E /* SentryScreenshot.h */; }; + D8CAC02E2BA0663E00E38F34 /* SentryReplayOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */; }; + D8CAC02F2BA0663E00E38F34 /* SentryVideoInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */; }; D8CAC0412BA0984500E38F34 /* SentryIntegrationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */; }; + D8CAC0732BA4473000E38F34 /* SentryViewPhotographer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */; }; D8CB74152947246600A5F964 /* SentryEnvelopeAttachmentHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */; }; D8CB7417294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */; }; D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -860,6 +885,7 @@ D8F6A2472885512100320515 /* SentryPredicateDescriptor.m in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */; }; D8F6A24B2885515C00320515 /* SentryPredicateDescriptor.h in Headers */ = {isa = PBXBuildFile; fileRef = D8F6A24A2885515B00320515 /* SentryPredicateDescriptor.h */; }; D8F6A24E288553A800320515 /* SentryPredicateDescriptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F6A24C2885534E00320515 /* SentryPredicateDescriptorTests.swift */; }; + D8F8F5572B835BC600AC5465 /* SentryMsgPackSerializerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = D8F8F5562B835BC600AC5465 /* SentryMsgPackSerializerTests.m */; }; D8FFE50C2703DBB400607131 /* SwizzlingCallTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FFE50B2703DAAE00607131 /* SwizzlingCallTests.swift */; }; /* End PBXBuildFile section */ @@ -1752,6 +1778,12 @@ A8F17B332902870300990B25 /* SentryHttpStatusCodeRange.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryHttpStatusCodeRange.m; sourceTree = ""; }; D800942628F82F3A005D3943 /* SwiftDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftDescriptor.swift; sourceTree = ""; }; D801990F286B089000C277F0 /* SentryCrashReportSinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCrashReportSinkTests.swift; sourceTree = ""; }; + D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryOnDemandReplay.swift; sourceTree = ""; }; + D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryPixelBuffer.swift; sourceTree = ""; }; + D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayEventTests.swift; sourceTree = ""; }; + D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryReplayRecordingTests.swift; sourceTree = ""; }; + D80694CB2B7E0A3E00B820E6 /* SentryReplayType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayType.h; path = include/SentryReplayType.h; sourceTree = ""; }; + D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayType.m; sourceTree = ""; }; D808FB86281AB31D009A2A33 /* SentryUIEventTrackerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIEventTrackerTests.swift; sourceTree = ""; }; D808FB89281BCE46009A2A33 /* TestSentrySwizzleWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestSentrySwizzleWrapper.swift; sourceTree = ""; }; D808FB90281BF6E9009A2A33 /* SentryUIEventTrackingIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryUIEventTrackingIntegrationTests.swift; sourceTree = ""; }; @@ -1768,10 +1800,18 @@ D8199DD029377C130074249E /* SentrySwiftUI.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SentrySwiftUI.podspec; sourceTree = ""; }; D81A346B291AECC7005A27A9 /* PrivateSentrySDKOnly.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PrivateSentrySDKOnly.h; path = include/HybridPublic/PrivateSentrySDKOnly.h; sourceTree = ""; }; D81FDF10280EA0080045E0E4 /* SentryScreenShotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenShotTests.swift; sourceTree = ""; }; + D820CDB12BB1886100BA339D /* SentrySessionReplay.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplay.h; path = include/SentrySessionReplay.h; sourceTree = ""; }; + D820CDB22BB1886100BA339D /* SentrySessionReplay.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplay.m; sourceTree = ""; }; + D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySessionReplayIntegration.h; path = include/SentrySessionReplayIntegration.h; sourceTree = ""; }; + D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySessionReplayIntegration.m; sourceTree = ""; }; + D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryCoreGraphicsHelper.h; path = include/SentryCoreGraphicsHelper.h; sourceTree = ""; }; + D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryCoreGraphicsHelper.m; sourceTree = ""; }; D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPHeaderSanitizer.swift; sourceTree = ""; }; D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitizedTests.swift; sourceTree = ""; }; D8370B68273DF1E900F66E2D /* SentryNSURLSessionTaskSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryNSURLSessionTaskSearch.m; sourceTree = ""; }; D8370B6B273DF20F00F66E2D /* SentryNSURLSessionTaskSearch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryNSURLSessionTaskSearch.h; path = include/SentryNSURLSessionTaskSearch.h; sourceTree = ""; }; + D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryMsgPackSerializer.h; path = include/SentryMsgPackSerializer.h; sourceTree = ""; }; + D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryMsgPackSerializer.m; sourceTree = ""; }; D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBinaryImageCacheTests.swift; sourceTree = ""; }; D84541192A2DC55100E2B11C /* SentryBinaryImageCache+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SentryBinaryImageCache+Private.h"; sourceTree = ""; }; D84793242788737D00BE8E99 /* SentryByteCountFormatter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryByteCountFormatter.m; sourceTree = ""; }; @@ -1780,11 +1820,13 @@ D84DAD4F2B17428D003CF120 /* SentryTestUtilsDynamic.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryTestUtilsDynamic.h; sourceTree = ""; }; D84F833B2A1CC401005828E0 /* SentrySwiftAsyncIntegration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentrySwiftAsyncIntegration.h; path = include/SentrySwiftAsyncIntegration.h; sourceTree = ""; }; D84F833C2A1CC401005828E0 /* SentrySwiftAsyncIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentrySwiftAsyncIntegration.m; sourceTree = ""; }; + D8511F722BAC8F750015E6FD /* Sentry.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Sentry.modulemap; sourceTree = ""; }; D85596F1280580F10041FF8B /* SentryScreenshotIntegration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryScreenshotIntegration.m; sourceTree = ""; }; D855AD61286ED6A4002573E1 /* SentryCrashTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryCrashTests.m; sourceTree = ""; }; D855B3E727D652AF00BCED76 /* SentryCoreDataTrackingIntegrationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackingIntegrationTest.swift; sourceTree = ""; }; D855B3E927D652C700BCED76 /* TestCoreDataStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCoreDataStack.swift; sourceTree = ""; }; D856272B2A374A8600FB8062 /* UrlSanitized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSanitized.swift; sourceTree = ""; }; + D85723EF2BBC3BDC004AC5E1 /* SentryNoUI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SentryNoUI.xcconfig; sourceTree = ""; }; D85790282976A69F00C6AC1F /* TestDebugImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDebugImageProvider.swift; sourceTree = ""; }; D85852B427ECEEDA00C6D8AE /* SentryScreenshot.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryScreenshot.m; sourceTree = ""; }; D85852B827EDDC5900C6D8AE /* SentryUIApplication.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryUIApplication.m; sourceTree = ""; }; @@ -1796,6 +1838,8 @@ D85D3BE9278DF63D001B2889 /* SentryByteCountFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryByteCountFormatterTests.swift; sourceTree = ""; }; D8603DD4284F8497000E1227 /* SentryBaggage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryBaggage.m; sourceTree = ""; }; D8603DD7284F894C000E1227 /* SentryBaggage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryBaggage.h; path = include/SentryBaggage.h; sourceTree = ""; }; + D86130112BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayIntegrationTests.swift; sourceTree = ""; }; + D861301B2BB5A267004C0F5E /* SentrySessionReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySessionReplayTests.swift; sourceTree = ""; }; D865892D29D6ECA7000BE151 /* SentryCrashBinaryImageCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SentryCrashBinaryImageCache.h; sourceTree = ""; }; D865892E29D6ECA7000BE151 /* SentryCrashBinaryImageCache.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = SentryCrashBinaryImageCache.c; sourceTree = ""; }; D867063A27C3BC2400048851 /* SentryCoreDataTrackingIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryCoreDataTrackingIntegration.h; path = include/SentryCoreDataTrackingIntegration.h; sourceTree = ""; }; @@ -1803,10 +1847,14 @@ D867063C27C3BC2400048851 /* SentryCoreDataTracker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryCoreDataTracker.h; path = include/SentryCoreDataTracker.h; sourceTree = ""; }; D86B6820293F39E000B8B1FC /* TestSentryViewHierarchy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TestSentryViewHierarchy.h; sourceTree = ""; }; D86B6834294348A400B8B1FC /* SentryAttachment+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryAttachment+Private.h"; path = "include/SentryAttachment+Private.h"; sourceTree = ""; }; + D86B7B5A2B7A529C0017E8D9 /* SentryReplayEvent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayEvent.h; path = include/SentryReplayEvent.h; sourceTree = ""; }; + D86B7B5B2B7A529C0017E8D9 /* SentryReplayEvent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayEvent.m; sourceTree = ""; }; D86F419727C8FEFA00490520 /* SentryCoreDataTrackerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryCoreDataTrackerExtension.swift; sourceTree = ""; }; D8751FA4274743710032F4DE /* SentryNSURLSessionTaskSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSURLSessionTaskSearchTests.swift; sourceTree = ""; }; D8757D142A209F7300BFEFCC /* SentrySampleDecision+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentrySampleDecision+Private.h"; path = "include/SentrySampleDecision+Private.h"; sourceTree = ""; }; D875ED0A276CC84700422FAC /* SentryNSDataTrackerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryNSDataTrackerTests.swift; sourceTree = ""; }; + D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryRedactOptions.swift; sourceTree = ""; }; + D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryExperimentalOptions.swift; sourceTree = ""; }; D878C6C02BC8048A0039D6A3 /* SentryPrivate.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = SentryPrivate.podspec; sourceTree = ""; }; D880E3A628573E87008A90DB /* SentryBaggageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryBaggageTests.swift; sourceTree = ""; }; D880E3B02860A5A0008A90DB /* SentryEvent+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryEvent+Private.h"; path = "include/SentryEvent+Private.h"; sourceTree = ""; }; @@ -1816,6 +1864,8 @@ D88817D926D72AB800BF2251 /* SentryTraceContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryTraceContext.h; path = include/SentryTraceContext.h; sourceTree = ""; }; D88817DB26D72B7B00BF2251 /* SentryTraceStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceStateTests.swift; sourceTree = ""; }; D88D25E92B8E0BAC0073C3D5 /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; + D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryReplayRecording.h; path = include/SentryReplayRecording.h; sourceTree = ""; }; + D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryReplayRecording.m; sourceTree = ""; }; D8918B212849FA6D00701F9A /* SentrySDKIntegrationTestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySDKIntegrationTestsBase.swift; sourceTree = ""; }; D8AB40DA2806EC1900E5E9F7 /* SentryScreenshotIntegration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshotIntegration.h; path = include/SentryScreenshotIntegration.h; sourceTree = ""; }; D8ACE3C42762187200F5A213 /* SentryNSDataSwizzling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SentryNSDataSwizzling.m; sourceTree = ""; }; @@ -1843,7 +1893,10 @@ D8C66A352A77B1F70015696A /* SentryPropagationContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryPropagationContext.m; sourceTree = ""; }; D8C67E9928000E23007E326E /* SentryUIApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryUIApplication.h; path = include/SentryUIApplication.h; sourceTree = ""; }; D8C67E9A28000E23007E326E /* SentryScreenshot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryScreenshot.h; path = include/SentryScreenshot.h; sourceTree = ""; }; + D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryReplayOptions.swift; sourceTree = ""; }; + D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SentryVideoInfo.swift; sourceTree = ""; }; D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryIntegrationProtocol.swift; sourceTree = ""; }; + D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryViewPhotographer.swift; sourceTree = ""; }; D8CB74142947246600A5F964 /* SentryEnvelopeAttachmentHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeAttachmentHeader.h; path = include/SentryEnvelopeAttachmentHeader.h; sourceTree = ""; }; D8CB7416294724CC00A5F964 /* SentryEnvelopeAttachmentHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryEnvelopeAttachmentHeader.m; sourceTree = ""; }; D8CB74182947285A00A5F964 /* SentryEnvelopeItemHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryEnvelopeItemHeader.h; path = Public/SentryEnvelopeItemHeader.h; sourceTree = ""; }; @@ -1860,6 +1913,7 @@ D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryPredicateDescriptor.m; sourceTree = ""; }; D8F6A24A2885515B00320515 /* SentryPredicateDescriptor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryPredicateDescriptor.h; path = include/SentryPredicateDescriptor.h; sourceTree = ""; }; D8F6A24C2885534E00320515 /* SentryPredicateDescriptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryPredicateDescriptorTests.swift; sourceTree = ""; }; + D8F8F5562B835BC600AC5465 /* SentryMsgPackSerializerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryMsgPackSerializerTests.m; sourceTree = ""; }; D8FFE50B2703DAAE00607131 /* SwizzlingCallTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwizzlingCallTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2248,6 +2302,7 @@ 7BE0DC35272AE7BF004FA8B7 /* SentryCrash */, D85596EF280580BE0041FF8B /* Screenshot */, 0A9BF4E028A114690068D266 /* ViewHierarchy */, + D80CD8D52B752FD9002F710B /* SessionReplay */, 7D7F0A5E23DF3D2C00A4629C /* SentryGlobalEventProcessor.h */, 7DAC588E23D8B2E0001CF26B /* SentryGlobalEventProcessor.m */, 7BA235622600B61200E12865 /* SentryInternalNotificationNames.h */, @@ -2404,6 +2459,7 @@ 84B7FA4729B2995A00AD93B1 /* DeploymentTargets.xcconfig */, 63AA75C51EB8B00100D153DE /* Sentry.xcconfig */, D8199DCF29376FF40074249E /* SentrySwiftUI.xcconfig */, + D85723EF2BBC3BDC004AC5E1 /* SentryNoUI.xcconfig */, ); path = Configuration; sourceTree = ""; @@ -2824,6 +2880,7 @@ 7BE0DC40272AEA0A004FA8B7 /* Performance */, 7BE0DC3F272AE9F0004FA8B7 /* Session */, 7BE0DC3E272AE9DC004FA8B7 /* SentryCrash */, + D80694C12B7CC85800B820E6 /* SessionReplay */, 7B59398324AB481B0003AAD2 /* NotificationCenterTestCase.swift */, 0A2D8D8628992260008720F6 /* SentryBaseIntegrationTests.swift */, ); @@ -3355,6 +3412,8 @@ D8F6A2452885512100320515 /* SentryPredicateDescriptor.m */, 0A2D8DA6289BC905008720F6 /* SentryViewHierarchy.h */, 0A2D8DA7289BC905008720F6 /* SentryViewHierarchy.m */, + D83D07992B7F9D1C00CC9674 /* SentryMsgPackSerializer.h */, + D83D079A2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m */, ); name = Tools; sourceTree = ""; @@ -3412,6 +3471,7 @@ D800942328F82E8D005D3943 /* Swift */ = { isa = PBXGroup; children = ( + D8CAC02D2BA0663E00E38F34 /* Integrations */, 62262B892BA1C4B0004DA3DD /* Metrics */, 621D9F2D2B9B030E003D94DE /* Helper */, D8F016B42B962533007B9AFB /* Extensions */, @@ -3419,11 +3479,23 @@ D8F016B12B9622B7007B9AFB /* Protocol */, D856272A2A374A6800FB8062 /* Tools */, D800942628F82F3A005D3943 /* SwiftDescriptor.swift */, + D87C892A2BC67BC20086C7DF /* SentryExperimentalOptions.swift */, D8B665BB2B95F5A100BD0E7B /* module.modulemap */, ); path = Swift; sourceTree = ""; }; + D80694C12B7CC85800B820E6 /* SessionReplay */ = { + isa = PBXGroup; + children = ( + D80694C22B7CC86E00B820E6 /* SentryReplayEventTests.swift */, + D80694C52B7CCFA100B820E6 /* SentryReplayRecordingTests.swift */, + D86130112BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift */, + D861301B2BB5A267004C0F5E /* SentrySessionReplayTests.swift */, + ); + path = SessionReplay; + sourceTree = ""; + }; D808FB85281AB2EF009A2A33 /* UIEvents */ = { isa = PBXGroup; children = ( @@ -3433,6 +3505,25 @@ path = UIEvents; sourceTree = ""; }; + D80CD8D52B752FD9002F710B /* SessionReplay */ = { + isa = PBXGroup; + children = ( + D86B7B5A2B7A529C0017E8D9 /* SentryReplayEvent.h */, + D86B7B5B2B7A529C0017E8D9 /* SentryReplayEvent.m */, + D80694CB2B7E0A3E00B820E6 /* SentryReplayType.h */, + D80694CC2B7E0A3E00B820E6 /* SentryReplayType.m */, + D88D6C1B2B7B5A8800C8C633 /* SentryReplayRecording.h */, + D88D6C1C2B7B5A8800C8C633 /* SentryReplayRecording.m */, + D820CDB12BB1886100BA339D /* SentrySessionReplay.h */, + D820CDB22BB1886100BA339D /* SentrySessionReplay.m */, + D820CDB52BB1895F00BA339D /* SentrySessionReplayIntegration.h */, + D820CDB62BB1895F00BA339D /* SentrySessionReplayIntegration.m */, + D820CE112BB2F13C00BA339D /* SentryCoreGraphicsHelper.h */, + D820CE122BB2F13C00BA339D /* SentryCoreGraphicsHelper.m */, + ); + name = SessionReplay; + sourceTree = ""; + }; D8199DB329376ECC0074249E /* SentrySwiftUI */ = { isa = PBXGroup; children = ( @@ -3465,6 +3556,7 @@ D84541172A2DC2CD00E2B11C /* SentryBinaryImageCacheTests.swift */, D84541192A2DC55100E2B11C /* SentryBinaryImageCache+Private.h */, D8292D7C2A39A027009872F7 /* UrlSanitizedTests.swift */, + D8F8F5562B835BC600AC5465 /* SentryMsgPackSerializerTests.m */, D8B425112B9A0FD6000BFDF3 /* StringExtensionTests.swift */, ); name = Tools; @@ -3493,6 +3585,7 @@ children = ( D856272B2A374A8600FB8062 /* UrlSanitized.swift */, D8292D7A2A38AF04009872F7 /* HTTPHeaderSanitizer.swift */, + D8CAC0722BA4473000E38F34 /* SentryViewPhotographer.swift */, ); path = Tools; sourceTree = ""; @@ -3610,15 +3703,36 @@ isa = PBXGroup; children = ( D8B0542D2A7D2C720056BAF6 /* PrivacyInfo.xcprivacy */, + D8511F722BAC8F750015E6FD /* Sentry.modulemap */, ); path = Resources; sourceTree = ""; }; + D8CAC02C2BA0663E00E38F34 /* SessionReplay */ = { + isa = PBXGroup; + children = ( + D8CAC02A2BA0663E00E38F34 /* SentryReplayOptions.swift */, + D8CAC02B2BA0663E00E38F34 /* SentryVideoInfo.swift */, + D802994D2BA836EF000F0081 /* SentryOnDemandReplay.swift */, + D802994F2BA83A88000F0081 /* SentryPixelBuffer.swift */, + ); + path = SessionReplay; + sourceTree = ""; + }; + D8CAC02D2BA0663E00E38F34 /* Integrations */ = { + isa = PBXGroup; + children = ( + D8CAC02C2BA0663E00E38F34 /* SessionReplay */, + ); + path = Integrations; + sourceTree = ""; + }; D8F016B12B9622B7007B9AFB /* Protocol */ = { isa = PBXGroup; children = ( D8F016B22B9622D6007B9AFB /* SentryId.swift */, D8CAC0402BA0984500E38F34 /* SentryIntegrationProtocol.swift */, + D87C89022BC43C9C0086C7DF /* SentryRedactOptions.swift */, ); path = Protocol; sourceTree = ""; @@ -3687,6 +3801,7 @@ 7B0A54222521C21E00A71716 /* SentryFrameRemover.h in Headers */, 63FE70CD20DA4C1000CDBAE8 /* SentryCrashDoctor.h in Headers */, D8C67E9B28000E24007E326E /* SentryUIApplication.h in Headers */, + D820CE132BB2F13C00BA339D /* SentryCoreGraphicsHelper.h in Headers */, 7B6438AA26A70F24000D0F65 /* UIViewController+Sentry.h in Headers */, 639FCFAC1EBC811400778193 /* SentryUser.h in Headers */, D8CB74192947285A00A5F964 /* SentryEnvelopeItemHeader.h in Headers */, @@ -3741,9 +3856,11 @@ 8EAE980B261E9F530073B6B3 /* SentryPerformanceTracker.h in Headers */, 63FE718520DA4C1100CDBAE8 /* SentryCrashC.h in Headers */, 8EA1ED0D2669028C00E62B98 /* SentryUIViewControllerSwizzling.h in Headers */, + D820CDB82BB1895F00BA339D /* SentrySessionReplayIntegration.h in Headers */, 7B98D7E425FB7A7200C5A389 /* SentryAppState.h in Headers */, 7BDEAA022632A4580001EA25 /* SentryOptions+Private.h in Headers */, A8AFFCCD29069C3E00967CD7 /* SentryHttpStatusCodeRange.h in Headers */, + D83D079B2B7F9D1C00CC9674 /* SentryMsgPackSerializer.h in Headers */, D84F833D2A1CC401005828E0 /* SentrySwiftAsyncIntegration.h in Headers */, 15E0A8EA240F2C9000F044E3 /* SentrySerialization.h in Headers */, 63FE70EF20DA4C1000CDBAE8 /* SentryCrashMonitor_AppState.h in Headers */, @@ -3778,7 +3895,9 @@ 7B8713AE26415ADF006D6004 /* SentryAppStartTrackingIntegration.h in Headers */, 7B7D873224864BB900D2ECFF /* SentryCrashMachineContextWrapper.h in Headers */, 861265F92404EC1500C4AFDE /* NSArray+SentrySanitize.h in Headers */, + D820CDB42BB1886100BA339D /* SentrySessionReplay.h in Headers */, 63FE712320DA4C1000CDBAE8 /* SentryCrashID.h in Headers */, + D88D6C1D2B7B5A8800C8C633 /* SentryReplayRecording.h in Headers */, 7DC27EC523997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.h in Headers */, 63FE707F20DA4C1000CDBAE8 /* SentryCrashVarArgs.h in Headers */, 03F84D2627DD414C008FE43F /* SentryThreadMetadataCache.hpp in Headers */, @@ -3889,10 +4008,12 @@ D8AB40DB2806EC1900E5E9F7 /* SentryScreenshotIntegration.h in Headers */, 7B3B83722833832B0001FDEB /* SentrySpanOperations.h in Headers */, 7BF9EF722722A84800B5BBEF /* SentryClassRegistrator.h in Headers */, + D86B7B5C2B7A529C0017E8D9 /* SentryReplayEvent.h in Headers */, 63FE715520DA4C1100CDBAE8 /* SentryCrashStackCursor_MachineContext.h in Headers */, 62E081A929ED4260000F69FC /* SentryBreadcrumbDelegate.h in Headers */, 15360CF02433A16D00112302 /* SentryInstallation.h in Headers */, 63FE714720DA4C1100CDBAE8 /* SentryCrashMachineContext.h in Headers */, + D80694CD2B7E0A3E00B820E6 /* SentryReplayType.h in Headers */, 7BA61CAB247BA98100C130A8 /* SentryDebugImageProvider.h in Headers */, 7BC63F0828081242009D9E37 /* SentrySwizzleWrapper.h in Headers */, 638DC9A01EBC6B6400A66E41 /* SentryRequestOperation.h in Headers */, @@ -4203,6 +4324,7 @@ 0A2D8D9628997845008720F6 /* NSLocale+Sentry.m in Sources */, 7B0DC730288698F70039995F /* NSMutableDictionary+Sentry.m in Sources */, 7BD4BD4527EB29F50071F4FF /* SentryClientReport.m in Sources */, + D87C89032BC43C9C0086C7DF /* SentryRedactOptions.swift in Sources */, 631E6D341EBC679C00712345 /* SentryQueueableRequestManager.m in Sources */, 7B8713B426415BAA006D6004 /* SentryAppStartTracker.m in Sources */, 7BDB03BB2513652900BAE198 /* SentryDispatchQueueWrapper.m in Sources */, @@ -4232,6 +4354,7 @@ 15E0A8ED240F2CB000F044E3 /* SentrySerialization.m in Sources */, 7BC85235245880AE005A70F0 /* SentryDataCategoryMapper.m in Sources */, 7B7A30C824B48389005A4C6E /* SentryCrashWrapper.m in Sources */, + D8CAC0732BA4473000E38F34 /* SentryViewPhotographer.swift in Sources */, D8ACE3C92762187200F5A213 /* SentryFileIOTrackingIntegration.m in Sources */, 63FE713B20DA4C1100CDBAE8 /* SentryCrashFileUtils.c in Sources */, 63FE716920DA4C1100CDBAE8 /* SentryCrashStackCursor.c in Sources */, @@ -4275,6 +4398,7 @@ 844EDC77294144DB00C86F34 /* SentrySystemWrapper.mm in Sources */, 630435FF1EBCA9D900C4D3FA /* SentryNSURLRequest.m in Sources */, 62C1AFAB2B7E10EA0038C5F7 /* SentrySpotlightTransport.m in Sources */, + D87C892B2BC67BC20086C7DF /* SentryExperimentalOptions.swift in Sources */, 7B5CAF7727F5A68C00ED0DB6 /* SentryNSURLRequestBuilder.m in Sources */, 639FCFA11EBC804600778193 /* SentryException.m in Sources */, D80CD8D42B75144B002F710B /* SwiftDescriptor.swift in Sources */, @@ -4292,6 +4416,7 @@ D8F6A2472885512100320515 /* SentryPredicateDescriptor.m in Sources */, A839D89A24864BA8003B7AFD /* SentrySystemEventBreadcrumbs.m in Sources */, 7D082B8323C628790029866B /* SentryMeta.m in Sources */, + D8CAC02F2BA0663E00E38F34 /* SentryVideoInfo.swift in Sources */, 63FE710720DA4C1000CDBAE8 /* SentryCrashStackCursor_SelfThread.m in Sources */, 63FE711120DA4C1000CDBAE8 /* SentryCrashDebug.c in Sources */, 7B883F49253D714C00879E62 /* SentryCrashUUIDConversion.c in Sources */, @@ -4313,6 +4438,8 @@ 7B56D73324616D9500B842DA /* SentryConcurrentRateLimitsDictionary.m in Sources */, 8ECC674825C23A20000E2BF6 /* SentryTransaction.m in Sources */, 0A80E433291017C300095219 /* SentryWatchdogTerminationScopeObserver.m in Sources */, + D88D6C1E2B7B5A8800C8C633 /* SentryReplayRecording.m in Sources */, + D8CAC02E2BA0663E00E38F34 /* SentryReplayOptions.swift in Sources */, 7BECF42826145CD900D9826E /* SentryMechanismMeta.m in Sources */, 8E7C982F2693D56000E6336C /* SentryTraceHeader.m in Sources */, 63FE715F20DA4C1100CDBAE8 /* SentryCrashID.c in Sources */, @@ -4335,8 +4462,10 @@ 63FE70FD20DA4C1000CDBAE8 /* SentryCrashCachedData.c in Sources */, A8F17B2E2901765900990B25 /* SentryRequest.m in Sources */, 7BE1E33424F7E3CB009D3AD0 /* SentryMigrateSessionInit.m in Sources */, + D80299502BA83A88000F0081 /* SentryPixelBuffer.swift in Sources */, 15E0A8F22411A45A00F044E3 /* SentrySession.m in Sources */, 844EDCE62947DC3100C86F34 /* SentryNSTimerFactory.m in Sources */, + D83D079C2B7F9D1C00CC9674 /* SentryMsgPackSerializer.m in Sources */, 7B6D1261265F784000C9BE4B /* PrivateSentrySDKOnly.mm in Sources */, 63BE85711ECEC6DE00DC44F5 /* SentryDateUtils.m in Sources */, 7BD4BD4927EB2A5D0071F4FF /* SentryDiscardedEvent.m in Sources */, @@ -4346,10 +4475,12 @@ 7B127B0F27CF6F4700A71ED2 /* SentryANRTrackingIntegration.m in Sources */, 62C316832B1F2EA1000D7031 /* SentryDelayedFramesTracker.m in Sources */, D8BFE37329A3782F002E73F3 /* SentryTimeToDisplayTracker.m in Sources */, + D80694CE2B7E0A3E00B820E6 /* SentryReplayType.m in Sources */, 15360CCF2432777500112302 /* SentrySessionTracker.m in Sources */, 6334314320AD9AE40077E581 /* SentryMechanism.m in Sources */, 63FE70D320DA4C1000CDBAE8 /* SentryCrashMonitor_AppState.c in Sources */, 639FCFA51EBC809A00778193 /* SentryStacktrace.m in Sources */, + D820CDB32BB1886100BA339D /* SentrySessionReplay.m in Sources */, 63FE70DF20DA4C1000CDBAE8 /* SentryCrashMonitorType.c in Sources */, 7BF9EF7E2722B91F00B5BBEF /* SentryDefaultObjCRuntimeWrapper.m in Sources */, 7BC3936E25B1AB72004F03D3 /* SentryLevelMapper.m in Sources */, @@ -4375,6 +4506,7 @@ 63FE712D20DA4C1100CDBAE8 /* SentryCrashJSONCodecObjC.m in Sources */, 7BBD18932449BEDD00427C76 /* SentryDefaultRateLimits.m in Sources */, 7BD729982463E93500EA3610 /* SentryDateUtil.m in Sources */, + D878C6A82BC7F01C0039D6A3 /* SentryCoreGraphicsHelper.m in Sources */, 62262B882BA1C490004DA3DD /* SentryStatsdClient.m in Sources */, 639FCF9D1EBC7F9500778193 /* SentryThread.m in Sources */, 8E8C57A225EEFC07001CEEFA /* SentrySampling.m in Sources */, @@ -4425,8 +4557,10 @@ 8453421228BE855D00C22EEC /* SentrySampleDecision.m in Sources */, 7B7D872E2486482600D2ECFF /* SentryStacktraceBuilder.m in Sources */, 861265FA2404EC1500C4AFDE /* NSArray+SentrySanitize.m in Sources */, + D802994E2BA836EF000F0081 /* SentryOnDemandReplay.swift in Sources */, D8603DD6284F8497000E1227 /* SentryBaggage.m in Sources */, 63FE711520DA4C1000CDBAE8 /* SentryCrashJSONCodec.c in Sources */, + D86B7B5D2B7A529C0017E8D9 /* SentryReplayEvent.m in Sources */, 03F84D3327DD4191008FE43F /* SentryMachLogging.cpp in Sources */, D85852BA27EDDC5900C6D8AE /* SentryUIApplication.m in Sources */, 7B4E375F258231FC00059C93 /* SentryAttachment.m in Sources */, @@ -4442,6 +4576,7 @@ 0A2D8D5B289815C0008720F6 /* SentryBaseIntegration.m in Sources */, 62262B912BA1C520004DA3DD /* CounterMetric.swift in Sources */, 639FCF991EBC7B9700778193 /* SentryEvent.m in Sources */, + D820CDB72BB1895F00BA339D /* SentrySessionReplayIntegration.m in Sources */, 632F43521F581D5400A18A36 /* SentryCrashExceptionApplication.m in Sources */, 62A2F4422BA9AE12000C9FDD /* SetMetric.swift in Sources */, 620379DD2AFE1432005AC0C1 /* SentryBuildAppStartSpans.m in Sources */, @@ -4494,6 +4629,7 @@ 7B3B473E25D6CEA500D01640 /* SentryNSErrorTests.swift in Sources */, 632331F62404FFA8008D91D6 /* SentryScopeTests.m in Sources */, D808FB88281AB33C009A2A33 /* SentryUIEventTrackerTests.swift in Sources */, + D8F8F5572B835BC600AC5465 /* SentryMsgPackSerializerTests.m in Sources */, 0A283E79291A67E000EF4126 /* SentryUIDeviceWrapperTests.swift in Sources */, 63FE720D20DA66EC00CDBAE8 /* SentryCrashNSErrorUtilTests.m in Sources */, 69BEE6F72620729E006DF9DF /* UrlSessionDelegateSpy.swift in Sources */, @@ -4529,6 +4665,7 @@ D8137D54272B53070082656C /* TestSentrySpan.m in Sources */, 7BECF432261463E600D9826E /* SentryMechanismMetaTests.swift in Sources */, 7BE8E8462593313500C4DA1F /* SentryAttachment+Equality.m in Sources */, + D80694C72B7CD22B00B820E6 /* SentryReplayRecordingTests.swift in Sources */, 63FE721F20DA66EC00CDBAE8 /* SentryCrashSignalInfo_Tests.m in Sources */, 0ADC33F128D9BE940078D980 /* TestSentryUIDeviceWrapper.swift in Sources */, 63FE721420DA66EC00CDBAE8 /* SentryCrashMemory_Tests.m in Sources */, @@ -4595,6 +4732,7 @@ 62BAD74E2BA1C58D00EBAAFC /* EncodeMetricTests.swift in Sources */, 7BE0DC29272A9E1C004FA8B7 /* SentryBreadcrumbTrackerTests.swift in Sources */, 63FE722520DA66EC00CDBAE8 /* SentryCrashFileUtils_Tests.m in Sources */, + D86130122BB563FD004C0F5E /* SentrySessionReplayIntegrationTests.swift in Sources */, 7BFC16BA2524D4AF00FF6266 /* SentryMessage+Equality.m in Sources */, 7B4260342630315C00B36EDD /* SampleError.swift in Sources */, D855B3E827D652AF00BCED76 /* SentryCoreDataTrackingIntegrationTest.swift in Sources */, @@ -4623,6 +4761,7 @@ 7BD4BD4B27EB2DC20071F4FF /* SentryDiscardedEventTests.swift in Sources */, 63FE721A20DA66EC00CDBAE8 /* SentryCrashSysCtl_Tests.m in Sources */, 7B88F30424BC8E6500ADF90A /* SentrySerializationTests.swift in Sources */, + D80694C42B7CC9AE00B820E6 /* SentryReplayEventTests.swift in Sources */, 7B34721728086A9D0041F047 /* SentrySwizzleWrapperTests.swift in Sources */, 8EC4CF5025C3A0070093DEE9 /* SentrySpanContextTests.swift in Sources */, 7BE0DC2F272ABAF6004FA8B7 /* SentryAutoBreadcrumbTrackingIntegrationTests.swift in Sources */, @@ -4679,6 +4818,7 @@ 7B965728268321CD00C66E25 /* SentryCrashScopeObserverTests.swift in Sources */, 626866742BA89683006995EA /* BucketMetricsAggregatorTests.swift in Sources */, 7BD86ECB264A6DB5005439DB /* TestSysctl.swift in Sources */, + D861301C2BB5A267004C0F5E /* SentrySessionReplayTests.swift in Sources */, 7B0DC73428869BF40039995F /* NSMutableDictionarySentryTests.swift in Sources */, 7B6ADFCF26A02CAE0076C206 /* SentryCrashReportTests.swift in Sources */, D8B76B062808066D000A58C4 /* SentryScreenshotIntegrationTests.swift in Sources */, @@ -5276,7 +5416,7 @@ }; 841C60C42A69DE6B00E1C00F /* Debug_without_UIKit */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 63AA75C51EB8B00100D153DE /* Sentry.xcconfig */; + baseConfigurationReference = D85723EF2BBC3BDC004AC5E1 /* SentryNoUI.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; @@ -5769,7 +5909,7 @@ }; 8483D06B2AC7627800143615 /* Release_without_UIKit */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 63AA75C51EB8B00100D153DE /* Sentry.xcconfig */; + baseConfigurationReference = D85723EF2BBC3BDC004AC5E1 /* SentryNoUI.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; @@ -6324,7 +6464,6 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -6381,7 +6520,6 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug_without_UIKit; @@ -6435,7 +6573,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Test; @@ -6489,7 +6626,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = TestCI; @@ -6543,7 +6679,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -6597,7 +6732,6 @@ SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release_without_UIKit; diff --git a/SentryTestUtils/TestCurrentDateProvider.swift b/SentryTestUtils/TestCurrentDateProvider.swift index d80436ff57d..3a62a39f63d 100644 --- a/SentryTestUtils/TestCurrentDateProvider.swift +++ b/SentryTestUtils/TestCurrentDateProvider.swift @@ -43,6 +43,10 @@ public class TestCurrentDateProvider: SentryCurrentDateProvider { setDate(date: date().addingTimeInterval(TimeInterval(nanoseconds) / 1e9)) internalSystemTime += nanoseconds } + + public func advanceBy(interval: TimeInterval) { + setDate(date: date().addingTimeInterval(interval)) + } public var timezoneOffsetValue = 0 public override func timezoneOffset() -> Int { diff --git a/SentryTestUtils/TestTransport.swift b/SentryTestUtils/TestTransport.swift index 5e3a31bbbf6..eab268c9299 100644 --- a/SentryTestUtils/TestTransport.swift +++ b/SentryTestUtils/TestTransport.swift @@ -1,3 +1,4 @@ +import _SentryPrivate import Foundation @objc diff --git a/Sources/Configuration/SentryNoUI.xcconfig b/Sources/Configuration/SentryNoUI.xcconfig new file mode 100644 index 00000000000..ed2b439eaa4 --- /dev/null +++ b/Sources/Configuration/SentryNoUI.xcconfig @@ -0,0 +1,5 @@ +#include "Sentry.xcconfig" + +//This is how we avoid linking UIKit from Swift code +//when compiling without UIKit +OTHER_SWIFT_FLAGS = -DSENTRY_NO_UIKIT diff --git a/Sources/Sentry/Public/SentryOptions.h b/Sources/Sentry/Public/SentryOptions.h index f079aa4bd44..8f67f545c8d 100644 --- a/Sources/Sentry/Public/SentryOptions.h +++ b/Sources/Sentry/Public/SentryOptions.h @@ -3,7 +3,9 @@ NS_ASSUME_NONNULL_BEGIN -@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope; +@class SentryDsn, SentryMeasurementValue, SentryHttpStatusCodeRange, SentryScope, + SentryReplayOptions; +@class SentryExperimentalOptions; NS_SWIFT_NAME(Options) @interface SentryOptions : NSObject @@ -269,6 +271,7 @@ NS_SWIFT_NAME(Options) * @note Default value is @c NO . */ @property (nonatomic, assign) BOOL enablePreWarmedAppStartTracing; + #endif // SENTRY_UIKIT_AVAILABLE /** @@ -604,6 +607,12 @@ NS_SWIFT_NAME(Options) */ @property (nullable, nonatomic, copy) SentryBeforeEmitMetricCallback beforeEmitMetric; +/** + * This aggregates options for experimental features. + * Be aware that the options available for experimental can change at any time. + */ +@property (nonatomic, readonly) SentryExperimentalOptions *experimental; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryBaseIntegration.m b/Sources/Sentry/SentryBaseIntegration.m index ce9dc99ac6d..e1fc0b7ad81 100644 --- a/Sources/Sentry/SentryBaseIntegration.m +++ b/Sources/Sentry/SentryBaseIntegration.m @@ -1,6 +1,7 @@ #import "SentryBaseIntegration.h" #import "SentryCrashWrapper.h" #import "SentryLog.h" +#import "SentrySwift.h" #import #import #import @@ -140,6 +141,19 @@ - (BOOL)shouldBeEnabledWithOptions:(SentryOptions *)options [self logWithOptionName:@"attachViewHierarchy"]; return NO; } + + if (integrationOptions & kIntegrationOptionEnableReplay) { + if (@available(iOS 16.0, tvOS 16.0, *)) { + if (options.experimental.sessionReplay.errorSampleRate == 0 + && options.experimental.sessionReplay.sessionSampleRate == 0) { + [self logWithOptionName:@"sessionReplaySettings"]; + return NO; + } + } else { + [self logWithReason:@"Session replay requires iOS 16 or above"]; + return NO; + } + } #endif if ((integrationOptions & kIntegrationOptionEnableCrashHandler) diff --git a/Sources/Sentry/SentryClient.m b/Sources/Sentry/SentryClient.m index 5db9a2fe27b..0dd2139a01c 100644 --- a/Sources/Sentry/SentryClient.m +++ b/Sources/Sentry/SentryClient.m @@ -11,7 +11,7 @@ #import "SentryDependencyContainer.h" #import "SentryDispatchQueueWrapper.h" #import "SentryDsn.h" -#import "SentryEnvelope.h" +#import "SentryEnvelope+Private.h" #import "SentryEnvelopeItemType.h" #import "SentryEvent.h" #import "SentryException.h" @@ -27,13 +27,16 @@ #import "SentryMechanismMeta.h" #import "SentryMessage.h" #import "SentryMeta.h" +#import "SentryMsgPackSerializer.h" #import "SentryNSDictionarySanitize.h" #import "SentryNSError.h" #import "SentryOptions+Private.h" #import "SentryPropagationContext.h" #import "SentryRandom.h" +#import "SentryReplayEvent.h" #import "SentrySDK+Private.h" #import "SentryScope+Private.h" +#import "SentrySerialization.h" #import "SentrySession.h" #import "SentryStacktraceBuilder.h" #import "SentrySwift.h" @@ -472,13 +475,44 @@ - (void)captureSession:(SentrySession *)session } SentryEnvelopeItem *item = [[SentryEnvelopeItem alloc] initWithSession:session]; - SentryEnvelopeHeader *envelopeHeader = [[SentryEnvelopeHeader alloc] initWithId:nil - traceContext:nil]; - SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:envelopeHeader + SentryEnvelope *envelope = [[SentryEnvelope alloc] initWithHeader:[SentryEnvelopeHeader empty] singleItem:item]; [self captureEnvelope:envelope]; } +- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL + withScope:(SentryScope *)scope +{ + replayEvent = (SentryReplayEvent *)[self prepareEvent:replayEvent + withScope:scope + alwaysAttachStacktrace:NO]; + + if (![replayEvent isKindOfClass:SentryReplayEvent.class]) { + SENTRY_LOG_DEBUG(@"The event preprocessor didn't update the replay event in place. The " + @"replay was discarded."); + return; + } + + SentryEnvelopeItem *videoEnvelopeItem = + [[SentryEnvelopeItem alloc] initWithReplayEvent:replayEvent + replayRecording:replayRecording + video:videoURL]; + + if (videoEnvelopeItem == nil) { + SENTRY_LOG_DEBUG(@"The Session Replay segment will not be sent to Sentry because an " + @"Envelope Item could not be created."); + return; + } + + SentryEnvelope *envelope = [[SentryEnvelope alloc] + initWithHeader:[[SentryEnvelopeHeader alloc] initWithId:replayEvent.eventId] + items:@[ videoEnvelopeItem ]]; + + [self captureEnvelope:envelope]; +} + - (void)captureEnvelope:(SentryEnvelope *)envelope { if ([self isDisabled]) { @@ -553,9 +587,11 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event BOOL eventIsNotATransaction = event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeTransaction]; + BOOL eventIsNotReplay + = event.type == nil || ![event.type isEqualToString:SentryEnvelopeItemTypeReplayVideo]; - // Transactions have their own sampleRate - if (eventIsNotATransaction && [self isSampled:self.options.sampleRate]) { + // Transactions and replays have their own sampleRate + if (eventIsNotATransaction && eventIsNotReplay && [self isSampled:self.options.sampleRate]) { SENTRY_LOG_DEBUG(@"Event got sampled, will not send the event"); [self recordLostEvent:kSentryDataCategoryError reason:kSentryDiscardReasonSampleRate]; return nil; @@ -582,8 +618,8 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event [self setSdk:event]; - // We don't want to attach debug meta and stacktraces for transactions; - if (eventIsNotATransaction) { + // We don't want to attach debug meta and stacktraces for transactions and replays. + if (eventIsNotATransaction && eventIsNotReplay) { BOOL shouldAttachStacktrace = alwaysAttachStacktrace || self.options.attachStacktrace || (nil != event.exceptions && [event.exceptions count] > 0); @@ -623,6 +659,10 @@ - (SentryEvent *_Nullable)prepareEvent:(SentryEvent *)event event = [scope applyToEvent:event maxBreadcrumb:self.options.maxBreadcrumbs]; + if (!eventIsNotReplay) { + event.breadcrumbs = nil; + } + if ([self isWatchdogTermination:event isCrashEvent:isCrashEvent]) { // Remove some mutable properties from the device/app contexts which are no longer // applicable diff --git a/Sources/Sentry/SentryCoreGraphicsHelper.m b/Sources/Sentry/SentryCoreGraphicsHelper.m new file mode 100644 index 00000000000..56bb3816299 --- /dev/null +++ b/Sources/Sentry/SentryCoreGraphicsHelper.m @@ -0,0 +1,18 @@ +#import "SentryCoreGraphicsHelper.h" +#if SENTRY_HAS_UIKIT +@implementation SentryCoreGraphicsHelper ++ (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path +{ +# if (TARGET_OS_IOS || TARGET_OS_TV) +# ifdef __IPHONE_16_0 + if (@available(iOS 16.0, tvOS 16.0, *)) { + CGPathRef exclude = CGPathCreateWithRect(rectangle, nil); + CGPathRef newPath = CGPathCreateCopyBySubtractingPath(path, exclude, YES); + return CGPathCreateMutableCopy(newPath); + } +# endif // defined(__IPHONE_16_0) +# endif // (TARGET_OS_IOS || TARGET_OS_TV) + return path; +} +@end +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentryDataCategoryMapper.m b/Sources/Sentry/SentryDataCategoryMapper.m index b971bf19502..33664570c23 100644 --- a/Sources/Sentry/SentryDataCategoryMapper.m +++ b/Sources/Sentry/SentryDataCategoryMapper.m @@ -11,6 +11,7 @@ NSString *const kSentryDataCategoryNameAttachment = @"attachment"; NSString *const kSentryDataCategoryNameUserFeedback = @"user_report"; NSString *const kSentryDataCategoryNameProfile = @"profile"; +NSString *const kSentryDataCategoryNameReplay = @"replay"; NSString *const kSentryDataCategoryNameMetricBucket = @"metric_bucket"; NSString *const kSentryDataCategoryNameUnknown = @"unknown"; @@ -34,6 +35,9 @@ if ([itemType isEqualToString:SentryEnvelopeItemTypeProfile]) { return kSentryDataCategoryProfile; } + if ([itemType isEqualToString:SentryEnvelopeItemTypeReplayVideo]) { + return kSentryDataCategoryReplay; + } // The envelope item type used for metrics is statsd whereas the client report category for // discarded events is metric_bucket. if ([itemType isEqualToString:SentryEnvelopeItemTypeStatsd]) { @@ -79,6 +83,9 @@ if ([value isEqualToString:kSentryDataCategoryNameProfile]) { return kSentryDataCategoryProfile; } + if ([value isEqualToString:kSentryDataCategoryNameReplay]) { + return kSentryDataCategoryReplay; + } if ([value isEqualToString:kSentryDataCategoryNameMetricBucket]) { return kSentryDataCategoryMetricBucket; } @@ -114,6 +121,8 @@ return kSentryDataCategoryNameMetricBucket; case kSentryDataCategoryUnknown: return kSentryDataCategoryNameUnknown; + case kSentryDataCategoryReplay: + return kSentryDataCategoryNameReplay; } } diff --git a/Sources/Sentry/SentryDateUtil.m b/Sources/Sentry/SentryDateUtil.m index 49f2287af7d..ff7fb9fbced 100644 --- a/Sources/Sentry/SentryDateUtil.m +++ b/Sources/Sentry/SentryDateUtil.m @@ -46,6 +46,11 @@ + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_ } } ++ (long)millisecondsSince1970:(NSDate *)date +{ + return (long)([date timeIntervalSince1970] * 1000); +} + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryEnvelope.m b/Sources/Sentry/SentryEnvelope.m index fbe35f5c2d7..3ce89060b37 100644 --- a/Sources/Sentry/SentryEnvelope.m +++ b/Sources/Sentry/SentryEnvelope.m @@ -9,6 +9,9 @@ #import "SentryLog.h" #import "SentryMessage.h" #import "SentryMeta.h" +#import "SentryMsgPackSerializer.h" +#import "SentryReplayEvent.h" +#import "SentryReplayRecording.h" #import "SentrySdkInfo.h" #import "SentrySerialization.h" #import "SentrySession.h" @@ -48,6 +51,11 @@ - (instancetype)initWithId:(nullable SentryId *)eventId return self; } ++ (instancetype)empty +{ + return [[SentryEnvelopeHeader alloc] initWithId:nil traceContext:nil]; +} + @end @implementation SentryEnvelopeItem @@ -198,6 +206,38 @@ - (_Nullable instancetype)initWithAttachment:(SentryAttachment *)attachment return [self initWithHeader:itemHeader data:data]; } +- (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL +{ + NSData *replayEventData = [SentrySerialization dataWithJSONObject:[replayEvent serialize]]; + NSData *recording = [SentrySerialization dataWithReplayRecording:replayRecording]; + NSURL *envelopeContentUrl = + [[videoURL URLByDeletingPathExtension] URLByAppendingPathExtension:@"dat"]; + + BOOL success = [SentryMsgPackSerializer serializeDictionaryToMessagePack:@{ + @"replay_event" : replayEventData, + @"replay_recording" : recording, + @"replay_video" : videoURL + } + intoFile:envelopeContentUrl]; + if (success == NO) { + SENTRY_LOG_ERROR(@"Could not create MessagePack for session replay envelope item."); + return nil; + } + + NSData *envelopeItemContent = [NSData dataWithContentsOfURL:envelopeContentUrl]; + + NSError *error; + if (![NSFileManager.defaultManager removeItemAtURL:envelopeContentUrl error:&error]) { + SENTRY_LOG_ERROR(@"Cound not delete temporary replay content from disk: %@", error); + } + return [self initWithHeader:[[SentryEnvelopeItemHeader alloc] + initWithType:SentryEnvelopeItemTypeReplayVideo + length:envelopeItemContent.length] + data:envelopeItemContent]; +} + @end @implementation SentryEnvelope diff --git a/Sources/Sentry/SentryHub.m b/Sources/Sentry/SentryHub.m index a5891d5c21c..81be5ef1355 100644 --- a/Sources/Sentry/SentryHub.m +++ b/Sources/Sentry/SentryHub.m @@ -313,6 +313,16 @@ - (SentryId *)captureEvent:(SentryEvent *)event return SentryId.empty; } +- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL +{ + [_client captureReplayEvent:replayEvent + replayRecording:replayRecording + video:videoURL + withScope:self.scope]; +} + - (id)startTransactionWithName:(NSString *)name operation:(NSString *)operation { return [self startTransactionWithContext:[[SentryTransactionContext alloc] diff --git a/Sources/Sentry/SentryMsgPackSerializer.m b/Sources/Sentry/SentryMsgPackSerializer.m new file mode 100644 index 00000000000..1bbe76e027b --- /dev/null +++ b/Sources/Sentry/SentryMsgPackSerializer.m @@ -0,0 +1,110 @@ +#import "SentryMsgPackSerializer.h" +#import "SentryLog.h" + +@implementation SentryMsgPackSerializer + ++ (BOOL)serializeDictionaryToMessagePack: + (NSDictionary> *)dictionary + intoFile:(NSURL *)path +{ + NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:path append:NO]; + [outputStream open]; + + uint8_t mapHeader = (uint8_t)(0x80 | dictionary.count); // Map up to 15 elements + [outputStream write:&mapHeader maxLength:sizeof(uint8_t)]; + + for (NSString *key in dictionary) { + id value = dictionary[key]; + + NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding]; + uint8_t str8Header = (uint8_t)0xD9; // String up to 255 characters + uint8_t keyLength = (uint8_t)keyData.length; + [outputStream write:&str8Header maxLength:sizeof(uint8_t)]; + [outputStream write:&keyLength maxLength:sizeof(uint8_t)]; + + [outputStream write:keyData.bytes maxLength:keyData.length]; + + NSInteger dataLength = [value streamSize]; + if (dataLength <= 0) { + // MsgPack is being used strictly for session replay. + // An item with a length of 0 will not be useful. + // If we plan to use MsgPack for something else, + // this needs to be re-evaluated. + SENTRY_LOG_DEBUG(@"Data for MessagePack dictionary has no content - Input: %@", value); + return NO; + } + + uint32_t valueLength = (uint32_t)dataLength; + // We will always use the 4 bytes data length for simplicity. + // Worst case we're losing 3 bytes. + uint8_t bin32Header = (uint8_t)0xC6; + [outputStream write:&bin32Header maxLength:sizeof(uint8_t)]; + valueLength = NSSwapHostIntToBig(valueLength); + [outputStream write:(uint8_t *)&valueLength maxLength:sizeof(uint32_t)]; + + NSInputStream *inputStream = [value asInputStream]; + [inputStream open]; + + uint8_t buffer[1024]; + NSInteger bytesRead; + + while ([inputStream hasBytesAvailable]) { + bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; + if (bytesRead > 0) { + [outputStream write:buffer maxLength:bytesRead]; + } else if (bytesRead < 0) { + SENTRY_LOG_DEBUG(@"Error reading bytes from input stream - Input: %@ - %li", value, + (long)bytesRead); + + [inputStream close]; + [outputStream close]; + return NO; + } + } + + [inputStream close]; + } + [outputStream close]; + + return YES; +} + +@end + +@implementation +NSURL (SentryStreameble) + +- (NSInputStream *)asInputStream +{ + return [[NSInputStream alloc] initWithURL:self]; +} + +- (NSInteger)streamSize +{ + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error; + NSDictionary *attributes = [fileManager attributesOfItemAtPath:self.path error:&error]; + if (attributes == nil) { + SENTRY_LOG_DEBUG(@"Could not read file attributes - File: %@ - %@", self, error); + return -1; + } + NSNumber *fileSize = attributes[NSFileSize]; + return [fileSize unsignedIntegerValue]; +} + +@end + +@implementation +NSData (SentryStreameble) + +- (NSInputStream *)asInputStream +{ + return [[NSInputStream alloc] initWithData:self]; +} + +- (NSInteger)streamSize +{ + return self.length; +} + +@end diff --git a/Sources/Sentry/SentryOptions.m b/Sources/Sentry/SentryOptions.m index 24640b20046..8e114e7773f 100644 --- a/Sources/Sentry/SentryOptions.m +++ b/Sources/Sentry/SentryOptions.m @@ -15,17 +15,16 @@ #import "SentryOptions+Private.h" #import "SentrySDK.h" #import "SentryScope.h" +#import "SentrySessionReplayIntegration.h" +#import "SentrySwift.h" #import "SentrySwiftAsyncIntegration.h" - #import #if SENTRY_HAS_UIKIT # import "SentryAppStartTrackingIntegration.h" # import "SentryFramesTrackingIntegration.h" # import "SentryPerformanceTrackingIntegration.h" -# if SENTRY_HAS_UIKIT -# import "SentryScreenshotIntegration.h" -# endif // SENTRY_HAS_UIKIT +# import "SentryScreenshotIntegration.h" # import "SentryUIEventTrackingIntegration.h" # import "SentryViewHierarchyIntegration.h" # import "SentryWatchdogTerminationTrackingIntegration.h" @@ -59,6 +58,9 @@ - (void)setMeasurement:(SentryMeasurementValue *)measurement NSStringFromClass([SentryUIEventTrackingIntegration class]), NSStringFromClass([SentryViewHierarchyIntegration class]), NSStringFromClass([SentryWatchdogTerminationTrackingIntegration class]), +# if !TARGET_OS_VISION + NSStringFromClass([SentrySessionReplayIntegration class]), +# endif #endif // SENTRY_HAS_UIKIT NSStringFromClass([SentryANRTrackingIntegration class]), NSStringFromClass([SentryAutoBreadcrumbTrackingIntegration class]), @@ -104,7 +106,7 @@ - (instancetype)init self.enableTimeToFullDisplayTracing = NO; self.initialScope = ^SentryScope *(SentryScope *scope) { return scope; }; - + _experimental = [[SentryExperimentalOptions alloc] init]; _enableTracing = NO; _enableTracingManual = NO; #if SENTRY_HAS_UIKIT @@ -387,7 +389,6 @@ - (BOOL)validateOptions:(NSDictionary *)options if ([self isBlock:options[@"initialScope"]]) { self.initialScope = options[@"initialScope"]; } - #if SENTRY_HAS_UIKIT [self setBool:options[@"enableUIViewControllerTracing"] block:^(BOOL value) { self->_enableUIViewControllerTracing = value; }]; @@ -407,6 +408,7 @@ - (BOOL)validateOptions:(NSDictionary *)options [self setBool:options[@"enablePreWarmedAppStartTracing"] block:^(BOOL value) { self->_enablePreWarmedAppStartTracing = value; }]; + #endif // SENTRY_HAS_UIKIT [self setBool:options[@"enableAppHangTracking"] @@ -522,6 +524,10 @@ - (BOOL)validateOptions:(NSDictionary *)options self.beforeEmitMetric = options[@"beforeEmitMetric"]; } + if ([options[@"experimental"] isKindOfClass:NSDictionary.class]) { + [self.experimental validateOptions:options[@"experimental"]]; + } + return YES; } diff --git a/Sources/Sentry/SentryReplayEvent.m b/Sources/Sentry/SentryReplayEvent.m new file mode 100644 index 00000000000..c5b28c8485c --- /dev/null +++ b/Sources/Sentry/SentryReplayEvent.m @@ -0,0 +1,41 @@ +#import "SentryReplayEvent.h" +#import "SentryDateUtil.h" +#import "SentryEnvelopeItemType.h" +#import "SentrySwift.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SentryReplayEvent + +- (instancetype)init +{ + if (self = [super init]) { + self.type = SentryEnvelopeItemTypeReplayVideo; + } + return self; +} + +- (NSDictionary *)serialize +{ + NSMutableDictionary *result = [[super serialize] mutableCopy]; + + NSMutableArray *trace_ids = [[NSMutableArray alloc] initWithCapacity:self.traceIds.count]; + + for (SentryId *traceId in self.traceIds) { + [trace_ids addObject:traceId.sentryIdString]; + } + + result[@"urls"] = self.urls; + result[@"replay_start_timestamp"] = @(self.replayStartTimestamp.timeIntervalSince1970); + result[@"trace_ids"] = trace_ids; + result[@"replay_id"] = self.eventId.sentryIdString; + result[@"segment_id"] = @(self.segmentId); + result[@"replay_type"] = nameForSentryReplayType(self.replayType); + result[@"error_ids"] = @[]; + + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryReplayRecording.m b/Sources/Sentry/SentryReplayRecording.m new file mode 100644 index 00000000000..059ac1bfff7 --- /dev/null +++ b/Sources/Sentry/SentryReplayRecording.m @@ -0,0 +1,75 @@ +#import "SentryReplayRecording.h" +#import "SentryDateUtil.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation SentryReplayRecording + +- (instancetype)initWithSegmentId:(NSInteger)segmentId + size:(NSInteger)size + start:(NSDate *)start + duration:(NSTimeInterval)duration + frameCount:(NSInteger)frameCount + frameRate:(NSInteger)frameRate + height:(NSInteger)height + width:(NSInteger)width +{ + if (self = [super init]) { + self.segmentId = segmentId; + self.size = size; + self.start = start; + self.duration = duration; + self.frameCount = frameCount; + self.frameRate = frameRate; + self.height = height; + self.width = width; + } + return self; +} + +- (NSDictionary *)headerForReplayRecording +{ + return @{ @"segment_id" : @(self.segmentId) }; +} + +- (NSArray *> *)serialize +{ + + long timestamp = [SentryDateUtil millisecondsSince1970:self.start]; + + // This format is defined by RRWeb + // empty values are required by the format + NSDictionary *metaInfo = @{ + @"type" : @4, + @"timestamp" : @(timestamp), + @"data" : @ { @"href" : @"", @"height" : @(self.height), @"width" : @(self.width) } + }; + + NSDictionary *recordingInfo = @{ + @"type" : @5, + @"timestamp" : @(timestamp), + @"data" : @ { + @"tag" : @"video", + @"payload" : @ { + @"segmentId" : @(self.segmentId), + @"size" : @(self.size), + @"duration" : @(self.duration), + @"encoding" : SentryReplayEncoding, + @"container" : SentryReplayContainer, + @"height" : @(self.height), + @"width" : @(self.width), + @"frameCount" : @(self.frameCount), + @"frameRateType" : SentryReplayFrameRateType, + @"frameRate" : @(self.frameRate), + @"left" : @0, + @"top" : @0, + } + } + }; + + return @[ metaInfo, recordingInfo ]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryReplayType.m b/Sources/Sentry/SentryReplayType.m new file mode 100644 index 00000000000..c4d200310f7 --- /dev/null +++ b/Sources/Sentry/SentryReplayType.m @@ -0,0 +1,14 @@ +#import "SentryReplayType.h" + +NSString *const kSentryReplayTypeNameBuffer = @"buffer"; +NSString *const kSentryReplayTypeNameSession = @"session"; + +NSString *_Nonnull nameForSentryReplayType(SentryReplayType replayType) +{ + switch (replayType) { + case kSentryReplayTypeBuffer: + return kSentryReplayTypeNameBuffer; + case kSentryReplayTypeSession: + return kSentryReplayTypeNameSession; + } +} diff --git a/Sources/Sentry/SentrySerialization.m b/Sources/Sentry/SentrySerialization.m index f81b2e45600..841567515a9 100644 --- a/Sources/Sentry/SentrySerialization.m +++ b/Sources/Sentry/SentrySerialization.m @@ -7,6 +7,7 @@ #import "SentryError.h" #import "SentryLevelMapper.h" #import "SentryLog.h" +#import "SentryReplayRecording.h" #import "SentrySdkInfo.h" #import "SentrySession.h" #import "SentrySwift.h" @@ -16,15 +17,15 @@ @implementation SentrySerialization -+ (NSData *_Nullable)dataWithJSONObject:(NSDictionary *)dictionary ++ (NSData *_Nullable)dataWithJSONObject:(id)jsonObject { - if (![NSJSONSerialization isValidJSONObject:dictionary]) { + if (![NSJSONSerialization isValidJSONObject:jsonObject]) { SENTRY_LOG_ERROR(@"Dictionary is not a valid JSON object."); return nil; } NSError *error = nil; - NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:&error]; + NSData *data = [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:&error]; if (error) { SENTRY_LOG_ERROR(@"Internal error while serializing JSON: %@", error); } @@ -321,6 +322,16 @@ + (SentrySession *_Nullable)sessionWithData:(NSData *)sessionData return session; } ++ (NSData *)dataWithReplayRecording:(SentryReplayRecording *)replayRecording +{ + NSMutableData *recording = [NSMutableData data]; + [recording appendData:[SentrySerialization + dataWithJSONObject:[replayRecording headerForReplayRecording]]]; + [recording appendData:[@"\n" dataUsingEncoding:NSUTF8StringEncoding]]; + [recording appendData:[SentrySerialization dataWithJSONObject:[replayRecording serialize]]]; + return recording; +} + + (SentryAppState *_Nullable)appStateWithData:(NSData *)data { NSError *error = nil; diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m new file mode 100644 index 00000000000..d6830ed4f19 --- /dev/null +++ b/Sources/Sentry/SentrySessionReplay.m @@ -0,0 +1,276 @@ +#import "SentrySessionReplay.h" +#import "SentryAttachment+Private.h" +#import "SentryDependencyContainer.h" +#import "SentryDisplayLinkWrapper.h" +#import "SentryFileManager.h" +#import "SentryHub+Private.h" +#import "SentryLog.h" +#import "SentryRandom.h" +#import "SentryReplayEvent.h" +#import "SentryReplayRecording.h" +#import "SentrySDK+Private.h" +#import "SentrySwift.h" + +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION + +NS_ASSUME_NONNULL_BEGIN + +@interface +SentrySessionReplay () + +@property (nonatomic) BOOL isRunning; + +@property (nonatomic) BOOL isFullSession; + +@end + +@implementation SentrySessionReplay { + NSURL *_urlToCache; + UIView *_rootView; + NSDate *_lastScreenShot; + NSDate *_videoSegmentStart; + NSDate *_sessionStart; + NSMutableArray *imageCollection; + SentryId *sessionReplayId; + SentryReplayOptions *_replayOptions; + SentryOnDemandReplay *_replayMaker; + SentryDisplayLinkWrapper *_displayLink; + SentryCurrentDateProvider *_dateProvider; + id _sentryRandom; + id _screenshotProvider; + int _currentSegmentId; + BOOL _processingScreenshot; + BOOL _reachedMaximumDuration; +} + +- (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions + replayFolderPath:(NSURL *)folderPath + screenshotProvider:(id)screenshotProvider + replayMaker:(id)replayMaker + dateProvider:(SentryCurrentDateProvider *)dateProvider + random:(id)random + displayLinkWrapper:(SentryDisplayLinkWrapper *)displayLinkWrapper; +{ + if (self = [super init]) { + _replayOptions = replayOptions; + _dateProvider = dateProvider; + _sentryRandom = random; + _screenshotProvider = screenshotProvider; + _displayLink = displayLinkWrapper; + _isRunning = NO; + _urlToCache = folderPath; + _replayMaker = replayMaker; + _reachedMaximumDuration = NO; + } + return self; +} + +- (void)start:(UIView *)rootView fullSession:(BOOL)full +{ + if (rootView == nil) { + SENTRY_LOG_DEBUG(@"rootView cannot be nil. Session replay will not be recorded."); + return; + } + + if (_isRunning) { + return; + } + + @synchronized(self) { + if (_isRunning) { + return; + } + [_displayLink linkWithTarget:self selector:@selector(newFrame:)]; + _isRunning = YES; + } + + _rootView = rootView; + _lastScreenShot = _dateProvider.date; + _videoSegmentStart = nil; + _currentSegmentId = 0; + sessionReplayId = [[SentryId alloc] init]; + + imageCollection = [NSMutableArray array]; + if (full) { + [self startFullReplay]; + } +} + +- (void)startFullReplay +{ + _sessionStart = _lastScreenShot; + _isFullSession = YES; +} + +- (void)stop +{ + @synchronized(self) { + [_displayLink invalidate]; + _isRunning = NO; + } +} + +- (void)captureReplayForEvent:(SentryEvent *)event; +{ + if (_isFullSession || !_isRunning) { + return; + } + + if (event.error == nil && (event.exceptions == nil || event.exceptions.count == 0)) { + return; + } + + if ([_sentryRandom nextNumber] > _replayOptions.errorSampleRate) { + return; + } + + NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; + NSDate *replayStart = + [_dateProvider.date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; + + [self createAndCapture:finalPath + duration:_replayOptions.errorReplayDuration + startedAt:replayStart]; + + [self startFullReplay]; +} + +- (void)newFrame:(CADisplayLink *)sender +{ + if (!_isRunning) { + return; + } + + NSDate *now = _dateProvider.date; + + if (_isFullSession && + [now timeIntervalSinceDate:_sessionStart] > _replayOptions.maximumDuration) { + _reachedMaximumDuration = YES; + [self prepareSegmentUntil:now]; + [self stop]; + return; + } + + if ([now timeIntervalSinceDate:_lastScreenShot] >= 1) { + [self takeScreenshot]; + _lastScreenShot = now; + + if (_videoSegmentStart == nil) { + _videoSegmentStart = now; + } else if (_isFullSession && + [now timeIntervalSinceDate:_videoSegmentStart] + >= _replayOptions.sessionSegmentDuration) { + [self prepareSegmentUntil:now]; + } + } +} + +- (void)prepareSegmentUntil:(NSDate *)date +{ + NSURL *pathToSegment = [_urlToCache URLByAppendingPathComponent:@"segments"]; + + if (![NSFileManager.defaultManager fileExistsAtPath:pathToSegment.path]) { + NSError *error; + if (![NSFileManager.defaultManager createDirectoryAtPath:pathToSegment.path + withIntermediateDirectories:YES + attributes:nil + error:&error]) { + SENTRY_LOG_ERROR(@"Can't create session replay segment folder. Error: %@", + error.localizedDescription); + return; + } + } + + pathToSegment = [pathToSegment + URLByAppendingPathComponent:[NSString stringWithFormat:@"%i.mp4", _currentSegmentId]]; + + NSDate *segmentStart = + [_dateProvider.date dateByAddingTimeInterval:-_replayOptions.sessionSegmentDuration]; + + [self createAndCapture:pathToSegment + duration:_replayOptions.sessionSegmentDuration + startedAt:segmentStart]; +} + +- (void)createAndCapture:(NSURL *)videoUrl + duration:(NSTimeInterval)duration + startedAt:(NSDate *)start +{ + [_replayMaker + createVideoWithDuration:duration + beginning:start + outputFileURL:videoUrl + error:nil + completion:^(SentryVideoInfo *videoInfo, NSError *error) { + if (error != nil) { + SENTRY_LOG_ERROR(@"Could not create replay video - %@", error); + } else { + [self captureSegment:self->_currentSegmentId++ + video:videoInfo + replayId:self->sessionReplayId + replayType:kSentryReplayTypeSession]; + + [self->_replayMaker releaseFramesUntil:videoInfo.end]; + self->_videoSegmentStart = nil; + } + }]; +} + +- (void)captureSegment:(NSInteger)segment + video:(SentryVideoInfo *)videoInfo + replayId:(SentryId *)replayid + replayType:(SentryReplayType)replayType +{ + SentryReplayEvent *replayEvent = [[SentryReplayEvent alloc] init]; + replayEvent.replayType = replayType; + replayEvent.eventId = replayid; + replayEvent.replayStartTimestamp = videoInfo.start; + replayEvent.segmentId = segment; + replayEvent.timestamp = videoInfo.end; + + SentryReplayRecording *recording = + [[SentryReplayRecording alloc] initWithSegmentId:replayEvent.segmentId + size:videoInfo.fileSize + start:videoInfo.start + duration:videoInfo.duration + frameCount:videoInfo.frameCount + frameRate:videoInfo.frameRate + height:videoInfo.height + width:videoInfo.width]; + + [SentrySDK.currentHub captureReplayEvent:replayEvent + replayRecording:recording + video:videoInfo.path]; + + NSError *error; + if (![NSFileManager.defaultManager removeItemAtURL:videoInfo.path error:&error]) { + SENTRY_LOG_ERROR(@"Cound not delete replay segment from disk: %@", error); + } +} + +- (void)takeScreenshot +{ + if (_processingScreenshot) { + return; + } + @synchronized(self) { + if (_processingScreenshot) { + return; + } + _processingScreenshot = YES; + } + + UIImage *screenshot = [_screenshotProvider imageWithView:_rootView options:_replayOptions]; + + _processingScreenshot = NO; + + dispatch_queue_t backgroundQueue + = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(backgroundQueue, ^{ [self->_replayMaker addFrameWithImage:screenshot]; }); +} + +@end + +NS_ASSUME_NONNULL_END + +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/SentrySessionReplayIntegration.m b/Sources/Sentry/SentrySessionReplayIntegration.m new file mode 100644 index 00000000000..3664c37e058 --- /dev/null +++ b/Sources/Sentry/SentrySessionReplayIntegration.m @@ -0,0 +1,129 @@ +#import "SentrySessionReplayIntegration.h" + +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION + +# import "SentryClient+Private.h" +# import "SentryDependencyContainer.h" +# import "SentryDisplayLinkWrapper.h" +# import "SentryFileManager.h" +# import "SentryGlobalEventProcessor.h" +# import "SentryHub+Private.h" +# import "SentryOptions.h" +# import "SentryRandom.h" +# import "SentrySDK+Private.h" +# import "SentrySessionReplay.h" +# import "SentrySwift.h" +# import "SentryUIApplication.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *SENTRY_REPLAY_FOLDER = @"replay"; + +API_AVAILABLE(ios(16.0), tvos(16.0)) +@interface +SentrySessionReplayIntegration () +@property (nonatomic, strong) SentrySessionReplay *sessionReplay; +@end + +API_AVAILABLE(ios(16.0), tvos(16.0)) +@interface +SentryViewPhotographer (SentryViewScreenshotProvider) +@end + +API_AVAILABLE(ios(16.0), tvos(16.0)) +@interface +SentryOnDemandReplay (SentryReplayMaker) +@end + +@implementation SentrySessionReplayIntegration + +- (BOOL)installWithOptions:(nonnull SentryOptions *)options +{ + if ([super installWithOptions:options] == NO) { + return NO; + } + + if (@available(iOS 16.0, tvOS 16.0, *)) { + SentryReplayOptions *replayOptions = options.experimental.sessionReplay; + + BOOL shouldReplayFullSession = + [self shouldReplayFullSession:replayOptions.sessionSampleRate]; + + if (!shouldReplayFullSession && replayOptions.errorSampleRate == 0) { + return NO; + } + + NSURL *docs = [NSURL + fileURLWithPath:[SentryDependencyContainer.sharedInstance.fileManager sentryPath]]; + docs = [docs URLByAppendingPathComponent:SENTRY_REPLAY_FOLDER]; + NSString *currentSession = [NSUUID UUID].UUIDString; + docs = [docs URLByAppendingPathComponent:currentSession]; + + if (![NSFileManager.defaultManager fileExistsAtPath:docs.path]) { + [NSFileManager.defaultManager createDirectoryAtURL:docs + withIntermediateDirectories:YES + attributes:nil + error:nil]; + } + + SentryOnDemandReplay *replayMaker = + [[SentryOnDemandReplay alloc] initWithOutputPath:docs.path]; + replayMaker.bitRate = replayOptions.replayBitRate; + replayMaker.cacheMaxSize + = (NSInteger)(shouldReplayFullSession ? replayOptions.sessionSegmentDuration + : replayOptions.errorReplayDuration); + + self.sessionReplay = [[SentrySessionReplay alloc] + initWithSettings:replayOptions + replayFolderPath:docs + screenshotProvider:SentryViewPhotographer.shared + replayMaker:replayMaker + dateProvider:SentryDependencyContainer.sharedInstance.dateProvider + random:SentryDependencyContainer.sharedInstance.random + + displayLinkWrapper:[[SentryDisplayLinkWrapper alloc] init]]; + + [self.sessionReplay + start:SentryDependencyContainer.sharedInstance.application.windows.firstObject + fullSession:[self shouldReplayFullSession:replayOptions.sessionSampleRate]]; + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(stop) + name:UIApplicationDidEnterBackgroundNotification + object:nil]; + + [SentryGlobalEventProcessor.shared + addEventProcessor:^SentryEvent *_Nullable(SentryEvent *_Nonnull event) { + [self.sessionReplay captureReplayForEvent:event]; + return event; + }]; + + return YES; + } else { + return NO; + } +} + +- (void)stop +{ + [self.sessionReplay stop]; +} + +- (SentryIntegrationOption)integrationOptions +{ + return kIntegrationOptionEnableReplay; +} + +- (void)uninstall +{ +} + +- (BOOL)shouldReplayFullSession:(CGFloat)rate +{ + return [SentryDependencyContainer.sharedInstance.random nextNumber] < rate; +} + +@end +NS_ASSUME_NONNULL_END + +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/HybridPublic/SentryEnvelope.h b/Sources/Sentry/include/HybridPublic/SentryEnvelope.h index d9d55084c72..6caa365e029 100644 --- a/Sources/Sentry/include/HybridPublic/SentryEnvelope.h +++ b/Sources/Sentry/include/HybridPublic/SentryEnvelope.h @@ -72,6 +72,8 @@ SENTRY_NO_INIT */ @property (nullable, nonatomic, copy) NSDate *sentAt; ++ (instancetype)empty; + @end @interface SentryEnvelopeItem : NSObject diff --git a/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h b/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h index fed1a34577f..12616156570 100644 --- a/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h +++ b/Sources/Sentry/include/HybridPublic/SentryEnvelopeItemType.h @@ -5,4 +5,5 @@ static NSString *const SentryEnvelopeItemTypeTransaction = @"transaction"; static NSString *const SentryEnvelopeItemTypeAttachment = @"attachment"; static NSString *const SentryEnvelopeItemTypeClientReport = @"client_report"; static NSString *const SentryEnvelopeItemTypeProfile = @"profile"; +static NSString *const SentryEnvelopeItemTypeReplayVideo = @"replay_video"; static NSString *const SentryEnvelopeItemTypeStatsd = @"statsd"; diff --git a/Sources/Sentry/include/SentryBaseIntegration.h b/Sources/Sentry/include/SentryBaseIntegration.h index f78866b8377..c8adad48fc9 100644 --- a/Sources/Sentry/include/SentryBaseIntegration.h +++ b/Sources/Sentry/include/SentryBaseIntegration.h @@ -22,6 +22,7 @@ typedef NS_OPTIONS(NSUInteger, SentryIntegrationOption) { kIntegrationOptionAttachViewHierarchy = 1 << 15, kIntegrationOptionEnableCrashHandler = 1 << 16, kIntegrationOptionEnableMetricKit = 1 << 17, + kIntegrationOptionEnableReplay = 1 << 18, }; @class SentryOptions; diff --git a/Sources/Sentry/include/SentryClient+Private.h b/Sources/Sentry/include/SentryClient+Private.h index a9bcd469818..5bd2d6f3387 100644 --- a/Sources/Sentry/include/SentryClient+Private.h +++ b/Sources/Sentry/include/SentryClient+Private.h @@ -3,7 +3,7 @@ #import "SentryDiscardReason.h" @class SentrySession, SentryEnvelopeItem, SentryId, SentryAttachment, SentryThreadInspector, - SentryEnvelope; + SentryReplayEvent, SentryReplayRecording, SentryEnvelope; NS_ASSUME_NONNULL_BEGIN @@ -42,6 +42,11 @@ SentryClient () additionalEnvelopeItems:(NSArray *)additionalEnvelopeItems NS_SWIFT_NAME(capture(event:scope:additionalEnvelopeItems:)); +- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL + withScope:(SentryScope *)scope; + - (void)captureSession:(SentrySession *)session NS_SWIFT_NAME(capture(session:)); /** diff --git a/Sources/Sentry/include/SentryCoreGraphicsHelper.h b/Sources/Sentry/include/SentryCoreGraphicsHelper.h new file mode 100644 index 00000000000..e561984de1b --- /dev/null +++ b/Sources/Sentry/include/SentryCoreGraphicsHelper.h @@ -0,0 +1,13 @@ +#import "SentryDefines.h" +#import +#import + +NS_ASSUME_NONNULL_BEGIN +#if SENTRY_HAS_UIKIT + +@interface SentryCoreGraphicsHelper : NSObject ++ (CGMutablePathRef)excludeRect:(CGRect)rectangle fromPath:(CGMutablePathRef)path; +@end + +#endif +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryDataCategory.h b/Sources/Sentry/include/SentryDataCategory.h index 67da2b9ff49..7b4e281deb1 100644 --- a/Sources/Sentry/include/SentryDataCategory.h +++ b/Sources/Sentry/include/SentryDataCategory.h @@ -15,5 +15,6 @@ typedef NS_ENUM(NSUInteger, SentryDataCategory) { kSentryDataCategoryUserFeedback = 6, kSentryDataCategoryProfile = 7, kSentryDataCategoryMetricBucket = 8, - kSentryDataCategoryUnknown = 9 + kSentryDataCategoryReplay = 9, + kSentryDataCategoryUnknown = 10 }; diff --git a/Sources/Sentry/include/SentryDataCategoryMapper.h b/Sources/Sentry/include/SentryDataCategoryMapper.h index 021f99e71e1..2fcfd9303f4 100644 --- a/Sources/Sentry/include/SentryDataCategoryMapper.h +++ b/Sources/Sentry/include/SentryDataCategoryMapper.h @@ -11,6 +11,7 @@ FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameTransaction; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameAttachment; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameUserFeedback; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameProfile; +FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameReplay; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameMetricBucket; FOUNDATION_EXPORT NSString *const kSentryDataCategoryNameUnknown; diff --git a/Sources/Sentry/include/SentryDateUtil.h b/Sources/Sentry/include/SentryDateUtil.h index 0b27914b21c..333e20eb208 100644 --- a/Sources/Sentry/include/SentryDateUtil.h +++ b/Sources/Sentry/include/SentryDateUtil.h @@ -13,6 +13,8 @@ SENTRY_NO_INIT + (NSDate *_Nullable)getMaximumDate:(NSDate *_Nullable)first andOther:(NSDate *_Nullable)second; ++ (long)millisecondsSince1970:(NSDate *)date; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryEnvelope+Private.h b/Sources/Sentry/include/SentryEnvelope+Private.h index faedf16321d..b2b29f67fb5 100644 --- a/Sources/Sentry/include/SentryEnvelope+Private.h +++ b/Sources/Sentry/include/SentryEnvelope+Private.h @@ -2,6 +2,8 @@ NS_ASSUME_NONNULL_BEGIN +@class SentryReplayEvent; +@class SentryReplayRecording; @class SentryClientReport; @interface @@ -9,6 +11,10 @@ SentryEnvelopeItem () - (instancetype)initWithClientReport:(SentryClientReport *)clientReport; +- (nullable instancetype)initWithReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL; + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryHub+Private.h b/Sources/Sentry/include/SentryHub+Private.h index 275109fb451..0a95cf674cc 100644 --- a/Sources/Sentry/include/SentryHub+Private.h +++ b/Sources/Sentry/include/SentryHub+Private.h @@ -10,6 +10,8 @@ @class SentrySession; @class SentryTracer; @class SentryTracerConfiguration; +@class SentryReplayEvent; +@class SentryReplayRecording; @protocol SentryIntegrationProtocol; NS_ASSUME_NONNULL_BEGIN @@ -34,6 +36,10 @@ SentryHub () - (void)captureCrashEvent:(SentryEvent *)event withScope:(SentryScope *)scope; +- (void)captureReplayEvent:(SentryReplayEvent *)replayEvent + replayRecording:(SentryReplayRecording *)replayRecording + video:(NSURL *)videoURL; + - (void)closeCachedSessionWithTimestamp:(NSDate *_Nullable)timestamp; - (SentryTracer *)startTransactionWithContext:(SentryTransactionContext *)transactionContext diff --git a/Sources/Sentry/include/SentryMsgPackSerializer.h b/Sources/Sentry/include/SentryMsgPackSerializer.h new file mode 100644 index 00000000000..d6a1485e372 --- /dev/null +++ b/Sources/Sentry/include/SentryMsgPackSerializer.h @@ -0,0 +1,33 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol SentryStreamable + +- (NSInputStream *)asInputStream; + +- (NSInteger)streamSize; + +@end + +/** + * This is a partial implementation of the MessagePack format. + * We only need to concatenate a list of NSData into an envelope item. + */ +@interface SentryMsgPackSerializer : NSObject + ++ (BOOL)serializeDictionaryToMessagePack: + (NSDictionary> *)dictionary + intoFile:(NSURL *)path; + +@end + +@interface +NSData (inputStreameble) +@end + +@interface +NSURL (inputStreameble) +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryPrivate.h b/Sources/Sentry/include/SentryPrivate.h index 60473b17298..5d66a9971e1 100644 --- a/Sources/Sentry/include/SentryPrivate.h +++ b/Sources/Sentry/include/SentryPrivate.h @@ -1,10 +1,14 @@ // Sentry internal headers that are needed for swift code - #import "SentryBaggage.h" -#import "SentryBaseIntegration.h" #import "SentryDispatchQueueWrapper.h" #import "SentryNSDataUtils.h" #import "SentryRandom.h" +#import "SentryReplayRecording.h" +#import "SentryReplayType.h" #import "SentrySdkInfo.h" #import "SentryStatsdClient.h" #import "SentryTime.h" + +// Headers that also import SentryDefines should be at the end of this list +// otherwise it wont compile +#import "SentryCoreGraphicsHelper.h" diff --git a/Sources/Sentry/include/SentryReplayEvent.h b/Sources/Sentry/include/SentryReplayEvent.h new file mode 100644 index 00000000000..14a9fd382d5 --- /dev/null +++ b/Sources/Sentry/include/SentryReplayEvent.h @@ -0,0 +1,40 @@ +#import "SentryEvent.h" +#import "SentryReplayType.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@class SentryId; + +@interface SentryReplayEvent : SentryEvent + +/** + * Start time of the replay segment + */ +@property (nonatomic, strong) NSDate *replayStartTimestamp; + +/** + * Number of the segment in the replay. + * This is an incremental number + */ +@property (nonatomic) NSInteger segmentId; + +/** + * This will be used to store the name of the screens + * that appear during the duration of the replay segment. + */ +@property (nonatomic, strong) NSArray *urls; + +/** + * Trace ids happening during the duration of the replay segment. + */ +@property (nonatomic, strong) NSArray *traceIds; + +/** + * The type of the replay + */ +@property (nonatomic) SentryReplayType replayType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryReplayRecording.h b/Sources/Sentry/include/SentryReplayRecording.h new file mode 100644 index 00000000000..40cedc079dd --- /dev/null +++ b/Sources/Sentry/include/SentryReplayRecording.h @@ -0,0 +1,45 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const SentryReplayEncoding = @"h264"; +static NSString *const SentryReplayContainer = @"mp4"; +static NSString *const SentryReplayFrameRateType = @"constant"; + +@interface SentryReplayRecording : NSObject + +@property (nonatomic) NSInteger segmentId; + +/** + * Video file size + */ +@property (nonatomic) NSInteger size; + +@property (nonatomic, strong) NSDate *start; + +@property (nonatomic) NSTimeInterval duration; + +@property (nonatomic) NSInteger frameCount; + +@property (nonatomic) NSInteger frameRate; + +@property (nonatomic) NSInteger height; + +@property (nonatomic) NSInteger width; + +- (instancetype)initWithSegmentId:(NSInteger)segmentId + size:(NSInteger)size + start:(NSDate *)start + duration:(NSTimeInterval)duration + frameCount:(NSInteger)frameCount + frameRate:(NSInteger)frameRate + height:(NSInteger)height + width:(NSInteger)width; + +- (NSArray *> *)serialize; + +- (NSDictionary *)headerForReplayRecording; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentryReplayType.h b/Sources/Sentry/include/SentryReplayType.h new file mode 100644 index 00000000000..93c018806b5 --- /dev/null +++ b/Sources/Sentry/include/SentryReplayType.h @@ -0,0 +1,16 @@ + +#import + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, SentryReplayType) { + kSentryReplayTypeBuffer = 0, // Replay triggered by an action + kSentryReplayTypeSession // Full session replay +}; + +FOUNDATION_EXPORT NSString *const kSentryReplayTypeNameBuffer; +FOUNDATION_EXPORT NSString *const kSentryReplayTypeNameSession; + +NSString *nameForSentryReplayType(SentryReplayType replayType); + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentrySerialization.h b/Sources/Sentry/include/SentrySerialization.h index fbfcec32e4d..0ff1c23951b 100644 --- a/Sources/Sentry/include/SentrySerialization.h +++ b/Sources/Sentry/include/SentrySerialization.h @@ -1,6 +1,6 @@ #import "SentryDefines.h" -@class SentrySession, SentryEnvelope, SentryAppState; +@class SentrySession, SentryEnvelope, SentryAppState, SentryReplayRecording; NS_ASSUME_NONNULL_BEGIN @@ -8,7 +8,7 @@ static int const SENTRY_BAGGAGE_MAX_SIZE = 8192; @interface SentrySerialization : NSObject -+ (NSData *_Nullable)dataWithJSONObject:(NSDictionary *)dictionary; ++ (NSData *_Nullable)dataWithJSONObject:(id)jsonObject; + (NSData *_Nullable)dataWithSession:(SentrySession *)session; @@ -20,6 +20,8 @@ static int const SENTRY_BAGGAGE_MAX_SIZE = 8192; + (NSData *_Nullable)dataWithEnvelope:(SentryEnvelope *)envelope error:(NSError *_Nullable *_Nullable)error; ++ (NSData *)dataWithReplayRecording:(SentryReplayRecording *)replayRecording; + + (SentryEnvelope *_Nullable)envelopeWithData:(NSData *)data; + (SentryAppState *_Nullable)appStateWithData:(NSData *)sessionData; diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h new file mode 100644 index 00000000000..953f11fdf81 --- /dev/null +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -0,0 +1,65 @@ +#import "SentryDefines.h" +#import + +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +# import + +@class SentryReplayOptions; +@class SentryEvent; +@class SentryCurrentDateProvider; +@class SentryDisplayLinkWrapper; +@class SentryVideoInfo; + +@protocol SentryRandom; +@protocol SentryRedactOptions; + +NS_ASSUME_NONNULL_BEGIN + +@protocol SentryReplayMaker + +- (void)addFrameWithImage:(UIImage *)image; +- (void)releaseFramesUntil:(NSDate *)date; +- (BOOL)createVideoWithDuration:(NSTimeInterval)duration + beginning:(NSDate *)beginning + outputFileURL:(NSURL *)outputFileURL + error:(NSError *_Nullable *_Nullable)error + completion: + (void (^)(SentryVideoInfo *_Nullable, NSError *_Nullable))completion; + +@end + +@protocol SentryViewScreenshotProvider +- (UIImage *)imageWithView:(UIView *)view options:(id)options; +@end + +API_AVAILABLE(ios(16.0), tvos(16.0)) +@interface SentrySessionReplay : NSObject + +- (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions + replayFolderPath:(NSURL *)folderPath + screenshotProvider:(id)photographer + replayMaker:(id)replayMaker + dateProvider:(SentryCurrentDateProvider *)dateProvider + random:(id)random + displayLinkWrapper:(SentryDisplayLinkWrapper *)displayLinkWrapper; + +/** + * Start recording the session using rootView as image source. + * If full is @c YES, we transmit the entire session to sentry. + */ +- (void)start:(UIView *)rootView fullSession:(BOOL)full; + +/** + * Stop recording the session replay + */ +- (void)stop; + +/** + * Captures a replay for given event. + */ +- (void)captureReplayForEvent:(SentryEvent *)event; + +@end + +NS_ASSUME_NONNULL_END +#endif // SENTRY_HAS_UIKIT diff --git a/Sources/Sentry/include/SentrySessionReplayIntegration.h b/Sources/Sentry/include/SentrySessionReplayIntegration.h new file mode 100644 index 00000000000..4500aeaa3d9 --- /dev/null +++ b/Sources/Sentry/include/SentrySessionReplayIntegration.h @@ -0,0 +1,12 @@ +#import "SentryBaseIntegration.h" +#import "SentryDefines.h" +#import "SentrySwift.h" +#import + +NS_ASSUME_NONNULL_BEGIN +#if SENTRY_HAS_UIKIT && !TARGET_OS_VISION +@interface SentrySessionReplayIntegration : SentryBaseIntegration + +@end +#endif // SENTRY_HAS_UIKIT && !TARGET_OS_VISION +NS_ASSUME_NONNULL_END diff --git a/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift new file mode 100644 index 00000000000..8c069b3c540 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryOnDemandReplay.swift @@ -0,0 +1,188 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) + +@_implementationOnly import _SentryPrivate +import AVFoundation +import CoreGraphics +import Foundation +import UIKit + +struct SentryReplayFrame { + let imagePath: String + let time: Date + + init(imagePath: String, time: Date) { + self.imagePath = imagePath + self.time = time + } +} + +enum SentryOnDemandReplayError: Error { + case cantReadVideoSize +} + +@available(iOS 16.0, tvOS 16.0, *) +@objcMembers +class SentryOnDemandReplay: NSObject { + private let _outputPath: String + private let _onDemandDispatchQueue: DispatchQueue + + private var _starttime = Date() + private var _frames = [SentryReplayFrame]() + private var _currentPixelBuffer: SentryPixelBuffer? + + var videoWidth = 200 + var videoHeight = 434 + + var bitRate = 20_000 + var frameRate = 1 + var cacheMaxSize = UInt.max + + init(outputPath: String) { + self._outputPath = outputPath + _onDemandDispatchQueue = DispatchQueue(label: "io.sentry.sessionreplay.ondemand") + } + + func addFrame(image: UIImage) { + _onDemandDispatchQueue.async { + self.asyncAddFrame(image: image) + } + } + + private func asyncAddFrame(image: UIImage) { + guard let data = resizeImage(image, maxWidth: 300)?.pngData() else { return } + + let date = Date() + let interval = date.timeIntervalSince(_starttime) + let imagePath = (_outputPath as NSString).appendingPathComponent("\(interval).png") + do { + try data.write(to: URL(fileURLWithPath: imagePath)) + } catch { + print("[SentryOnDemandReplay] Could not save replay frame. Error: \(error)") + return + } + _frames.append(SentryReplayFrame(imagePath: imagePath, time: date)) + + while _frames.count > cacheMaxSize { + let first = _frames.removeFirst() + try? FileManager.default.removeItem(at: URL(fileURLWithPath: first.imagePath)) + } + } + + private func resizeImage(_ originalImage: UIImage, maxWidth: CGFloat) -> UIImage? { + let originalSize = originalImage.size + let aspectRatio = originalSize.width / originalSize.height + + let newWidth = min(originalSize.width, maxWidth) + let newHeight = newWidth / aspectRatio + + let newSize = CGSize(width: newWidth, height: newHeight) + + UIGraphicsBeginImageContextWithOptions(newSize, false, 1) + originalImage.draw(in: CGRect(x: 0, y: 0, width: newSize.width, height: newSize.height)) + let resizedImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + + return resizedImage + } + + func releaseFramesUntil(_ date: Date) { + _onDemandDispatchQueue.async { + while let first = self._frames.first, first.time < date { + self._frames.removeFirst() + try? FileManager.default.removeItem(at: URL(fileURLWithPath: first.imagePath)) + } + } + } + + func createVideoWith(duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws { + let videoWriter = try AVAssetWriter(url: outputFileURL, fileType: .mov) + + let videoSettings = createVideoSettings() + + let videoWriterInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoSettings) + let bufferAttributes: [String: Any] = [ + String(kCVPixelBufferPixelFormatTypeKey): kCVPixelFormatType_32ARGB + ] + + let pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: videoWriterInput, sourcePixelBufferAttributes: bufferAttributes) + + videoWriter.add(videoWriterInput) + videoWriter.startWriting() + videoWriter.startSession(atSourceTime: .zero) + + var frameCount = 0 + let (frames, start, end) = filterFrames(beginning: beginning, end: beginning.addingTimeInterval(duration)) + + if frames.isEmpty { return } + + _currentPixelBuffer = SentryPixelBuffer(size: CGSize(width: videoWidth, height: videoHeight)) + + videoWriterInput.requestMediaDataWhenReady(on: _onDemandDispatchQueue) { [weak self] in + guard let self = self else { return } + + if frameCount < frames.count { + let imagePath = frames[frameCount] + + if let image = UIImage(contentsOfFile: imagePath) { + let presentTime = CMTime(seconds: Double(frameCount), preferredTimescale: CMTimeScale(self.frameRate)) + guard self._currentPixelBuffer?.append(image: image, pixelBufferAdapter: pixelBufferAdaptor, presentationTime: presentTime) == true else { + completion(nil, videoWriter.error) + videoWriterInput.markAsFinished() + return + } + } + frameCount += 1 + } else { + videoWriterInput.markAsFinished() + videoWriter.finishWriting { + var videoInfo: SentryVideoInfo? + if videoWriter.status == .completed { + do { + let fileAttributes = try FileManager.default.attributesOfItem(atPath: outputFileURL.path) + guard let fileSize = fileAttributes[FileAttributeKey.size] as? Int else { + completion(nil, SentryOnDemandReplayError.cantReadVideoSize) + return + } + videoInfo = SentryVideoInfo(path: outputFileURL, height: self.videoHeight, width: self.videoWidth, duration: TimeInterval(frames.count / self.frameRate), frameCount: frames.count, frameRate: self.frameRate, start: start, end: end, fileSize: fileSize) + } catch { + completion(nil, error) + } + } + completion(videoInfo, videoWriter.error) + } + } + } + } + + private func filterFrames(beginning: Date, end: Date) -> ([String], firstFrame: Date, lastFrame: Date) { + var frames = [String]() + + var start = Date() + var actualEnd = Date() + + for frame in _frames { + if frame.time < beginning { continue } else if frame.time > end { break } + if frame.time < start { start = frame.time } + + actualEnd = frame.time + frames.append(frame.imagePath) + } + return (frames, start, actualEnd) + } + + private func createVideoSettings() -> [String: Any] { + return [ + AVVideoCodecKey: AVVideoCodecType.h264, + AVVideoWidthKey: videoWidth, + AVVideoHeightKey: videoHeight, + AVVideoCompressionPropertiesKey: [ + AVVideoAverageBitRateKey: bitRate, + AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel + ] as [String: Any] + ] + } +} + +#endif // os(iOS) || os(tvOS) +#endif // canImport(UIKit) diff --git a/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift new file mode 100644 index 00000000000..264e2b5c056 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryPixelBuffer.swift @@ -0,0 +1,49 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) + +import AVFoundation +import CoreGraphics +import Foundation +import UIKit + +class SentryPixelBuffer { + private var pixelBuffer: CVPixelBuffer? + private let rgbColorSpace = CGColorSpaceCreateDeviceRGB() + private let size: CGSize + + init?(size: CGSize) { + self.size = size + let status = CVPixelBufferCreate(kCFAllocatorDefault, Int(size.width), Int(size.height), kCVPixelFormatType_32ARGB, nil, &pixelBuffer) + if status != kCVReturnSuccess { + return nil + } + } + + func append(image: UIImage, pixelBufferAdapter: AVAssetWriterInputPixelBufferAdaptor, presentationTime: CMTime) -> Bool { + guard let pixelBuffer = pixelBuffer else { return false } + + CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) + let pixelData = CVPixelBufferGetBaseAddress(pixelBuffer) + + guard + let cgimage = image.cgImage, + let context = CGContext( + data: pixelData, + width: Int(size.width), + height: Int(size.height), + bitsPerComponent: 8, + bytesPerRow: CVPixelBufferGetBytesPerRow(pixelBuffer), + space: rgbColorSpace, + bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue + ) else { + return false + } + + context.draw(cgimage, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + + return pixelBufferAdapter.append(pixelBuffer, withPresentationTime: presentationTime) + } +} +#endif // os(iOS) || os(tvOS) +#endif // canImport(UIKit) && !SENTRY_NO_UIKIT diff --git a/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift new file mode 100644 index 00000000000..ce0f3fcfb32 --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift @@ -0,0 +1,101 @@ +import Foundation + +@objcMembers +public class SentryReplayOptions: NSObject, SentryRedactOptions { + /** + * Indicates the percentage in which the replay for the session will be created. + * - Specifying @c 0 means never, @c 1.0 means always. + * - note: The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it + * to the default. + * - note: The default is 0. + */ + public var sessionSampleRate: Float + + /** + * Indicates the percentage in which a 30 seconds replay will be send with error events. + * - Specifying 0 means never, 1.0 means always. + * - note: The value needs to be >= 0.0 and \<= 1.0. When setting a value out of range the SDK sets it + * to the default. + * - note: The default is 0. + */ + public var errorSampleRate: Float + + /** + * Indicates whether session replay should redact all text in the app + * by drawing a black rectangle over it. + * + * - note: The default is true + */ + public var redactAllText = true + + /** + * Indicates whether session replay should redact all non-bundled image + * in the app by drawing a black rectangle over it. + * + * - note: The default is true + */ + public var redactAllImages = true + + /** + * Defines the quality of the session replay. + * Higher bit rates better quality, but also bigger files to transfer. + * @note The default value is @c 20000; + */ + let replayBitRate = 20_000 + + /** + * Number of frames per second of the replay. + * The more the havier the process is. + */ + let frameRate = 1 + + /** + * The scale related to the window size at which the replay will be created + */ + let sizeScale = 0.8 + + /** + * The maximum duration of replays for error events. + */ + let errorReplayDuration = TimeInterval(30) + + /** + * The maximum duration of the segment of a session replay. + */ + let sessionSegmentDuration = TimeInterval(5) + + /** + * The maximum duration of a replay session. + */ + let maximumDuration = TimeInterval(3_600) + + /** + * Inittialize session replay options disabled + */ + public override init() { + self.sessionSampleRate = 0 + self.errorSampleRate = 0 + } + + /** + * Initialize session replay options + * - parameters: + * - sessionSampleRate Indicates the percentage in which the replay for the session will be created. + * - errorSampleRate Indicates the percentage in which a 30 seconds replay will be send with + * error events. + */ + public init(sessionSampleRate: Float = 0, errorSampleRate: Float = 0, redactAllText: Bool = true, redactAllImages: Bool = true) { + self.sessionSampleRate = sessionSampleRate + self.errorSampleRate = errorSampleRate + self.redactAllText = redactAllText + self.redactAllImages = redactAllImages + } + + convenience init(dictionary: [String: Any]) { + let sessionSampleRate = (dictionary["sessionSampleRate"] as? NSNumber)?.floatValue ?? 0 + let onErrorSampleRate = (dictionary["errorSampleRate"] as? NSNumber)?.floatValue ?? 0 + let redactAllText = (dictionary["redactAllText"] as? NSNumber)?.boolValue ?? true + let redactAllImages = (dictionary["redactAllImages"] as? NSNumber)?.boolValue ?? true + self.init(sessionSampleRate: sessionSampleRate, errorSampleRate: onErrorSampleRate, redactAllText: redactAllText, redactAllImages: redactAllImages) + } +} diff --git a/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift new file mode 100644 index 00000000000..2d7518f9e7b --- /dev/null +++ b/Sources/Swift/Integrations/SessionReplay/SentryVideoInfo.swift @@ -0,0 +1,28 @@ +import Foundation + +@objcMembers +class SentryVideoInfo: NSObject { + + let path: URL + let height: Int + let width: Int + let duration: TimeInterval + let frameCount: Int + let frameRate: Int + let start: Date + let end: Date + let fileSize: Int + + init(path: URL, height: Int, width: Int, duration: TimeInterval, frameCount: Int, frameRate: Int, start: Date, end: Date, fileSize: Int) { + self.height = height + self.width = width + self.duration = duration + self.frameCount = frameCount + self.frameRate = frameRate + self.start = start + self.end = end + self.path = path + self.fileSize = fileSize + } + +} diff --git a/Sources/Swift/Protocol/SentryRedactOptions.swift b/Sources/Swift/Protocol/SentryRedactOptions.swift new file mode 100644 index 00000000000..cdd38e819a1 --- /dev/null +++ b/Sources/Swift/Protocol/SentryRedactOptions.swift @@ -0,0 +1,7 @@ +import Foundation + +@objc +protocol SentryRedactOptions { + var redactAllText: Bool { get } + var redactAllImages: Bool { get } +} diff --git a/Sources/Swift/SentryExperimentalOptions.swift b/Sources/Swift/SentryExperimentalOptions.swift new file mode 100644 index 00000000000..9cf1a1947fd --- /dev/null +++ b/Sources/Swift/SentryExperimentalOptions.swift @@ -0,0 +1,18 @@ +@objcMembers +public class SentryExperimentalOptions: NSObject { + #if canImport(UIKit) + /** + * Settings to configure the session replay. + */ + public var sessionReplay = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 0) + #endif + + func validateOptions(_ options: [String: Any]?) { + #if canImport(UIKit) + if let sessionReplayOptions = options?["sessionReplay"] as? [String: Any] { + sessionReplay = SentryReplayOptions(dictionary: sessionReplayOptions) + } + #endif + } + +} diff --git a/Sources/Swift/Tools/SentryViewPhotographer.swift b/Sources/Swift/Tools/SentryViewPhotographer.swift new file mode 100644 index 00000000000..36667e4dec2 --- /dev/null +++ b/Sources/Swift/Tools/SentryViewPhotographer.swift @@ -0,0 +1,115 @@ +#if canImport(UIKit) && !SENTRY_NO_UIKIT +#if os(iOS) || os(tvOS) + +@_implementationOnly import _SentryPrivate +import CoreGraphics +import Foundation +import UIKit + +@available(iOS, introduced: 16.0) +@available(tvOS, introduced: 16.0) +@objcMembers +class SentryViewPhotographer: NSObject { + + //This is a list of UIView subclasses that will be ignored during redact process + private var ignoreClasses: [AnyClass] = [] + //This is a list of UIView subclasses that need to be redacted from screenshot + private var redactClasses: [AnyClass] = [] + + static let shared = SentryViewPhotographer() + + override init() { +#if os(iOS) + ignoreClasses = [ UISlider.self, UISwitch.self ] +#endif // os(iOS) + redactClasses = [ UILabel.self, UITextView.self, UITextField.self ] + [ + "_TtCOCV7SwiftUI11DisplayList11ViewUpdater8Platform13CGDrawingView", + "_TtC7SwiftUIP33_A34643117F00277B93DEBAB70EC0697122_UIShapeHitTestingView", + "SwiftUI._UIGraphicsView", "SwiftUI.ImageLayer" + ].compactMap { NSClassFromString($0) } + } + + @objc(imageWithView:options:) + func image(view: UIView, options: SentryRedactOptions) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(view.bounds.size, true, 0) + + defer { + UIGraphicsEndImageContext() + } + + guard let currentContext = UIGraphicsGetCurrentContext() else { return nil } + + view.layer.render(in: currentContext) + self.mask(view: view, context: currentContext, options: options) + + guard let screenshot = UIGraphicsGetImageFromCurrentImageContext() else { return nil } + return screenshot + } + + private func mask(view: UIView, context: CGContext, options: SentryRedactOptions?) { + UIColor.black.setFill() + let maskPath = self.buildPath(view: view, + path: CGMutablePath(), + area: view.frame, + redactText: options?.redactAllText ?? true, + redactImage: options?.redactAllImages ?? true) + context.addPath(maskPath) + context.fillPath() + } + + private func shouldIgnore(view: UIView) -> Bool { + ignoreClasses.contains { view.isKind(of: $0) } + } + + private func shouldRedact(view: UIView) -> Bool { + return redactClasses.contains { view.isKind(of: $0) } + } + + private func shouldRedact(imageView: UIImageView) -> Bool { + // Checking the size is to avoid redact gradient backgroud that + // are usually small lines repeating + guard let image = imageView.image, image.size.width > 10 && image.size.height > 10 else { return false } + return image.imageAsset?.value(forKey: "_containingBundle") == nil + } + + private func buildPath(view: UIView, path: CGMutablePath, area: CGRect, redactText: Bool, redactImage: Bool) -> CGMutablePath { + let rectInWindow = view.convert(view.bounds, to: nil) + + if (!redactImage && !redactText) || !area.intersects(rectInWindow) || view.isHidden || view.alpha == 0 { + return path + } + + var result = path + + let ignore = shouldIgnore(view: view) + + let redact: Bool = { + if redactImage, let imageView = view as? UIImageView { + return shouldRedact(imageView: imageView) + } + return redactText && shouldRedact(view: view) + }() + + if !ignore && redact { + result.addRect(rectInWindow) + return result + } else if isOpaqueOrHasBackground(view) { + result = SentryCoreGraphicsHelper.excludeRect(rectInWindow, from: result).takeRetainedValue() + } + + if !ignore { + for subview in view.subviews { + result = buildPath(view: subview, path: path, area: area, redactText: redactText, redactImage: redactImage) + } + } + + return result + } + + private func isOpaqueOrHasBackground(_ view: UIView) -> Bool { + return view.isOpaque || (view.backgroundColor != nil && (view.backgroundColor?.cgColor.alpha ?? 0) > 0.9) + } +} + +#endif // os(iOS) || os(tvOS) +#endif // canImport(UIKit) && !SENTRY_NO_UIKIT diff --git a/Tests/SentryTests/Helper/SentryDateUtilTests.swift b/Tests/SentryTests/Helper/SentryDateUtilTests.swift index ebb161c9f31..17898e59432 100644 --- a/Tests/SentryTests/Helper/SentryDateUtilTests.swift +++ b/Tests/SentryTests/Helper/SentryDateUtilTests.swift @@ -1,3 +1,4 @@ +import Nimble import SentryTestUtils import XCTest @@ -55,4 +56,11 @@ class SentryDateUtilTests: XCTestCase { XCTAssertNil(SentryDateUtil.getMaximumDate(nil, andOther: nil)) } + func testJavascriptDate() { + let testDate = Date(timeIntervalSince1970: 60) + let timestamp = SentryDateUtil.millisecondsSince1970(testDate) + + expect(timestamp) == 60_000 + } + } diff --git a/Tests/SentryTests/Helper/SentrySerializationTests.swift b/Tests/SentryTests/Helper/SentrySerializationTests.swift index 26f277c6639..a848b898773 100644 --- a/Tests/SentryTests/Helper/SentrySerializationTests.swift +++ b/Tests/SentryTests/Helper/SentrySerializationTests.swift @@ -1,3 +1,4 @@ +import Nimble import XCTest class SentrySerializationTests: XCTestCase { @@ -230,6 +231,22 @@ class SentrySerializationTests: XCTestCase { XCTAssertNil(SentrySerialization.session(with: data)) } + func testSerializeReplayRecording() { + class MockReplayRecording: SentryReplayRecording { + override func serialize() -> [[String: Any]] { + return [["KEY": "VALUE"]] + } + } + + let date = Date(timeIntervalSince1970: 2) + let recording = MockReplayRecording(segmentId: 5, size: 5_000, start: date, duration: 5_000, frameCount: 5, frameRate: 1, height: 320, width: 950) + let data = SentrySerialization.data(with: recording) + + let serialized = String(data: data, encoding: .utf8) + + expect(serialized) == "{\"segment_id\":5}\n[{\"KEY\":\"VALUE\"}]" + } + func testLevelFromEventData() { let envelopeItem = SentryEnvelopeItem(event: TestData.event) diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift new file mode 100644 index 00000000000..00dc0162ab8 --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayEventTests.swift @@ -0,0 +1,30 @@ +import Foundation +import Nimble +import XCTest + +class SentryReplayEventTests: XCTestCase { + + func test_Serialize() { + let sut = SentryReplayEvent() + sut.urls = ["Screen 1", "Screen 2"] + sut.replayStartTimestamp = Date(timeIntervalSince1970: 1) + + let traceIds = [SentryId(), SentryId()] + sut.traceIds = traceIds + + let replayId = SentryId() + sut.eventId = replayId + + sut.segmentId = 3 + + let result = sut.serialize() + + expect(result["urls"] as? [String]) == ["Screen 1", "Screen 2"] + expect(result["replay_start_timestamp"] as? Int) == 1 + expect(result["trace_ids"] as? [String]) == [ traceIds[0].sentryIdString, traceIds[1].sentryIdString] + expect(result["replay_id"] as? String) == replayId.sentryIdString + expect(result["segment_id"] as? Int) == 3 + expect(result["replay_type"] as? String) == "buffer" + } + +} diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentryReplayRecordingTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayRecordingTests.swift new file mode 100644 index 00000000000..3d8f01c3da3 --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentryReplayRecordingTests.swift @@ -0,0 +1,41 @@ +import Foundation +import Nimble +import XCTest + +class SentryReplayRecordingTests: XCTestCase { + + func test_serialize() { + let sut = SentryReplayRecording(segmentId: 3, size: 200, start: Date(timeIntervalSince1970: 2), duration: 5_000, frameCount: 5, frameRate: 1, height: 930, width: 390) + + let data = sut.serialize() + + let metaInfo = data[0] + let metaInfoData = metaInfo["data"] as? [String: Any] + + let recordingInfo = data[1] + let recordingData = recordingInfo["data"] as? [String: Any] + let recordingPayload = recordingData?["payload"] as? [String: Any] + + expect(metaInfo["type"] as? Int) == 4 + expect(metaInfo["timestamp"] as? Int) == 2_000 + expect(metaInfoData?["href"] as? String) == "" + expect(metaInfoData?["height"] as? Int) == 930 + expect(metaInfoData?["width"] as? Int) == 390 + + expect(recordingInfo["type"] as? Int) == 5 + expect(recordingInfo["timestamp"] as? Int) == 2_000 + expect(recordingData?["tag"] as? String) == "video" + expect(recordingPayload?["segmentId"] as? Int) == 3 + expect(recordingPayload?["size"] as? Int) == 200 + expect(recordingPayload?["duration"] as? Int) == 5_000 + expect(recordingPayload?["encoding"] as? String) == "h264" + expect(recordingPayload?["container"] as? String) == "mp4" + expect(recordingPayload?["height"] as? Int) == 930 + expect(recordingPayload?["width"] as? Int) == 390 + expect(recordingPayload?["frameCount"] as? Int) == 5 + expect(recordingPayload?["frameRateType"] as? String) == "constant" + expect(recordingPayload?["frameRate"] as? Int) == 1 + expect(recordingPayload?["left"] as? Int) == 0 + expect(recordingPayload?["top"] as? Int) == 0 + } +} diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift new file mode 100644 index 00000000000..57af570ad3f --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayIntegrationTests.swift @@ -0,0 +1,73 @@ +import Foundation +import Nimble +@testable import Sentry +import SentryTestUtils +import XCTest + +#if os(iOS) || os(tvOS) + +class SentrySessionReplayIntegrationTests: XCTestCase { + + override func setUpWithError() throws { + guard #available(iOS 16.0, tvOS 16.0, *) else { + throw XCTSkip("iOS version not supported") + } + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + func startSDK(sessionSampleRate: Float, errorSampleRate: Float) { + if #available(iOS 16.0, tvOS 16.0, *) { + SentrySDK.start { + $0.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: sessionSampleRate, errorSampleRate: errorSampleRate) + $0.setIntegrations([SentrySessionReplayIntegration.self]) + } + } + } + + func testNoInstall() { + startSDK(sessionSampleRate: 0, errorSampleRate: 0) + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 0 + expect(SentryGlobalEventProcessor.shared().processors.count) == 0 + } + + func testInstallFullSessionReplay() { + startSDK(sessionSampleRate: 1, errorSampleRate: 0) + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 + expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + } + + func testNoInstallFullSessionReplayBecauseOfRandom() { + + SentryDependencyContainer.sharedInstance().random = TestRandom(value: 0.3) + + startSDK(sessionSampleRate: 0.2, errorSampleRate: 0) + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 0 + expect(SentryGlobalEventProcessor.shared().processors.count) == 0 + } + + func testInstallFullSessionReplayBecauseOfRandom() { + + SentryDependencyContainer.sharedInstance().random = TestRandom(value: 0.1) + + startSDK(sessionSampleRate: 0.2, errorSampleRate: 0) + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 + expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + } + + func testInstallErrorReplay() { + startSDK(sessionSampleRate: 0, errorSampleRate: 0.1) + + expect(SentrySDK.currentHub().trimmedInstalledIntegrationNames().count) == 1 + expect(SentryGlobalEventProcessor.shared().processors.count) == 1 + } +} + +#endif diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift new file mode 100644 index 00000000000..1925b4eb2e9 --- /dev/null +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -0,0 +1,210 @@ +import Foundation +import Nimble +@testable import Sentry +import SentryTestUtils +import XCTest + +#if os(iOS) || os(tvOS) +class SentrySessionReplayTests: XCTestCase { + + private class ScreenshotProvider: NSObject, SentryViewScreenshotProvider { + func image(with view: UIView, options: SentryRedactOptions) -> UIImage { UIImage.add } + } + + private class TestReplayMaker: NSObject, SentryReplayMaker { + struct CreateVideoCall { + var duration: TimeInterval + var beginning: Date + var outputFileURL: URL + var completion: ((Sentry.SentryVideoInfo?, Error?) -> Void) + } + + var lastCallToCreateVideo: CreateVideoCall? + func createVideo(withDuration duration: TimeInterval, beginning: Date, outputFileURL: URL, completion: @escaping (SentryVideoInfo?, Error?) -> Void) throws { + lastCallToCreateVideo = CreateVideoCall(duration: duration, + beginning: beginning, + outputFileURL: outputFileURL, + completion: completion) + + try? "Video Data".write(to: outputFileURL, atomically: true, encoding: .utf8) + + let videoInfo = SentryVideoInfo(path: outputFileURL, height: 1_024, width: 480, duration: duration, frameCount: 5, frameRate: 1, start: beginning, end: beginning.addingTimeInterval(duration), fileSize: 10) + + completion(videoInfo, nil) + } + + var lastFrame: UIImage? + func addFrame(with image: UIImage) { + lastFrame = image + } + + var lastReleaseUntil: Date? + func releaseFrames(until date: Date) { + lastReleaseUntil = date + } + } + + private class ReplayHub: SentryHub { + var lastEvent: SentryReplayEvent? + var lastRecording: SentryReplayRecording? + var lastVideo: URL? + + override func capture(_ replayEvent: SentryReplayEvent, replayRecording: SentryReplayRecording, video videoURL: URL) { + lastEvent = replayEvent + lastRecording = replayRecording + lastVideo = videoURL + } + } + + @available(iOS 16.0, tvOS 16.0, *) + private class Fixture { + let dateProvider = TestCurrentDateProvider() + let random = TestRandom(value: 0) + let screenshotProvider = ScreenshotProvider() + let displayLink = TestDisplayLinkWrapper() + let rootView = UIView() + let hub = ReplayHub(client: nil, andScope: nil) + let replayMaker = TestReplayMaker() + let cacheFolder = FileManager.default.temporaryDirectory + + func getSut(options: SentryReplayOptions = .init(sessionSampleRate: 0, errorSampleRate: 0) ) -> SentrySessionReplay { + return SentrySessionReplay(settings: options, + replayFolderPath: cacheFolder, + screenshotProvider: screenshotProvider, + replayMaker: replayMaker, + dateProvider: dateProvider, + random: random, + displayLinkWrapper: displayLink) + } + } + + override func setUpWithError() throws { + guard #available(iOS 16.0, tvOS 16.0, *) else { + throw XCTSkip("iOS version not supported") + } + } + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + clearTestState() + } + + @available(iOS 16.0, tvOS 16, *) + private func startFixture() -> Fixture { + let fixture = Fixture() + SentrySDK.setCurrentHub(fixture.hub) + return fixture + } + + @available(iOS 16.0, tvOS 16, *) + func testDontSentReplay_NoFullSession() { + let fixture = startFixture() + let sut = fixture.getSut() + sut.start(fixture.rootView, fullSession: false) + + fixture.dateProvider.advance(by: 1) + Dynamic(sut).newFrame(nil) + fixture.dateProvider.advance(by: 5) + Dynamic(sut).newFrame(nil) + + expect(fixture.hub.lastEvent) == nil + } + + @available(iOS 16.0, tvOS 16, *) + func testSentReplay_FullSession() { + let fixture = startFixture() + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + sut.start(fixture.rootView, fullSession: true) + + fixture.dateProvider.advance(by: 1) + + let start = fixture.dateProvider.date() + + Dynamic(sut).newFrame(nil) + fixture.dateProvider.advance(by: 5) + Dynamic(sut).newFrame(nil) + + guard let videoArguments = fixture.replayMaker.lastCallToCreateVideo else { + fail("Replay maker create video was not called") + return + } + + expect(videoArguments.duration) == 5 + expect(videoArguments.beginning) == start + expect(videoArguments.outputFileURL) == fixture.cacheFolder.appendingPathComponent("segments/0.mp4") + + expect(fixture.hub.lastRecording) != nil + expect(fixture.hub.lastVideo) == videoArguments.outputFileURL + assertFullSession(sut, expected: true) + } + + @available(iOS 16.0, tvOS 16, *) + func testDontSentReplay_NotFullSession() { + let fixture = startFixture() + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + sut.start(fixture.rootView, fullSession: false) + + fixture.dateProvider.advance(by: 1) + + Dynamic(sut).newFrame(nil) + fixture.dateProvider.advance(by: 5) + Dynamic(sut).newFrame(nil) + + let videoArguments = fixture.replayMaker.lastCallToCreateVideo + + expect(videoArguments) == nil + assertFullSession(sut, expected: false) + } + + @available(iOS 16.0, tvOS 16, *) + func testChangeReplayMode_forErrorEvent() { + let fixture = startFixture() + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + sut.start(fixture.rootView, fullSession: false) + + let event = Event(error: NSError(domain: "Some error", code: 1)) + + sut.capture(for: event) + assertFullSession(sut, expected: true) + } + + @available(iOS 16.0, tvOS 16, *) + func testDontChangeReplayMode_forNonErrorEvent() { + let fixture = startFixture() + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + sut.start(fixture.rootView, fullSession: false) + + let event = Event(level: .info) + + sut.capture(for: event) + + assertFullSession(sut, expected: false) + } + + @available(iOS 16.0, tvOS 16, *) + func testSessionReplayMaximumDuration() { + let fixture = startFixture() + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) + sut.start(fixture.rootView, fullSession: true) + + Dynamic(sut).newFrame(nil) + fixture.dateProvider.advance(by: 5) + Dynamic(sut).newFrame(nil) + expect(Dynamic(sut).isRunning) == true + fixture.dateProvider.advance(by: 3_600) + Dynamic(sut).newFrame(nil) + + expect(Dynamic(sut).isRunning) == false + } + + @available(iOS 16.0, tvOS 16, *) + func assertFullSession(_ sessionReplay: SentrySessionReplay, expected: Bool) { + expect(Dynamic(sessionReplay).isFullSession) == expected + } +} + +#endif diff --git a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift index 35b05344e2f..0d6175ef157 100644 --- a/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift +++ b/Tests/SentryTests/Networking/SentryDataCategoryMapperTests.swift @@ -11,6 +11,7 @@ class SentryDataCategoryMapperTests: XCTestCase { expect(sentryDataCategoryForEnvelopItemType("attachment")) == .attachment expect(sentryDataCategoryForEnvelopItemType("profile")) == .profile expect(sentryDataCategoryForEnvelopItemType("statsd")) == .metricBucket + expect(sentryDataCategoryForEnvelopItemType("replay_video")) == .replay expect(sentryDataCategoryForEnvelopItemType("unknown item type")) == .default } @@ -24,7 +25,8 @@ class SentryDataCategoryMapperTests: XCTestCase { expect(sentryDataCategoryForNSUInteger(6)) == .userFeedback expect(sentryDataCategoryForNSUInteger(7)) == .profile expect(sentryDataCategoryForNSUInteger(8)) == .metricBucket - expect(sentryDataCategoryForNSUInteger(9)) == .unknown + expect(sentryDataCategoryForNSUInteger(9)) == .replay + expect(sentryDataCategoryForNSUInteger(10)) == .unknown XCTAssertEqual(.unknown, sentryDataCategoryForNSUInteger(10), "Failed to map unknown category number to case .unknown") } @@ -39,6 +41,7 @@ class SentryDataCategoryMapperTests: XCTestCase { expect(sentryDataCategoryForString(kSentryDataCategoryNameUserFeedback)) == .userFeedback expect(sentryDataCategoryForString(kSentryDataCategoryNameProfile)) == .profile expect(sentryDataCategoryForString(kSentryDataCategoryNameMetricBucket)) == .metricBucket + expect(sentryDataCategoryForString(kSentryDataCategoryNameReplay)) == .replay expect(sentryDataCategoryForString(kSentryDataCategoryNameUnknown)) == .unknown XCTAssertEqual(.unknown, sentryDataCategoryForString("gdfagdfsa"), "Failed to map unknown category name to case .unknown") @@ -54,6 +57,7 @@ class SentryDataCategoryMapperTests: XCTestCase { expect(nameForSentryDataCategory(.userFeedback)) == kSentryDataCategoryNameUserFeedback expect(nameForSentryDataCategory(.profile)) == kSentryDataCategoryNameProfile expect(nameForSentryDataCategory(.metricBucket)) == kSentryDataCategoryNameMetricBucket + expect(nameForSentryDataCategory(.replay)) == kSentryDataCategoryNameReplay expect(nameForSentryDataCategory(.unknown)) == kSentryDataCategoryNameUnknown } } diff --git a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift index d5ff4eb7ddb..49f771008ba 100644 --- a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift +++ b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift @@ -237,6 +237,12 @@ class SentryEnvelopeTests: XCTestCase { XCTAssertEqual(attachment.contentType, envelopeItem.header.contentType) } + func testEmptyHeader() { + let sut = SentryEnvelopeHeader.empty() + expect(sut.eventId) == nil + expect(sut.traceContext) == nil + } + func testInitWithFileAttachment() { writeDataToFile(data: fixture.data ?? Data()) diff --git a/Tests/SentryTests/SentryClientTests.swift b/Tests/SentryTests/SentryClientTests.swift index ca9dfdc8a77..14e83c0c935 100644 --- a/Tests/SentryTests/SentryClientTests.swift +++ b/Tests/SentryTests/SentryClientTests.swift @@ -1587,6 +1587,90 @@ class SentryClientTest: XCTestCase { } } + func testCaptureReplayEvent() { + let sut = fixture.getSut() + let replayEvent = SentryReplayEvent() + replayEvent.segmentId = 2 + let replayRecording = SentryReplayRecording() + replayRecording.segmentId = 2 + + //Not a video url, but its ok for test the envelope + let movieUrl = Bundle(for: self.classForCoder).url(forResource: "Resources/raw", withExtension: "json") + + sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!, with: Scope()) + let envelope = fixture.transport.sentEnvelopes.first + expect(envelope?.items[0].header.type) == SentryEnvelopeItemTypeReplayVideo + } + + func testCaptureReplayEvent_WrongEventFromEventProcessor() { + let sut = fixture.getSut() + sut.options.beforeSend = { _ in + return Event() + } + + let replayEvent = SentryReplayEvent() + let replayRecording = SentryReplayRecording() + + let movieUrl = Bundle(for: self.classForCoder).url(forResource: "Resources/raw", withExtension: "json") + sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!, with: Scope()) + + //Nothing should be captured because beforeSend returned a non ReplayEvent + expect(self.fixture.transport.sentEnvelopes.count) == 0 + } + + func testCaptureReplayEvent_DontCaptureNilEvent() { + let sut = fixture.getSut() + sut.options.beforeSend = { _ in + return nil + } + + let replayEvent = SentryReplayEvent() + let replayRecording = SentryReplayRecording() + + let movieUrl = Bundle(for: self.classForCoder).url(forResource: "Resources/raw", withExtension: "json") + sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!, with: Scope()) + + //Nothing should be captured because beforeSend returned nil + expect(self.fixture.transport.sentEnvelopes.count) == 0 + } + + func testCaptureReplayEvent_InvalidFile() { + let sut = fixture.getSut() + sut.options.beforeSend = { _ in + return nil + } + + let replayEvent = SentryReplayEvent() + let replayRecording = SentryReplayRecording() + + let movieUrl = URL(string: "NoFile")! + sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl, with: Scope()) + + //Nothing should be captured because beforeSend returned nil + expect(self.fixture.transport.sentEnvelopes.count) == 0 + } + + func testCaptureReplayEvent_noBradcrumbsThreadsDebugMeta() { + let sut = fixture.getSut() + let replayEvent = SentryReplayEvent() + replayEvent.segmentId = 2 + let replayRecording = SentryReplayRecording() + replayRecording.segmentId = 2 + + //Not a video url, but its ok for test the envelope + let movieUrl = Bundle(for: self.classForCoder).url(forResource: "Resources/raw", withExtension: "json") + + let scope = Scope() + scope.addBreadcrumb(Breadcrumb(level: .debug, category: "Test Breadcrumb")) + + sut.capture(replayEvent, replayRecording: replayRecording, video: movieUrl!, with: scope) + + expect(replayEvent.breadcrumbs) == nil + expect(replayEvent.threads) == nil + expect(replayEvent.debugMeta) == nil + + } + private func givenEventWithDebugMeta() -> Event { let event = Event(level: SentryLevel.fatal) let debugMeta = DebugMeta() diff --git a/Tests/SentryTests/SentryHubTests.swift b/Tests/SentryTests/SentryHubTests.swift index cea9a3eb3c9..18cf26b0931 100644 --- a/Tests/SentryTests/SentryHubTests.swift +++ b/Tests/SentryTests/SentryHubTests.swift @@ -744,6 +744,34 @@ class SentryHubTests: XCTestCase { assertNoEnvelopesCaptured() } + func testCaptureReplay() { + class SentryClientMockReplay: SentryClient { + var replayEvent: SentryReplayEvent? + var replayRecording: SentryReplayRecording? + var videoUrl: URL? + var scope: Scope? + override func capture(_ replayEvent: SentryReplayEvent, replayRecording: SentryReplayRecording, video videoURL: URL, with scope: Scope) { + self.replayEvent = replayEvent + self.replayRecording = replayRecording + self.videoUrl = videoURL + self.scope = scope + } + } + let mockClient = SentryClientMockReplay(options: fixture.options) + + let replayEvent = SentryReplayEvent() + let replayRecording = SentryReplayRecording() + let videoUrl = URL(string: "https://sentry.io")! + + sut.bindClient(mockClient) + sut.capture(replayEvent, replayRecording: replayRecording, video: videoUrl) + + expect(mockClient?.replayEvent) == replayEvent + expect(mockClient?.replayRecording) == replayRecording + expect(mockClient?.videoUrl) == videoUrl + expect(mockClient?.scope) == sut.scope + } + func testCaptureEnvelope_WithSession() { let envelope = SentryEnvelope(session: SentrySession(releaseName: "", distinctId: "")) sut.capture(envelope) diff --git a/Tests/SentryTests/SentryMsgPackSerializerTests.m b/Tests/SentryTests/SentryMsgPackSerializerTests.m new file mode 100644 index 00000000000..6606a1e121f --- /dev/null +++ b/Tests/SentryTests/SentryMsgPackSerializerTests.m @@ -0,0 +1,103 @@ +#import "SentryMsgPackSerializer.h" +#import +#import + +@interface SentryMsgPackSerializerTests : XCTestCase + +@end + +@implementation SentryMsgPackSerializerTests + +- (void)testSerializeNSData +{ + NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; + NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; + + NSDictionary> *dictionary = @{ + @"key1" : [@"Data 1" dataUsingEncoding:NSUTF8StringEncoding], + @"key2" : [@"Data 2" dataUsingEncoding:NSUTF8StringEncoding] + }; + + BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary + intoFile:tempFileURL]; + XCTAssertTrue(result); + NSData *tempFile = [NSData dataWithContentsOfURL:tempFileURL]; + [self assertMsgPack:tempFile]; + + [[NSFileManager defaultManager] removeItemAtURL:tempFileURL error:nil]; +} + +- (void)testSerializeURL +{ + NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; + NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; + NSURL *file1URL = [tempDirectoryURL URLByAppendingPathComponent:@"file1.dat"]; + NSURL *file2URL = [tempDirectoryURL URLByAppendingPathComponent:@"file2.dat"]; + + [@"File 1" writeToURL:file1URL atomically:YES encoding:NSUTF8StringEncoding error:nil]; + [@"File 2" writeToURL:file2URL atomically:YES encoding:NSUTF8StringEncoding error:nil]; + + NSDictionary> *dictionary = + @{ @"key1" : file1URL, @"key2" : file2URL }; + + BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary + intoFile:tempFileURL]; + XCTAssertTrue(result); + NSData *tempFile = [NSData dataWithContentsOfURL:tempFileURL]; + + [self assertMsgPack:tempFile]; + + [[NSFileManager defaultManager] removeItemAtURL:tempFileURL error:nil]; + [[NSFileManager defaultManager] removeItemAtURL:file1URL error:nil]; + [[NSFileManager defaultManager] removeItemAtURL:file2URL error:nil]; +} + +- (void)testSerializeInvalidFile +{ + NSURL *tempDirectoryURL = [NSURL fileURLWithPath:NSTemporaryDirectory()]; + NSURL *tempFileURL = [tempDirectoryURL URLByAppendingPathComponent:@"test.dat"]; + NSURL *file1URL = [tempDirectoryURL URLByAppendingPathComponent:@"notAFile.dat"]; + + NSDictionary> *dictionary = @{ @"key1" : file1URL }; + + BOOL result = [SentryMsgPackSerializer serializeDictionaryToMessagePack:dictionary + intoFile:tempFileURL]; + XCTAssertFalse(result); +} + +- (void)assertMsgPack:(NSData *)data +{ + NSInputStream *stream = [NSInputStream inputStreamWithData:data]; + [stream open]; + + uint8_t buffer[1024]; + [stream read:buffer maxLength:1]; + + XCTAssertEqual(buffer[0] & 0x80, 0x80); // Assert data is a dictionary + + uint8_t dicSize = buffer[0] & 0x0F; // Gets dictionary length + + for (int i = 0; i < dicSize; i++) { // for each item in the dictionary + [stream read:buffer maxLength:1]; + XCTAssertEqual(buffer[0], (uint8_t)0xD9); // Asserts key is a string of up to 255 + // characteres + [stream read:buffer maxLength:1]; + uint8_t stringLen = buffer[0]; // Gets string length + NSInteger read = [stream read:buffer maxLength:stringLen]; // read the key from the buffer + buffer[read] = 0; // append a null terminator to the string + NSString *key = [NSString stringWithCString:(char *)buffer encoding:NSUTF8StringEncoding]; + XCTAssertEqual(key.length, stringLen); + + [stream read:buffer maxLength:1]; + XCTAssertEqual(buffer[0], (uint8_t)0xC6); + [stream read:buffer maxLength:sizeof(uint32_t)]; + uint32_t dataLen = NSSwapBigIntToHost(*(uint32_t *)buffer); + [stream read:buffer maxLength:dataLen]; + } + + // We should be at the end of the data by now and nothing left to read + NSInteger IsEndOfFile = [stream read:buffer maxLength:1]; + XCTAssertEqual(IsEndOfFile, 0); +} + +@end diff --git a/Tests/SentryTests/SentryOptionsTest.m b/Tests/SentryTests/SentryOptionsTest.m index 693a84ad9c8..8e345e638ff 100644 --- a/Tests/SentryTests/SentryOptionsTest.m +++ b/Tests/SentryTests/SentryOptionsTest.m @@ -544,6 +544,7 @@ - (void)testNSNull_SetsDefaultValue #if SENTRY_HAS_UIKIT @"enableUIViewControllerTracing" : [NSNull null], @"attachScreenshot" : [NSNull null], + @"sessionReplayOptions" : [NSNull null], #endif @"enableAppHangTracking" : [NSNull null], @"appHangTimeoutInterval" : [NSNull null], @@ -604,6 +605,8 @@ - (void)assertDefaultValues:(SentryOptions *)options XCTAssertEqual(options.enableUserInteractionTracing, YES); XCTAssertEqual(options.enablePreWarmedAppStartTracing, NO); XCTAssertEqual(options.attachViewHierarchy, NO); + XCTAssertEqual(options.experimental.sessionReplay.errorSampleRate, 0); + XCTAssertEqual(options.experimental.sessionReplay.sessionSampleRate, 0); #endif XCTAssertFalse(options.enableTracing); XCTAssertTrue(options.enableAppHangTracking); @@ -781,6 +784,27 @@ - (void)testEnablePreWarmedAppStartTracking [self testBooleanField:@"enablePreWarmedAppStartTracing" defaultValue:NO]; } +- (void)testSessionReplaySettingsInit +{ + if (@available(iOS 16.0, tvOS 16.0, *)) { + SentryOptions *options = [self getValidOptions:@{ + @"experimental" : + @ { @"sessionReplay" : @ { @"sessionSampleRate" : @2, @"errorSampleRate" : @4 } } + }]; + XCTAssertEqual(options.experimental.sessionReplay.sessionSampleRate, 2); + XCTAssertEqual(options.experimental.sessionReplay.errorSampleRate, 4); + } +} + +- (void)testSessionReplaySettingsDefaults +{ + if (@available(iOS 16.0, tvOS 16.0, *)) { + SentryOptions *options = [self getValidOptions:@{ @"sessionReplayOptions" : @ {} }]; + XCTAssertEqual(options.experimental.sessionReplay.sessionSampleRate, 0); + XCTAssertEqual(options.experimental.sessionReplay.errorSampleRate, 0); + } +} + #endif #if SENTRY_HAS_METRIC_KIT diff --git a/Tests/SentryTests/SentryTests-Bridging-Header.h b/Tests/SentryTests/SentryTests-Bridging-Header.h index 2df351f5a29..dd54ed6b6b0 100644 --- a/Tests/SentryTests/SentryTests-Bridging-Header.h +++ b/Tests/SentryTests/SentryTests-Bridging-Header.h @@ -12,6 +12,8 @@ #if SENTRY_HAS_UIKIT # import "MockUIScene.h" # import "SentryFramesTracker+TestInit.h" +# import "SentrySessionReplay.h" +# import "SentrySessionReplayIntegration.h" # import "SentryUIApplication+Private.h" # import "SentryUIApplication.h" # import "SentryUIDeviceWrapper.h" @@ -36,6 +38,7 @@ #import "NSMutableDictionary+Sentry.h" #import "NSURLProtocolSwizzle.h" #import "PrivateSentrySDKOnly.h" +#import "Sentry/Sentry-Swift.h" #import "SentryANRTracker.h" #import "SentryANRTrackingIntegration.h" #import "SentryAppStartMeasurement.h" @@ -59,6 +62,7 @@ #import "SentryCoreDataSwizzling.h" #import "SentryCoreDataTracker+Test.h" #import "SentryCoreDataTrackingIntegration.h" +#import "SentryCrashBinaryImageCache.h" #import "SentryCrashBinaryImageProvider.h" #import "SentryCrashC.h" #import "SentryCrashDebug.h" @@ -94,13 +98,17 @@ #import "SentryDiscardReason.h" #import "SentryDiscardReasonMapper.h" #import "SentryDiscardedEvent.h" +#import "SentryDispatchFactory.h" #import "SentryDispatchQueueWrapper.h" +#import "SentryDispatchSourceWrapper.h" #import "SentryDisplayLinkWrapper.h" #import "SentryDsn.h" #import "SentryEnvelope+Private.h" +#import "SentryEnvelopeAttachmentHeader.h" #import "SentryEnvelopeItemType.h" #import "SentryEnvelopeRateLimit.h" #import "SentryEvent+Private.h" +#import "SentryExtraContextProvider.h" #import "SentryFileContents.h" #import "SentryFileIOTrackingIntegration.h" #import "SentryFileManager+Test.h" @@ -125,10 +133,12 @@ #import "SentryLog+TestInit.h" #import "SentryLog.h" #import "SentryLogOutput.h" +#import "SentryMeasurementValue.h" #import "SentryMechanism.h" #import "SentryMechanismMeta.h" #import "SentryMeta.h" #import "SentryMigrateSessionInit.h" +#import "SentryMsgPackSerializer.h" #import "SentryNSDataTracker.h" #import "SentryNSDataUtils.h" #import "SentryNSError.h" @@ -144,16 +154,20 @@ #import "SentryObjCRuntimeWrapper.h" #import "SentryOptions+HybridSDKs.h" #import "SentryOptions+Private.h" +#import "SentryPerformanceTracker+Testing.h" #import "SentryPerformanceTracker.h" #import "SentryPerformanceTrackingIntegration.h" #import "SentryPredicateDescriptor.h" +#import "SentryPropagationContext.h" #import "SentryQueueableRequestManager.h" #import "SentryRateLimitParser.h" #import "SentryRateLimits.h" #import "SentryReachability.h" +#import "SentryReplayEvent.h" #import "SentryRetryAfterHeaderParser.h" #import "SentrySDK+Private.h" #import "SentrySDK+Tests.h" +#import "SentrySampleDecision+Private.h" #import "SentryScope+Private.h" #import "SentryScopeObserver.h" #import "SentryScopeSyncC.h" @@ -185,6 +199,8 @@ #import "SentryStacktrace.h" #import "SentryStacktraceBuilder.h" #import "SentrySubClassFinder.h" +#import "SentrySwift.h" +#import "SentrySwiftAsyncIntegration.h" #import "SentrySwizzleWrapper.h" #import "SentrySysctl.h" #import "SentrySystemEventBreadcrumbs.h" @@ -217,3 +233,4 @@ #import "TestSentrySpan.h" #import "TestSentryViewHierarchy.h" #import "URLSessionTaskMock.h" +@import _SentryPrivate; From 5b3518ea31471a9ec3fe0d8912843da77cae74e3 Mon Sep 17 00:00:00 2001 From: Philipp Hofmann Date: Thu, 11 Apr 2024 16:56:32 +0200 Subject: [PATCH 08/86] test: Remove @objc for TestTransport (#3848) --- SentryTestUtils/TestTransport.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/SentryTestUtils/TestTransport.swift b/SentryTestUtils/TestTransport.swift index eab268c9299..03ed7359e6a 100644 --- a/SentryTestUtils/TestTransport.swift +++ b/SentryTestUtils/TestTransport.swift @@ -1,7 +1,6 @@ import _SentryPrivate import Foundation -@objc public class TestTransport: NSObject, Transport { public var sentEnvelopes = Invocations() From deeb22c3cb39c89f6c211254366498180b469bee Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 11 Apr 2024 10:12:12 -0800 Subject: [PATCH 09/86] test: dont crash if tapping button to stop benchmark before starting one (#3844) --- .../iOS-Swift/Profiling/ProfilingViewController.swift | 8 +++++++- Samples/iOS-Swift/iOS-Swift/Tools/SentryBenchmarking.mm | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift b/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift index b4ca222bc15..bb25956119a 100644 --- a/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift +++ b/Samples/iOS-Swift/iOS-Swift/Profiling/ProfilingViewController.swift @@ -43,7 +43,13 @@ class ProfilingViewController: UIViewController, UITextFieldDelegate { @IBAction func stopBenchmark(_ sender: UIButton) { highlightButton(sender) - let value = SentryBenchmarking.stopBenchmark()! + guard let value = SentryBenchmarking.stopBenchmark() else { + let alert = UIAlertController(title: "Benchmark Error", message: "No benchmark result available.", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self.present(alert, animated: false) + print("[iOS-Swift] [debug] [ProfilingViewController] no benchmark result returned") + return + } valueTextField.isHidden = false valueTextField.text = value print("[iOS-Swift] [debug] [ProfilingViewController] benchmarking results:\n\(value)") diff --git a/Samples/iOS-Swift/iOS-Swift/Tools/SentryBenchmarking.mm b/Samples/iOS-Swift/iOS-Swift/Tools/SentryBenchmarking.mm index 9bfeafe1e3b..96a8fb46352 100644 --- a/Samples/iOS-Swift/iOS-Swift/Tools/SentryBenchmarking.mm +++ b/Samples/iOS-Swift/iOS-Swift/Tools/SentryBenchmarking.mm @@ -88,6 +88,11 @@ + (void)startBenchmark + (NSString *)stopBenchmark { + if (source == NULL) { + printf("[Sentry Benchmark] no benchmark in progress.\n"); + return nil; + } + dispatch_cancel(source); [samples addObject:cpuInfoByThread()]; From 88ca9d87c1abf82b6a3e6b38854b469f91bf956b Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Thu, 11 Apr 2024 17:16:35 -0800 Subject: [PATCH 10/86] feat(profiling): add API to start/stop a continuous session (#3834) --- Sentry.xcodeproj/project.pbxproj | 8 ++++++ .../Profiling/SentryContinuousProfiler.m | 20 +++++++++++++++ Sources/Sentry/SentryProfiler.mm | 7 +++--- Sources/Sentry/SentrySDK.m | 25 +++++++++++++++++++ Sources/Sentry/SentryTracer.m | 1 + .../Sentry/include/SentryContinuousProfiler.h | 18 +++++++++++++ Sources/Sentry/include/SentrySDK+Private.h | 15 +++++++++++ 7 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 Sources/Sentry/Profiling/SentryContinuousProfiler.m create mode 100644 Sources/Sentry/include/SentryContinuousProfiler.h diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 278402352b3..107ccf5c047 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -669,6 +669,8 @@ 8454CF8D293EAF9A006AC140 /* SentryMetricProfiler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8454CF8B293EAF9A006AC140 /* SentryMetricProfiler.mm */; }; 845C16D52A622A5B00EC9519 /* SentryTracer+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 845C16D42A622A5B00EC9519 /* SentryTracer+Private.h */; }; 8489B8882A5F7905009A055A /* SentryThreadWrapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8489B8872A5F7905009A055A /* SentryThreadWrapperTests.swift */; }; + 848A45192BBF8D33006AAAEC /* SentryContinuousProfiler.m in Sources */ = {isa = PBXBuildFile; fileRef = 848A45182BBF8D33006AAAEC /* SentryContinuousProfiler.m */; }; + 848A451A2BBF8D33006AAAEC /* SentryContinuousProfiler.h in Headers */ = {isa = PBXBuildFile; fileRef = 848A45172BBF8D33006AAAEC /* SentryContinuousProfiler.h */; }; 849AC40029E0C1FF00889C16 /* SentryFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849AC3FF29E0C1FF00889C16 /* SentryFormatterTests.swift */; }; 84A5D75B29D5170700388BFA /* TimeInterval+Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A5D75A29D5170700388BFA /* TimeInterval+Sentry.swift */; }; 84A8891C28DBD28900C51DFD /* SentryDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = 84A8891A28DBD28900C51DFD /* SentryDevice.h */; }; @@ -1681,6 +1683,8 @@ 8454CF8B293EAF9A006AC140 /* SentryMetricProfiler.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = SentryMetricProfiler.mm; path = Sources/Sentry/SentryMetricProfiler.mm; sourceTree = SOURCE_ROOT; }; 845C16D42A622A5B00EC9519 /* SentryTracer+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryTracer+Private.h"; path = "include/SentryTracer+Private.h"; sourceTree = ""; }; 8489B8872A5F7905009A055A /* SentryThreadWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SentryThreadWrapperTests.swift; path = Helper/SentryThreadWrapperTests.swift; sourceTree = ""; }; + 848A45172BBF8D33006AAAEC /* SentryContinuousProfiler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SentryContinuousProfiler.h; path = ../include/SentryContinuousProfiler.h; sourceTree = ""; }; + 848A45182BBF8D33006AAAEC /* SentryContinuousProfiler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SentryContinuousProfiler.m; sourceTree = ""; }; 849472802971C107002603DE /* SentrySystemWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySystemWrapperTests.swift; sourceTree = ""; }; 849472822971C2CD002603DE /* SentryNSProcessInfoWrapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSProcessInfoWrapperTests.swift; sourceTree = ""; }; 849472842971C41A002603DE /* SentryNSTimerFactoryTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryNSTimerFactoryTest.swift; sourceTree = ""; }; @@ -3314,6 +3318,8 @@ 84AF45A529A7FFA500FBB177 /* SentryProfiledTracerConcurrency.mm */, 840B7EF22BBF83DF008B8120 /* SentryProfiler+Private.h */, 03F84D2B27DD4191008FE43F /* SentryProfiler.mm */, + 848A45172BBF8D33006AAAEC /* SentryContinuousProfiler.h */, + 848A45182BBF8D33006AAAEC /* SentryContinuousProfiler.m */, 0354A22A2A134D9C003C3A04 /* SentryProfilerState.h */, 84281C422A578E5600EE88F2 /* SentryProfilerState.mm */, 84281C642A57D36100EE88F2 /* SentryProfilerState+ObjCpp.h */, @@ -3765,6 +3771,7 @@ 92672BB629C9A2A9006B021C /* SentryBreadcrumb+Private.h in Headers */, 03BCC38E27E2A377003232C7 /* SentryProfilingConditionals.h in Headers */, 0ADC33EE28D9BB890078D980 /* SentryUIDeviceWrapper.h in Headers */, + 848A451A2BBF8D33006AAAEC /* SentryContinuousProfiler.h in Headers */, 8E133FA625E72EB400ABD0BF /* SentrySamplingContext.h in Headers */, 0A9BF4E428A114B50068D266 /* SentryViewHierarchyIntegration.h in Headers */, D8BBD32728FD9FC00011F850 /* SentrySwift.h in Headers */, @@ -4430,6 +4437,7 @@ 7BBD188B244841FB00427C76 /* SentryHttpDateParser.m in Sources */, 840A11122B61E27500650D02 /* SentrySamplerDecision.m in Sources */, 8E4E7C8225DAB2A5006AB9E2 /* SentryTracer.m in Sources */, + 848A45192BBF8D33006AAAEC /* SentryContinuousProfiler.m in Sources */, 15E0A8E5240C457D00F044E3 /* SentryEnvelope.m in Sources */, 03F84D3627DD4191008FE43F /* SentryProfilingLogging.mm in Sources */, 8EC3AE7A25CA23B600E7591A /* SentrySpan.m in Sources */, diff --git a/Sources/Sentry/Profiling/SentryContinuousProfiler.m b/Sources/Sentry/Profiling/SentryContinuousProfiler.m new file mode 100644 index 00000000000..df648dfefd0 --- /dev/null +++ b/Sources/Sentry/Profiling/SentryContinuousProfiler.m @@ -0,0 +1,20 @@ +#import "SentryContinuousProfiler.h" +#import "SentryInternalDefines.h" + +@implementation SentryContinuousProfiler + +#pragma mark - Public + ++ (void)start +{ + // TODO: start a continuous profiling session + SENTRY_GRACEFUL_FATAL(@"TODO: start a continuous profiling session"); +} + ++ (void)stop +{ + // TODO: stop the continuous profiling session + SENTRY_GRACEFUL_FATAL(@"TODO: stop the continuous profiling session"); +} + +@end diff --git a/Sources/Sentry/SentryProfiler.mm b/Sources/Sentry/SentryProfiler.mm index a46891a133a..1bea79f08fe 100644 --- a/Sources/Sentry/SentryProfiler.mm +++ b/Sources/Sentry/SentryProfiler.mm @@ -3,6 +3,7 @@ #if SENTRY_TARGET_PROFILING_SUPPORTED # import "SentryAppStartMeasurement.h" # import "SentryClient+Private.h" +# import "SentryContinuousProfiler.h" # import "SentryDateUtils.h" # import "SentryDebugImageProvider.h" # import "SentryDebugMeta.h" @@ -288,6 +289,8 @@ @implementation SentryProfiler { NSTimer *_timeoutTimer; } +# pragma mark - Private + - (instancetype)init { if (!(self = [super init])) { @@ -341,8 +344,6 @@ - (void)scheduleTimeoutTimer }]; } -# pragma mark - Public - + (BOOL)startWithTracer:(SentryId *)traceId { std::lock_guard l(_gProfilerLock); @@ -479,8 +480,6 @@ + (nullable SentryEnvelopeItem *)createEnvelopeItemForProfilePayload: return payload; } -# pragma mark - Private - + (void)updateProfilePayload:(NSMutableDictionary *)payload forTransaction:(SentryTransaction *)transaction startTimestamp:(NSDate *)startTimestamp diff --git a/Sources/Sentry/SentrySDK.m b/Sources/Sentry/SentrySDK.m index 8d64d9064b9..00bd559ab23 100644 --- a/Sources/Sentry/SentrySDK.m +++ b/Sources/Sentry/SentrySDK.m @@ -29,6 +29,7 @@ #endif // SENTRY_HAS_UIKIT #if SENTRY_TARGET_PROFILING_SUPPORTED +# import "SentryContinuousProfiler.h" # import "SentryLaunchProfiling.h" #endif // SENTRY_TARGET_PROFILING_SUPPORTED @@ -521,6 +522,30 @@ + (void)crash } #endif +#if SENTRY_TARGET_PROFILING_SUPPORTED ++ (void)startProfiler +{ + if (!SENTRY_ASSERT_RETURN(currentHub.client.options.enableContinuousProfiling, + @"You must set SentryOptions.enableContinuousProfiling to true before starting a " + @"continuous profiler.")) { + return; + } + + [SentryContinuousProfiler start]; +} + ++ (void)stopProfiler +{ + if (!SENTRY_ASSERT_RETURN(currentHub.client.options.enableContinuousProfiling, + @"You must set SentryOptions.enableContinuousProfiling to true before using continuous " + @"profiling API.")) { + return; + } + + [SentryContinuousProfiler stop]; +} +#endif // SENTRY_TARGET_PROFILING_SUPPORTED + @end NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/SentryTracer.m b/Sources/Sentry/SentryTracer.m index 1ee47db2095..d63d93d3209 100644 --- a/Sources/Sentry/SentryTracer.m +++ b/Sources/Sentry/SentryTracer.m @@ -10,6 +10,7 @@ #import "SentryNSDictionarySanitize.h" #import "SentryNSTimerFactory.h" #import "SentryNoOpSpan.h" +#import "SentryOptions+Private.h" #import "SentryProfilingConditionals.h" #import "SentryRandom.h" #import "SentrySDK+Private.h" diff --git a/Sources/Sentry/include/SentryContinuousProfiler.h b/Sources/Sentry/include/SentryContinuousProfiler.h new file mode 100644 index 00000000000..8ced5610f2d --- /dev/null +++ b/Sources/Sentry/include/SentryContinuousProfiler.h @@ -0,0 +1,18 @@ +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * An interface to the new continuous profiling implementation. + */ +@interface SentryContinuousProfiler : NSObject + +/** Start a continuous profiling session if one doesn't already exist. */ ++ (void)start; + +/** Stop a continuous profiling session if there is one ongoing. */ ++ (void)stop; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Sentry/include/SentrySDK+Private.h b/Sources/Sentry/include/SentrySDK+Private.h index e46b479a82f..2b1c41c5bf3 100644 --- a/Sources/Sentry/include/SentrySDK+Private.h +++ b/Sources/Sentry/include/SentrySDK+Private.h @@ -1,3 +1,4 @@ +#import "SentryProfilingConditionals.h" #import "SentrySDK.h" @class SentryHub, SentryId, SentryAppStartMeasurement, SentryEnvelope; @@ -42,6 +43,20 @@ SentrySDK () */ + (void)captureEnvelope:(SentryEnvelope *)envelope; +#if SENTRY_TARGET_PROFILING_SUPPORTED +/** + * Start a new continuous profiling session if one is not already running. + * @seealso https://docs.sentry.io/platforms/apple/profiling/ + */ ++ (void)startProfiler; + +/** + * Stop a continuous profiling session if there is one ongoing. + * @seealso https://docs.sentry.io/platforms/apple/profiling/ + */ ++ (void)stopProfiler; +#endif // SENTRY_TARGET_PROFILING_SUPPORTED + @end NS_ASSUME_NONNULL_END From d9308fd7531e4b4a45e32ecb21406736ad9fd632 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Fri, 12 Apr 2024 10:39:25 -0800 Subject: [PATCH 11/86] ci: update codeql actions to unpinned v3 (#3849) --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c7a436b3b31..7099eb68da7 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@cdcdbb579706841c47f7063dda365e292e5cad7a # pin@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} @@ -42,4 +42,4 @@ jobs: -destination platform="iOS Simulator,OS=latest,name=iPhone 14 Pro" | xcpretty && exit ${PIPESTATUS[0]} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdcdbb579706841c47f7063dda365e292e5cad7a # pin@v2 + uses: github/codeql-action/analyze@v3 From 97de6c8b2cf6226837f9a265332f17ede9239d12 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 16 Apr 2024 15:19:29 +0200 Subject: [PATCH 12/86] feat: Link errors with session replay (#3851) Added replay id as error event tag and to baggage and trace headers --- Samples/iOS-Swift/iOS-Swift/AppDelegate.swift | 2 +- Sentry.xcodeproj/project.pbxproj | 1 + Sources/Sentry/Public/SentryScope.h | 5 +++ Sources/Sentry/SentryBaggage.m | 6 +++ Sources/Sentry/SentryScope.m | 2 + Sources/Sentry/SentrySessionReplay.m | 37 ++++++++++++++--- Sources/Sentry/SentryTraceContext.m | 18 +++++++-- Sources/Sentry/include/SentryBaggage.h | 5 ++- Sources/Sentry/include/SentrySessionReplay.h | 3 ++ Sources/Sentry/include/SentryTraceContext.h | 8 +++- .../Helper/SentrySerializationTests.swift | 4 +- .../SentrySessionReplayTests.swift | 10 ++++- .../Protocol/SentryEnvelopeTests.swift | 2 +- Tests/SentryTests/SentryScopeTests.m | 8 ++++ .../Transaction/SentryBaggageTests.swift | 6 +-- .../Transaction/SentryTraceStateTests.swift | 40 +++++++++++-------- 16 files changed, 121 insertions(+), 36 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift index 7125eb93257..03d38b423b9 100644 --- a/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift +++ b/Samples/iOS-Swift/iOS-Swift/AppDelegate.swift @@ -25,7 +25,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { options.debug = true if #available(iOS 16.0, *) { - options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1, redactAllText: false, redactAllImages: true) + options.experimental.sessionReplay = SentryReplayOptions(sessionSampleRate: 0, errorSampleRate: 1, redactAllText: true, redactAllImages: true) } if #available(iOS 15.0, *) { diff --git a/Sentry.xcodeproj/project.pbxproj b/Sentry.xcodeproj/project.pbxproj index 107ccf5c047..0c0e65967af 100644 --- a/Sentry.xcodeproj/project.pbxproj +++ b/Sentry.xcodeproj/project.pbxproj @@ -4325,6 +4325,7 @@ 7BCFBD6F2681D0EE00BC27D8 /* SentryCrashScopeObserver.m in Sources */, 7BD86ED1264A7CF6005439DB /* SentryAppStartMeasurement.m in Sources */, 7DC27EC723997EB7006998B5 /* SentryAutoBreadcrumbTrackingIntegration.m in Sources */, + D820CE142BB2F13C00BA339D /* SentryCoreGraphicsHelper.m in Sources */, 63FE717B20DA4C1100CDBAE8 /* SentryCrashReport.c in Sources */, 7B7A599726B692F00060A676 /* SentryScreenFrames.m in Sources */, 7B3398652459C15200BD9C96 /* SentryEnvelopeRateLimit.m in Sources */, diff --git a/Sources/Sentry/Public/SentryScope.h b/Sources/Sentry/Public/SentryScope.h index 4b6f354e3a7..575baf8d08f 100644 --- a/Sources/Sentry/Public/SentryScope.h +++ b/Sources/Sentry/Public/SentryScope.h @@ -21,6 +21,11 @@ NS_SWIFT_NAME(Scope) */ @property (nullable, nonatomic, strong) id span; +/** + * The id of current session replay. + */ +@property (nullable, nonatomic, strong) NSString *replayId; + /** * Gets the dictionary of currently set tags. */ diff --git a/Sources/Sentry/SentryBaggage.m b/Sources/Sentry/SentryBaggage.m index 2727d950096..3ca47fbe12e 100644 --- a/Sources/Sentry/SentryBaggage.m +++ b/Sources/Sentry/SentryBaggage.m @@ -19,6 +19,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId userSegment:(nullable NSString *)userSegment sampleRate:(nullable NSString *)sampleRate sampled:(nullable NSString *)sampled + replayId:(nullable NSString *)replayId { if (self = [super init]) { @@ -30,6 +31,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId _userSegment = userSegment; _sampleRate = sampleRate; _sampled = sampled; + _replayId = replayId; } return self; @@ -67,6 +69,10 @@ - (NSString *)toHTTPHeaderWithOriginalBaggage:(NSDictionary *_Nullable)originalB [information setValue:_sampled forKey:@"sentry-sampled"]; } + if (_replayId != nil) { + [information setValue:_replayId forKey:@"sentry-replay_id"]; + } + return [SentrySerialization baggageEncodedDictionary:information]; } diff --git a/Sources/Sentry/SentryScope.m b/Sources/Sentry/SentryScope.m index 7051d63614b..0361433bd73 100644 --- a/Sources/Sentry/SentryScope.m +++ b/Sources/Sentry/SentryScope.m @@ -103,6 +103,7 @@ - (instancetype)initWithScope:(SentryScope *)scope self.environmentString = scope.environmentString; self.levelEnum = scope.levelEnum; self.span = scope.span; + self.replayId = scope.replayId; } return self; } @@ -428,6 +429,7 @@ - (void)clearAttachments [serializedData setValue:[self.userObject serialize] forKey:@"user"]; [serializedData setValue:self.distString forKey:@"dist"]; [serializedData setValue:self.environmentString forKey:@"environment"]; + [serializedData setValue:self.replayId forKey:@"replay_id"]; if (self.fingerprints.count > 0) { [serializedData setValue:[self fingerprints] forKey:@"fingerprint"]; } diff --git a/Sources/Sentry/SentrySessionReplay.m b/Sources/Sentry/SentrySessionReplay.m index d6830ed4f19..d3a921132b9 100644 --- a/Sources/Sentry/SentrySessionReplay.m +++ b/Sources/Sentry/SentrySessionReplay.m @@ -2,6 +2,7 @@ #import "SentryAttachment+Private.h" #import "SentryDependencyContainer.h" #import "SentryDisplayLinkWrapper.h" +#import "SentryEnvelopeItemType.h" #import "SentryFileManager.h" #import "SentryHub+Private.h" #import "SentryLog.h" @@ -9,7 +10,9 @@ #import "SentryReplayEvent.h" #import "SentryReplayRecording.h" #import "SentrySDK+Private.h" +#import "SentryScope+Private.h" #import "SentrySwift.h" +#import "SentryTraceContext.h" #if SENTRY_HAS_UIKIT && !TARGET_OS_VISION @@ -31,7 +34,6 @@ @implementation SentrySessionReplay { NSDate *_videoSegmentStart; NSDate *_sessionStart; NSMutableArray *imageCollection; - SentryId *sessionReplayId; SentryReplayOptions *_replayOptions; SentryOnDemandReplay *_replayMaker; SentryDisplayLinkWrapper *_displayLink; @@ -88,7 +90,7 @@ - (void)start:(UIView *)rootView fullSession:(BOOL)full _lastScreenShot = _dateProvider.date; _videoSegmentStart = nil; _currentSegmentId = 0; - sessionReplayId = [[SentryId alloc] init]; + _sessionReplayId = [[SentryId alloc] init]; imageCollection = [NSMutableArray array]; if (full) { @@ -100,6 +102,8 @@ - (void)startFullReplay { _sessionStart = _lastScreenShot; _isFullSession = YES; + [SentrySDK.currentHub configureScope:^( + SentryScope *_Nonnull scope) { scope.replayId = [self->_sessionReplayId sentryIdString]; }]; } - (void)stop @@ -112,7 +116,12 @@ - (void)stop - (void)captureReplayForEvent:(SentryEvent *)event; { - if (_isFullSession || !_isRunning) { + if (!_isRunning) { + return; + } + + if (_isFullSession) { + [self setEventContext:event]; return; } @@ -124,6 +133,9 @@ - (void)captureReplayForEvent:(SentryEvent *)event; return; } + [self startFullReplay]; + [self setEventContext:event]; + NSURL *finalPath = [_urlToCache URLByAppendingPathComponent:@"replay.mp4"]; NSDate *replayStart = [_dateProvider.date dateByAddingTimeInterval:-_replayOptions.errorReplayDuration]; @@ -131,8 +143,23 @@ - (void)captureReplayForEvent:(SentryEvent *)event; [self createAndCapture:finalPath duration:_replayOptions.errorReplayDuration startedAt:replayStart]; +} - [self startFullReplay]; +- (void)setEventContext:(SentryEvent *)event +{ + if ([event.type isEqualToString:SentryEnvelopeItemTypeReplayVideo]) { + return; + } + + NSMutableDictionary *context = event.context.mutableCopy ?: [[NSMutableDictionary alloc] init]; + context[@"replay"] = @{ @"replay_id" : [_sessionReplayId sentryIdString] }; + event.context = context; + + NSMutableDictionary *tags = @{ @"replayId" : [_sessionReplayId sentryIdString] }.mutableCopy; + if (event.tags != nil) { + [tags addEntriesFromDictionary:event.tags]; + } + event.tags = tags; } - (void)newFrame:(CADisplayLink *)sender @@ -207,7 +234,7 @@ - (void)createAndCapture:(NSURL *)videoUrl } else { [self captureSegment:self->_currentSegmentId++ video:videoInfo - replayId:self->sessionReplayId + replayId:self->_sessionReplayId replayType:kSentryReplayTypeSession]; [self->_replayMaker releaseFramesUntil:videoInfo.end]; diff --git a/Sources/Sentry/SentryTraceContext.m b/Sources/Sentry/SentryTraceContext.m index 6675e73ae13..d249209c553 100644 --- a/Sources/Sentry/SentryTraceContext.m +++ b/Sources/Sentry/SentryTraceContext.m @@ -24,6 +24,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId userSegment:(nullable NSString *)userSegment sampleRate:(nullable NSString *)sampleRate sampled:(nullable NSString *)sampled + replayId:(nullable NSString *)replayId { if (self = [super init]) { _traceId = traceId; @@ -34,6 +35,7 @@ - (instancetype)initWithTraceId:(SentryId *)traceId _userSegment = userSegment; _sampleRate = sampleRate; _sampled = sampled; + _replayId = replayId; } return self; } @@ -80,7 +82,8 @@ - (nullable instancetype)initWithTracer:(SentryTracer *)tracer transaction:tracer.transactionContext.name userSegment:userSegment sampleRate:sampleRate - sampled:sampled]; + sampled:sampled + replayId:scope.replayId]; } - (instancetype)initWithTraceId:(SentryId *)traceId @@ -94,7 +97,8 @@ - (instancetype)initWithTraceId:(SentryId *)traceId transaction:nil userSegment:userSegment sampleRate:nil - sampled:nil]; + sampled:nil + replayId:nil]; } - (nullable instancetype)initWithDict:(NSDictionary *)dictionary @@ -120,7 +124,8 @@ - (nullable instancetype)initWithDict:(NSDictionary *)dictionary transaction:dictionary[@"transaction"] userSegment:userSegment sampleRate:dictionary[@"sample_rate"] - sampled:dictionary[@"sampled"]]; + sampled:dictionary[@"sampled"] + replayId:dictionary[@"replay_id"]]; } - (SentryBaggage *)toBaggage @@ -132,7 +137,8 @@ - (SentryBaggage *)toBaggage transaction:_transaction userSegment:_userSegment sampleRate:_sampleRate - sampled:_sampled]; + sampled:_sampled + replayId:_replayId]; return result; } @@ -165,6 +171,10 @@ - (SentryBaggage *)toBaggage [result setValue:_sampleRate forKey:@"sampled"]; } + if (_replayId != nil) { + [result setValue:_replayId forKey:@"replay_id"]; + } + return result; } diff --git a/Sources/Sentry/include/SentryBaggage.h b/Sources/Sentry/include/SentryBaggage.h index 1aa9a06b558..e62ca45cc9a 100644 --- a/Sources/Sentry/include/SentryBaggage.h +++ b/Sources/Sentry/include/SentryBaggage.h @@ -54,6 +54,8 @@ static NSString *const SENTRY_BAGGAGE_HEADER = @"baggage"; */ @property (nullable, nonatomic, strong) NSString *sampled; +@property (nullable, nonatomic, strong) NSString *replayId; + - (instancetype)initWithTraceId:(SentryId *)traceId publicKey:(NSString *)publicKey releaseName:(nullable NSString *)releaseName @@ -61,7 +63,8 @@ static NSString *const SENTRY_BAGGAGE_HEADER = @"baggage"; transaction:(nullable NSString *)transaction userSegment:(nullable NSString *)userSegment sampleRate:(nullable NSString *)sampleRate - sampled:(nullable NSString *)sampled; + sampled:(nullable NSString *)sampled + replayId:(nullable NSString *)replayId; - (NSString *)toHTTPHeaderWithOriginalBaggage:(NSDictionary *_Nullable)originalBaggage; diff --git a/Sources/Sentry/include/SentrySessionReplay.h b/Sources/Sentry/include/SentrySessionReplay.h index 953f11fdf81..3aac91b620f 100644 --- a/Sources/Sentry/include/SentrySessionReplay.h +++ b/Sources/Sentry/include/SentrySessionReplay.h @@ -9,6 +9,7 @@ @class SentryCurrentDateProvider; @class SentryDisplayLinkWrapper; @class SentryVideoInfo; +@class SentryId; @protocol SentryRandom; @protocol SentryRedactOptions; @@ -35,6 +36,8 @@ NS_ASSUME_NONNULL_BEGIN API_AVAILABLE(ios(16.0), tvos(16.0)) @interface SentrySessionReplay : NSObject +@property (nonatomic, strong, readonly) SentryId *sessionReplayId; + - (instancetype)initWithSettings:(SentryReplayOptions *)replayOptions replayFolderPath:(NSURL *)folderPath screenshotProvider:(id)photographer diff --git a/Sources/Sentry/include/SentryTraceContext.h b/Sources/Sentry/include/SentryTraceContext.h index ff4f399339d..9ed6a67816a 100644 --- a/Sources/Sentry/include/SentryTraceContext.h +++ b/Sources/Sentry/include/SentryTraceContext.h @@ -52,6 +52,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, nonatomic, readonly) NSString *sampled; +/** + * Id of the current session replay. + */ +@property (nullable, nonatomic, readonly) NSString *replayId; + /** * Initializes a SentryTraceContext with given properties. */ @@ -62,7 +67,8 @@ NS_ASSUME_NONNULL_BEGIN transaction:(nullable NSString *)transaction userSegment:(nullable NSString *)userSegment sampleRate:(nullable NSString *)sampleRate - sampled:(nullable NSString *)sampled; + sampled:(nullable NSString *)sampled + replayId:(nullable NSString *)replayId; /** * Initializes a SentryTraceContext with data from scope and options. diff --git a/Tests/SentryTests/Helper/SentrySerializationTests.swift b/Tests/SentryTests/Helper/SentrySerializationTests.swift index a848b898773..06be17427c9 100644 --- a/Tests/SentryTests/Helper/SentrySerializationTests.swift +++ b/Tests/SentryTests/Helper/SentrySerializationTests.swift @@ -5,7 +5,7 @@ class SentrySerializationTests: XCTestCase { private class Fixture { static var invalidData = "hi".data(using: .utf8)! - static var traceContext = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", transaction: "transaction", userSegment: "some segment", sampleRate: "0.25", sampled: "true") + static var traceContext = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", transaction: "transaction", userSegment: "some segment", sampleRate: "0.25", sampled: "true", replayId: nil) } func testSerializationFailsWithInvalidJSONObject() { @@ -124,7 +124,7 @@ class SentrySerializationTests: XCTestCase { } func testSentryEnvelopeSerializer_TraceStateWithoutUser() { - let trace = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", transaction: "transaction", userSegment: nil, sampleRate: nil, sampled: nil) + let trace = SentryTraceContext(trace: SentryId(), publicKey: "PUBLIC_KEY", releaseName: "RELEASE_NAME", environment: "TEST", transaction: "transaction", userSegment: nil, sampleRate: nil, sampled: nil, replayId: nil) let envelopeHeader = SentryEnvelopeHeader(id: nil, traceContext: trace) let envelope = SentryEnvelope(header: envelopeHeader, singleItem: createItemWithEmptyAttachment()) diff --git a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift index 1925b4eb2e9..c3f1b13746f 100644 --- a/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift +++ b/Tests/SentryTests/Integrations/SessionReplay/SentrySessionReplayTests.swift @@ -63,7 +63,7 @@ class SentrySessionReplayTests: XCTestCase { let screenshotProvider = ScreenshotProvider() let displayLink = TestDisplayLinkWrapper() let rootView = UIView() - let hub = ReplayHub(client: nil, andScope: nil) + let hub = ReplayHub(client: SentryClient(options: Options()), andScope: nil) let replayMaker = TestReplayMaker() let cacheFolder = FileManager.default.temporaryDirectory @@ -117,8 +117,10 @@ class SentrySessionReplayTests: XCTestCase { @available(iOS 16.0, tvOS 16, *) func testSentReplay_FullSession() { let fixture = startFixture() + let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) sut.start(fixture.rootView, fullSession: true) + expect(fixture.hub.scope.replayId) == sut.sessionReplayId.sentryIdString fixture.dateProvider.advance(by: 1) @@ -148,6 +150,8 @@ class SentrySessionReplayTests: XCTestCase { let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) sut.start(fixture.rootView, fullSession: false) + expect(fixture.hub.scope.replayId) == nil + fixture.dateProvider.advance(by: 1) Dynamic(sut).newFrame(nil) @@ -165,10 +169,12 @@ class SentrySessionReplayTests: XCTestCase { let fixture = startFixture() let sut = fixture.getSut(options: SentryReplayOptions(sessionSampleRate: 1, errorSampleRate: 1)) sut.start(fixture.rootView, fullSession: false) - + expect(fixture.hub.scope.replayId) == nil let event = Event(error: NSError(domain: "Some error", code: 1)) sut.capture(for: event) + expect(fixture.hub.scope.replayId) == sut.sessionReplayId.sentryIdString + expect(event.context?["replay"]?["replay_id"] as? String) == sut.sessionReplayId.sentryIdString assertFullSession(sut, expected: true) } diff --git a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift index 49f771008ba..8118a1a6cd1 100644 --- a/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift +++ b/Tests/SentryTests/Protocol/SentryEnvelopeTests.swift @@ -157,7 +157,7 @@ class SentryEnvelopeTests: XCTestCase { func testInitSentryEnvelopeHeader_SetIdAndTraceState() { let eventId = SentryId() - let traceContext = SentryTraceContext(trace: SentryId(), publicKey: "publicKey", releaseName: "releaseName", environment: "environment", transaction: "transaction", userSegment: nil, sampleRate: nil, sampled: nil) + let traceContext = SentryTraceContext(trace: SentryId(), publicKey: "publicKey", releaseName: "releaseName", environment: "environment", transaction: "transaction", userSegment: nil, sampleRate: nil, sampled: nil, replayId: nil) let envelopeHeader = SentryEnvelopeHeader(id: eventId, traceContext: traceContext) XCTAssertEqual(eventId, envelopeHeader.eventId) diff --git a/Tests/SentryTests/SentryScopeTests.m b/Tests/SentryTests/SentryScopeTests.m index 458c1b005a3..5da23c3721e 100644 --- a/Tests/SentryTests/SentryScopeTests.m +++ b/Tests/SentryTests/SentryScopeTests.m @@ -154,6 +154,14 @@ - (void)testEnvironmentSerializes XCTAssertEqualObjects([[scope serialize] objectForKey:@"environment"], expectedEnvironment); } +- (void)testReplaySerializes +{ + SentryScope *scope = [[SentryScope alloc] init]; + NSString *expectedReplayId = @"Some_replay_id"; + [scope setReplayId:expectedReplayId]; + XCTAssertEqualObjects([[scope serialize] objectForKey:@"replay_id"], expectedReplayId); +} + - (void)testClearBreadcrumb { SentryScope *scope = [[SentryScope alloc] init]; diff --git a/Tests/SentryTests/Transaction/SentryBaggageTests.swift b/Tests/SentryTests/Transaction/SentryBaggageTests.swift index b42f3f16b10..330e4bb008d 100644 --- a/Tests/SentryTests/Transaction/SentryBaggageTests.swift +++ b/Tests/SentryTests/Transaction/SentryBaggageTests.swift @@ -5,13 +5,13 @@ import XCTest class SentryBaggageTests: XCTestCase { func test_baggageToHeader_AppendToOriginal() { - let header = SentryBaggage(trace: SentryId.empty, publicKey: "publicKey", releaseName: "release name", environment: "teste", transaction: "transaction", userSegment: "test user", sampleRate: "0.49", sampled: "true").toHTTPHeader(withOriginalBaggage: ["a": "a", "sentry-trace_id": "to-be-overwritten"]) + let header = SentryBaggage(trace: SentryId.empty, publicKey: "publicKey", releaseName: "release name", environment: "teste", transaction: "transaction", userSegment: "test user", sampleRate: "0.49", sampled: "true", replayId: "some_replay_id").toHTTPHeader(withOriginalBaggage: ["a": "a", "sentry-trace_id": "to-be-overwritten"]) - XCTAssertEqual(header, "a=a,sentry-environment=teste,sentry-public_key=publicKey,sentry-release=release%20name,sentry-sample_rate=0.49,sentry-sampled=true,sentry-trace_id=00000000000000000000000000000000,sentry-transaction=transaction,sentry-user_segment=test%20user") + XCTAssertEqual(header, "a=a,sentry-environment=teste,sentry-public_key=publicKey,sentry-release=release%20name,sentry-replay_id=some_replay_id,sentry-sample_rate=0.49,sentry-sampled=true,sentry-trace_id=00000000000000000000000000000000,sentry-transaction=transaction,sentry-user_segment=test%20user") } func test_baggageToHeader_onlyTrace_ignoreNils() { - let header = SentryBaggage(trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: nil, transaction: nil, userSegment: nil, sampleRate: nil, sampled: nil).toHTTPHeader(withOriginalBaggage: nil) + let header = SentryBaggage(trace: SentryId.empty, publicKey: "publicKey", releaseName: nil, environment: nil, transaction: nil, userSegment: nil, sampleRate: nil, sampled: nil, replayId: nil).toHTTPHeader(withOriginalBaggage: nil) XCTAssertEqual(header, "sentry-public_key=publicKey,sentry-trace_id=00000000000000000000000000000000") } diff --git a/Tests/SentryTests/Transaction/SentryTraceStateTests.swift b/Tests/SentryTests/Transaction/SentryTraceStateTests.swift index b5c7fa52c89..7ecd00628bb 100644 --- a/Tests/SentryTests/Transaction/SentryTraceStateTests.swift +++ b/Tests/SentryTests/Transaction/SentryTraceStateTests.swift @@ -1,3 +1,4 @@ +import Nimble import SentryTestUtils import XCTest @@ -19,6 +20,7 @@ class SentryTraceContextTests: XCTestCase { let releaseName = "SentrySessionTrackerIntegrationTests" let environment = "debug" let sampled = "true" + let replayId = "some_replay_id" init() { options = Options() @@ -33,6 +35,7 @@ class SentryTraceContextTests: XCTestCase { scope.setUser(User(userId: userId)) scope.userObject?.segment = userSegment scope.span = tracer + scope.replayId = replayId traceId = tracer.traceId } @@ -59,7 +62,9 @@ class SentryTraceContextTests: XCTestCase { transaction: fixture.transactionName, userSegment: fixture.userSegment, sampleRate: fixture.sampleRate, - sampled: fixture.sampled) + sampled: fixture.sampled, + replayId: fixture.replayId + ) assertTraceState(traceContext: traceContext) } @@ -131,27 +136,30 @@ class SentryTraceContextTests: XCTestCase { transaction: fixture.transactionName, userSegment: fixture.userSegment, sampleRate: fixture.sampleRate, - sampled: fixture.sampled) + sampled: fixture.sampled, + replayId: fixture.replayId) let baggage = traceContext.toBaggage() - XCTAssertEqual(baggage.traceId, fixture.traceId) - XCTAssertEqual(baggage.publicKey, fixture.publicKey) - XCTAssertEqual(baggage.releaseName, fixture.releaseName) - XCTAssertEqual(baggage.environment, fixture.environment) - XCTAssertEqual(baggage.userSegment, fixture.userSegment) - XCTAssertEqual(baggage.sampleRate, fixture.sampleRate) - XCTAssertEqual(baggage.sampled, fixture.sampled) + expect(baggage.traceId) == fixture.traceId + expect(baggage.publicKey) == fixture.publicKey + expect(baggage.releaseName) == fixture.releaseName + expect(baggage.environment) == fixture.environment + expect(baggage.userSegment) == fixture.userSegment + expect(baggage.sampleRate) == fixture.sampleRate + expect(baggage.sampled) == fixture.sampled + expect(baggage.replayId) == fixture.replayId } func assertTraceState(traceContext: SentryTraceContext) { - XCTAssertEqual(traceContext.traceId, fixture.traceId) - XCTAssertEqual(traceContext.publicKey, fixture.publicKey) - XCTAssertEqual(traceContext.releaseName, fixture.releaseName) - XCTAssertEqual(traceContext.environment, fixture.environment) - XCTAssertEqual(traceContext.transaction, fixture.transactionName) - XCTAssertEqual(traceContext.userSegment, fixture.userSegment) - XCTAssertEqual(traceContext.sampled, fixture.sampled) + expect(traceContext.traceId) == fixture.traceId + expect(traceContext.publicKey) == fixture.publicKey + expect(traceContext.releaseName) == fixture.releaseName + expect(traceContext.environment) == fixture.environment + expect(traceContext.transaction) == fixture.transactionName + expect(traceContext.userSegment) == fixture.userSegment + expect(traceContext.sampled) == fixture.sampled + expect(traceContext.replayId) == fixture.replayId } } From eb077dd952eb309235943f301ee6d2943c33e124 Mon Sep 17 00:00:00 2001 From: Dhiogo Brustolin Date: Tue, 16 Apr 2024 16:55:52 +0200 Subject: [PATCH 13/86] Save framework without UIKit/AppKit as Github Asset for releases (#3858) The compiled framework that is not linked with UI APIs wast not being saved as asset --- .github/workflows/release.yml | 1 + CHANGELOG.md | 4 ++++ Makefile | 1 + 3 files changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a37e4efd9e0..80146e2594e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,6 +35,7 @@ jobs: Carthage/Sentry.xcframework.zip Carthage/Sentry-Dynamic.xcframework.zip Carthage/SentrySwiftUI.xcframework.zip + Carthage/Sentry-WihoutUIKitOrAppKit.zip overwrite: true job_release: diff --git a/CHANGELOG.md b/CHANGELOG.md index f0ddfc61d63..461f681228d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Add Session Replay, which is **still experimental**. (#3625) +### Fixes + +- Save framework without UIKit/AppKit as Github Asset for releases (#3858) + ## 8.24.0 ### Features diff --git a/Makefile b/Makefile index 70717268578..018641e1c94 100644 --- a/Makefile +++ b/Makefile @@ -65,6 +65,7 @@ build-xcframework: ditto -c -k -X --rsrc --keepParent Carthage/Sentry.xcframework Carthage/Sentry.xcframework.zip ditto -c -k -X --rsrc --keepParent Carthage/Sentry-Dynamic.xcframework Carthage/Sentry-Dynamic.xcframework.zip ditto -c -k -X --rsrc --keepParent Carthage/SentrySwiftUI.xcframework Carthage/SentrySwiftUI.xcframework.zip + ditto -c -k -X --rsrc --keepParent Carthage/Sentry-WihoutUIKitOrAppKit.xcframework Carthage/Sentry-WihoutUIKitOrAppKit.zip build-xcframework-sample: ./scripts/create-carthage-json.sh From 953e914d3c154ff0a96298b980fa09f71a062678 Mon Sep 17 00:00:00 2001 From: mlch911 Date: Thu, 18 Apr 2024 23:18:12 +0800 Subject: [PATCH 14/86] fix: crash when call SentryUIApplication in background thread (#3855) Fix crash when call SentryUIApplication in background thread. #3836 Co-authored-by: Dhiogo Brustolin --- CHANGELOG.md | 1 + Sources/Sentry/SentryDispatchQueueWrapper.m | 28 ++++++-- Sources/Sentry/SentryUIApplication.m | 71 ++++++++++--------- .../include/SentryDispatchQueueWrapper.h | 5 ++ 4 files changed, 66 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 461f681228d..0e02e70b013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### Fixes +- Crash due to a background call to -[UIApplication applicationState] (#3855) - Save framework without UIKit/AppKit as Github Asset for releases (#3858) ## 8.24.0 diff --git a/Sources/Sentry/SentryDispatchQueueWrapper.m b/Sources/Sentry/SentryDispatchQueueWrapper.m index e092e4f1303..b238ee349dd 100644 --- a/Sources/Sentry/SentryDispatchQueueWrapper.m +++ b/Sources/Sentry/SentryDispatchQueueWrapper.m @@ -56,23 +56,43 @@ - (void)dispatchSyncOnMainQueue:(void (^)(void))block } } +- (nullable id)dispatchSyncOnMainQueueWithResult:(id (^)(void))block +{ + return [self dispatchSyncOnMainQueueWithResult:block timeout:DISPATCH_TIME_FOREVER]; +} + - (BOOL)dispatchSyncOnMainQueue:(void (^)(void))block timeout:(NSTimeInterval)timeout +{ + NSNumber *result = [self + dispatchSyncOnMainQueueWithResult:^id _Nonnull { + block(); + return @YES; + } + timeout:timeout]; + return result.boolValue; +} + +- (nullable id)dispatchSyncOnMainQueueWithResult:(id (^)(void))block timeout:(NSTimeInterval)timeout { if ([NSThread isMainThread]) { - block(); + return block(); } else { dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block id result; dispatch_async(dispatch_get_main_queue(), ^{ - block(); + result = block(); dispatch_semaphore_signal(semaphore); }); dispatch_time_t timeout_t = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); - return dispatch_semaphore_wait(semaphore, timeout_t) == 0; + if (dispatch_semaphore_wait(semaphore, timeout_t) == 0) { + return result; + } else { + return nil; + } } - return YES; } - (void)dispatchAfter:(NSTimeInterval)interval block:(dispatch_block_t)block diff --git a/Sources/Sentry/SentryUIApplication.m b/Sources/Sentry/SentryUIApplication.m index d6eb19a2240..bd1ca3bbbe1 100644 --- a/Sources/Sentry/SentryUIApplication.m +++ b/Sources/Sentry/SentryUIApplication.m @@ -28,7 +28,8 @@ - (instancetype)init // We store the application state when the app is initialized // and we keep track of its changes by the notifications // this way we avoid calling sharedApplication in a background thread - appState = self.sharedApplication.applicationState; + [SentryDependencyContainer.sharedInstance.dispatchQueueWrapper + dispatchOnMainQueue:^{ self->appState = self.sharedApplication.applicationState; }]; } return self; } @@ -63,29 +64,34 @@ - (UIApplication *)sharedApplication - (NSArray *)windows { - UIApplication *app = [self sharedApplication]; - NSMutableArray *result = [NSMutableArray array]; - - if (@available(iOS 13.0, tvOS 13.0, *)) { - NSArray *scenes = [self getApplicationConnectedScenes:app]; - for (UIScene *scene in scenes) { - if (scene.activationState == UISceneActivationStateForegroundActive && scene.delegate && - [scene.delegate respondsToSelector:@selector(window)]) { - id window = [scene.delegate performSelector:@selector(window)]; - if (window) { - [result addObject:window]; + return [SentryDependencyContainer.sharedInstance.dispatchQueueWrapper + dispatchSyncOnMainQueueWithResult:^id _Nonnull { + UIApplication *app = [self sharedApplication]; + NSMutableArray *result = [NSMutableArray array]; + + if (@available(iOS 13.0, tvOS 13.0, *)) { + NSArray *scenes = [self getApplicationConnectedScenes:app]; + for (UIScene *scene in scenes) { + if (scene.activationState == UISceneActivationStateForegroundActive + && scene.delegate && + [scene.delegate respondsToSelector:@selector(window)]) { + id window = [scene.delegate performSelector:@selector(window)]; + if (window) { + [result addObject:window]; + } + } } } - } - } - id appDelegate = [self getApplicationDelegate:app]; + id appDelegate = [self getApplicationDelegate:app]; - if ([appDelegate respondsToSelector:@selector(window)] && appDelegate.window != nil) { - [result addObject:appDelegate.window]; - } + if ([appDelegate respondsToSelector:@selector(window)] && appDelegate.window != nil) { + [result addObject:appDelegate.window]; + } - return result; + return result; + } + timeout:0.01]; } - (NSArray *)relevantViewControllers @@ -109,23 +115,18 @@ - (UIApplication *)sharedApplication - (nullable NSArray *)relevantViewControllersNames { - __block NSArray *result = nil; - - void (^addViewNames)(void) = ^{ - NSArray *viewControllers - = SentryDependencyContainer.sharedInstance.application.relevantViewControllers; - NSMutableArray *vcsNames = [[NSMutableArray alloc] initWithCapacity:viewControllers.count]; - for (id vc in viewControllers) { - [vcsNames addObject:[SwiftDescriptor getObjectClassName:vc]]; + return [SentryDependencyContainer.sharedInstance.dispatchQueueWrapper + dispatchSyncOnMainQueueWithResult:^id _Nonnull { + NSArray *viewControllers + = SentryDependencyContainer.sharedInstance.application.relevantViewControllers; + NSMutableArray *vcsNames = + [[NSMutableArray alloc] initWithCapacity:viewControllers.count]; + for (id vc in viewControllers) { + [vcsNames addObject:[SwiftDescriptor getObjectClassName:vc]]; + } + return [NSArray arrayWithArray:vcsNames]; } - result = [NSArray arrayWithArray:vcsNames]; - }; - - [[SentryDependencyContainer.sharedInstance dispatchQueueWrapper] - dispatchSyncOnMainQueue:addViewNames - timeout:0.01]; - - return result; + timeout:0.01]; } - (NSArray *)relevantViewControllerFromWindow:(UIWindow *)window diff --git a/Sources/Sentry/include/SentryDispatchQueueWrapper.h b/Sources/Sentry/include/SentryDispatchQueueWrapper.h index 61d9361af95..4a91f36989b 100644 --- a/Sources/Sentry/include/SentryDispatchQueueWrapper.h +++ b/Sources/Sentry/include/SentryDispatchQueueWrapper.h @@ -19,8 +19,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)dispatchSyncOnMainQueue:(void (^)(void))block; +- (nullable id)dispatchSyncOnMainQueueWithResult:(id (^)(void))block; + - (BOOL)dispatchSyncOnMainQueue:(void (^)(void))block timeout:(NSTimeInterval)timeout; +- (nullable id)dispatchSyncOnMainQueueWithResult:(id (^)(void))block + timeout:(NSTimeInterval)timeout; + - (void)dispatchAfter:(NSTimeInterval)interval block:(dispatch_block_t)block; - (void)dispatchCancel:(dispatch_block_t)block; From f80109876fb5910e278957499d566bc708adffe1 Mon Sep 17 00:00:00 2001 From: Andrew McKnight Date: Fri, 19 Apr 2024 15:04:18 -0800 Subject: [PATCH 15/86] test: use correct a11y property and skip if early OS versions (#3863) --- .../iOS-Swift/Base.lproj/Main.storyboard | 30 +++++++++---------- .../iOS-SwiftUITests/LaunchUITests.swift | 4 ++- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard index 99d889c2667..43363350dc6 100644 --- a/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard +++ b/Samples/iOS-Swift/iOS-Swift/Base.lproj/Main.storyboard @@ -518,7 +518,7 @@ - + @@ -686,13 +686,13 @@ - + - + - +