diff --git a/.github/workflows/sdk-release-manual.yml b/.github/workflows/sdk-release-manual.yml
index 582dfacfc..1e145db29 100644
--- a/.github/workflows/sdk-release-manual.yml
+++ b/.github/workflows/sdk-release-manual.yml
@@ -227,21 +227,8 @@ jobs:
echo "Updated Package.swift:"
head -15 Package.swift
- - name: Upload xcframeworks to S3
- env:
- AWS_ACCESS_KEY_ID: ${{ secrets.MP_IOS_SDK_S3_KEY }}
- AWS_SECRET_ACCESS_KEY: ${{ secrets.MP_IOS_SDK_S3_SECRET }}
- AWS_DEFAULT_REGION: ${{ secrets.MP_IOS_SDK_S3_REGION }}
- run: |
- aws s3 cp mParticle_Apple_SDK.xcframework.zip s3://static.mparticle.com/sdk/ios/v${VERSION}/mParticle_Apple_SDK.xcframework.zip
-
- echo "Uploaded xcframeworks to S3:"
- echo " - s3://static.mparticle.com/sdk/ios/v${VERSION}/mParticle_Apple_SDK.xcframework.zip"
-
- name: Commit version changes
run: |
- # trunk format the files before committing
- trunk fmt
# Only add the version-related files, exclude build artifacts
git add \
CHANGELOG.md \
@@ -337,5 +324,4 @@ jobs:
--base main \
--head chore/release-v${VERSION} \
--title "chore: Release v${VERSION}" \
- --body "$PR_BODY" \
- --reviewer mParticle/sdk-team
+ --body "$PR_BODY"
diff --git a/.github/workflows/sdk-release.yml b/.github/workflows/sdk-release.yml
index 253bdac17..6a36ac321 100644
--- a/.github/workflows/sdk-release.yml
+++ b/.github/workflows/sdk-release.yml
@@ -1,4 +1,4 @@
-name: iOS SDK Release
+name: iOS SDK Release (Deprecated)
on:
workflow_dispatch:
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..047d621f7
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,162 @@
+# AGENTS.md
+
+## About mParticle SDKs
+
+mParticle is a Customer Data Platform that collects, validates, and forwards event data to analytics and marketing integrations. The SDK is responsible for:
+
+- **Event Collection**: Capturing user interactions, commerce events, and custom events
+- **Identity Management**: Managing user identity across sessions and platforms
+- **Event Forwarding**: Routing events to configured integrations (kits/forwarders)
+- **Data Validation**: Enforcing data quality through data plans
+- **Consent Management**: Handling user consent preferences (GDPR, CCPA)
+- **Session Management**: Tracking user sessions and engagement
+- **Batch Upload**: Efficiently uploading events to mParticle servers
+
+### Glossary of Terms
+
+- **MPID (mParticle ID)**: Unique identifier for a user across sessions and devices
+- **Kit/Forwarder**: Third-party integration (e.g., Google Analytics, Braze) that receives events from the SDK
+- **Data Plan**: Validation schema that defines expected events and their attributes
+- **Workspace**: A customer's mParticle environment (identified by API key)
+- **Batch**: Collection of events grouped together for efficient server upload
+- **Identity Request**: API call to identify, login, logout, or modify a user's identity
+- **Session**: Period of user activity with automatic timeout (typically 30 minutes)
+- **Consent State**: User's privacy preferences (GDPR, CCPA) that control data collection and forwarding
+- **User Attributes**: Key-value pairs describing user properties (e.g., email, age, preferences)
+- **Custom Events**: Application-specific events defined by the developer
+- **Commerce Events**: Predefined events for e-commerce tracking (purchases, product views, etc.)
+- **Event Type**: Category of event (Navigation, Location, Transaction, UserContent, UserPreference, Social, Other)
+
+## Role for agents
+
+You are a senior iOS SDK engineer specializing in customer data platform (CDP) SDK development.
+
+- Treat this as a **public SDK / framework** (distributed via SPM, and CocoaPods), not a full consumer app.
+- Prioritize: API stability, minimal footprint, backward compatibility (iOS 15.6+, tvOS 15.6+), thread-safety, privacy compliance.
+- The SDK handles event tracking, identity management, consent, commerce events, push notifications, and integration kits.
+- Avoid proposing big refactors unless explicitly asked; prefer additive changes + deprecations.
+
+## Quick Start for Agents
+
+- Open the Xcode project/workspace with Xcode 16.4+.
+- Primary actions:
+ - Build: via Xcode scheme or `xcodebuild`.
+ - Run unit tests: `Rokt_WidgetTests/` or via Xcode (Command + U).
+ - Lint: `trunk check` (primary enforcement tool).
+ - Pod lint: `pod lib lint mParticle-Apple-SDK.podspec`.
+ - Size report: Check binary size impact via CI workflow.
+- Always validate changes with the full sequence in "Code style, quality, and validation" below before proposing or committing.
+
+## Strict Do's and Don'ts
+
+### Always Do
+
+- Maintain compatibility with mParticle's kit/integration ecosystem.
+- Keep public API surface additive; deprecate instead of remove.
+- Mark public APIs with thorough documentation (HeaderDoc for Obj-C, `///` for Swift).
+- Ensure changes work on both iOS and tvOS targets.
+- Run `trunk check` and unit tests before any commit.
+- Measure & report size impact before proposing dependency or asset changes.
+- Update `PrivacyInfo.xcprivacy` if data collection practices change.
+
+### Never
+
+- Introduce new third-party dependencies without size/performance justification and approval.
+- Block the main thread (no synchronous network, heavy computation, etc.).
+- Crash on bad input/network — always provide fallback / error callback.
+- Touch CI configs (`.github/`), release scripts (`Scripts/`), or CI YAML without explicit request.
+- Propose dropping iOS 15.6 / tvOS 15.6 support or raising min deployment target.
+- Break kit/integration compatibility without explicit coordination.
+- Modify vendored libraries in `Libraries/` without explicit request.
+
+## When to Ask for Clarification
+
+- Before adding any new dependency.
+- Before dropping support for OS versions.
+- Before making breaking API changes.
+- When changes affect the kit/integration interface.
+- When test failures suggest the original code may have had bugs.
+
+## Project overview
+
+- mParticle Apple SDK (Rokt fork): a comprehensive customer data platform SDK for iOS and tvOS written in Objective-C and Swift.
+- Handles event tracking, user identity management, consent management, commerce events, push notification handling, and integration kit orchestration.
+- Distributed via Swift Package Manager, CocoaPods, and Carthage.
+- Integration kits (like the Rokt kit) plug into this SDK to forward events to third-party services.
+
+## Key paths
+
+- `mParticle-Apple-SDK/` — Main SDK source (40+ subdirectories).
+ - `Include/` — Public headers (46 files).
+ - `AppNotifications/` — Push notification handling.
+ - `Consent/` — Consent management.
+ - `Data Model/` — Core data structures.
+ - `Ecommerce/` — Commerce event handling.
+ - `Event/` — Event processing.
+ - `Identity/` — User identity management.
+ - `Kits/` — Integration kit infrastructure.
+ - `Network/` — Network communication.
+ - `Persistence/` — Data storage.
+- `mParticle-Apple-SDK-Swift/` — Swift-only components.
+- `UnitTests/` — Unit tests (ObjCTests, SwiftTests, Mocks).
+- `IntegrationTests/` — Integration tests (Tuist + WireMock).
+- `Example/` — Sample app (11 subdirectories).
+- `Scripts/` — Build, release, and utility scripts.
+ - `release.sh`, `xcframework.sh`, `carthage.sh`, `check_coverage.sh`.
+- `Package.swift` — SPM manifest (swift-tools-version 5.5).
+- `mParticle-Apple-SDK.podspec` — CocoaPods spec (v8.41.1).
+- `PrivacyInfo.xcprivacy` — iOS privacy manifest.
+- `ARCHITECTURE.md` — Architecture documentation with sequence diagrams.
+- `CHANGELOG.md` — Release notes (extensive).
+- `MIGRATING.md` — Migration guides for older versions.
+- `RELEASE.md` — Release process documentation.
+- `CONTRIBUTING.md` — Contribution guidelines.
+
+## Code style, quality, and validation
+
+- **Lint & format tools**:
+ - SwiftFormat: configured in project.
+ - SwiftLint: configured in project.
+ - **Primary enforcement tool**: `trunk check` (via Trunk.io). If Trunk unavailable, fall back to `swiftformat .` && `swiftlint`.
+ - Important: Only add comments if absolutely necessary. If you're adding comments, review why the code is hard to reason with and rewrite that first.
+
+- **Strict post-change validation rule (always follow this)**:
+ After **any** code change, refactor, or addition — even small ones — you **must** run the full validation sequence:
+ 1. `trunk check` — to lint, format-check, and catch style/quality issues.
+ 2. Build the SDK: via Xcode or `xcodebuild` for both iOS and tvOS.
+ 3. Run unit tests: both Objective-C and Swift test suites in `UnitTests/`.
+ 4. `pod lib lint mParticle-Apple-SDK.podspec` — verify CocoaPods spec is valid.
+ 5. If change affects code, assets, or dependencies: check coverage via `Scripts/check_coverage.sh`.
+ - Only propose / commit changes if all steps pass cleanly.
+ - If `trunk check` suggests auto-fixes, apply them first and re-validate.
+ - Never bypass this — it's required to maintain SDK stability, footprint, and public API quality.
+
+- **Style preferences**:
+ - Objective-C: follow Apple's Coding Guidelines for Cocoa.
+ - Swift: prefer `let` over `var`; use value types where possible.
+ - Write thorough documentation for all public APIs.
+ - Avoid force-unwraps in Swift; use proper error handling in Objective-C.
+
+- **Testing expectations**:
+ - Unit tests in `UnitTests/ObjCTests/` and `UnitTests/SwiftTests/`.
+ - Mocks in `UnitTests/Mocks/`.
+ - Integration tests in `IntegrationTests/`.
+ - Code coverage tracked via `Scripts/check_coverage.sh`.
+ - After changes, always re-run affected tests + full suite if core/shared code is touched.
+
+- **CHANGELOG.md maintenance**:
+ - For **substantial changes**, **always add a clear entry** to `CHANGELOG.md`.
+ - Use standard categories: `Added`, `Changed`, `Deprecated`, `Fixed`, `Removed`, `Security`.
+ - Keep entries concise and written in imperative mood.
+ - Update `CHANGELOG.md` **before** finalizing a change.
+ - Never auto-generate or hallucinate changelog entries — flag for human review.
+
+## Pull request and branching
+
+- Follow mParticle's standard PR and branching conventions.
+
+## External Resources
+
+- [mParticle Apple SDK Documentation](https://docs.mparticle.com/developers/sdk/ios/)
+- [Rokt mParticle Integration Docs](https://docs.rokt.com/developers/integration-guides/rokt-ads/customer-data-platforms/mparticle/)
+- [ARCHITECTURE.md](./ARCHITECTURE.md) — SDK architecture and sequence diagrams.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7fade928..df7521da1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,22 @@
+# [8.43.1](https://github.com/mParticle/mparticle-apple-sdk/compare/v8.42.2...v8.43.1) (2026-02-16)
+
+### Bug Fixes
+
+- fix: MPNetworkCommunication background task (#584) ([dfab795e](https://github.com/mParticle/mparticle-apple-sdk/commit/dfab795e2bd68b6eb51f5cb0e5bcf1f551ae1182))
+- fix: PreferredLanguages may be empty (#583) ([5a538c3b](https://github.com/mParticle/mparticle-apple-sdk/commit/5a538c3b3fc67444b76b8e3e756ec3f520c1d23c))
+- fix: endSessionIfTimedOut race condition (#582) ([b2eb508c](https://github.com/mParticle/mparticle-apple-sdk/commit/b2eb508c401c3b189f9f4abcb149066f5959cbea))
+- fix: Potential MPURLRequestBuilder crash (#578) ([70c0076c](https://github.com/mParticle/mparticle-apple-sdk/commit/70c0076c3e4d0f32a1ff4c0ad9c5518d8a1a5fb8))
+- fix: Add Try/Catch to File Write (#581) ([18045c9e](https://github.com/mParticle/mparticle-apple-sdk/commit/18045c9e32c1a01cb954e08dd9a5ee590e6eb096))
+- fix: App crash when JSON serialization of upload dictionary (#579) ([a5e19600](https://github.com/mParticle/mparticle-apple-sdk/commit/a5e196005cbcda92889f4f032498646a5108b58e))
+- fix: Guarantee UserDefaults Thread Safety (#580) ([7baa7b41](https://github.com/mParticle/mparticle-apple-sdk/commit/7baa7b41a3d38b7bbc5afa4364b948b5de09f6f3))
+- fix: background expiration race (#577) ([9d97bd32](https://github.com/mParticle/mparticle-apple-sdk/commit/9d97bd32fee29e25cb748c1ade4af1c933fa949a))
+- fix: Add Brackets Thread Safety Tests (#573) ([0d831cc5](https://github.com/mParticle/mparticle-apple-sdk/commit/0d831cc522c38834081483dce8428a1737ce8b1e))
+- fix: Thread-safe access to currentUser to prevent crash during kit replay (#576) ([a3ba57e8](https://github.com/mParticle/mparticle-apple-sdk/commit/a3ba57e860c68552c2d6297f7b8deb04ac09fe95))
+- fix: app crash from [MPUpload description] (#572) ([91c0c5d4](https://github.com/mParticle/mparticle-apple-sdk/commit/91c0c5d4880e064fcb07569283fddb886ce1eb33))
+- fix: MPURLRequestBuilder build crash (#575) ([604afeef](https://github.com/mParticle/mparticle-apple-sdk/commit/604afeefdfe32e5c2785cc3404c64941fcfda847))
+- fix: Mitigate Thread-safety of DateFormatter (#574) ([7b36691d](https://github.com/mParticle/mparticle-apple-sdk/commit/7b36691d5fb50739da8e17b76cdba127caefaa19))
+- fix: Use Defensive Copy for ActiveKitsRegistry (#571) ([e5d5e273](https://github.com/mParticle/mparticle-apple-sdk/commit/e5d5e27313bd6445ff491590168ab9718f05b86e))
+
# [8.42.2](https://github.com/mParticle/mparticle-apple-sdk/compare/v8.42.1...v8.42.2) (2026-02-11)
### Features
diff --git a/Framework/Info.plist b/Framework/Info.plist
index 5f8e20ebc..c9656ac5d 100644
--- a/Framework/Info.plist
+++ b/Framework/Info.plist
@@ -15,7 +15,7 @@
CFBundlePackageType
FMWK
CFBundleShortVersionString
- 8.42.2
+ 8.43.1
CFBundleSignature
????
CFBundleVersion
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-ccpa-consent.json b/IntegrationTests/wiremock-recordings/mappings/mapping-ccpa-consent.json
index 5b52514e2..6248f6a80 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-ccpa-consent.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-ccpa-consent.json
@@ -69,7 +69,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"p": "arm64",
"tz": "${json-unit.ignore}",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-commerce-event-purchase.json b/IntegrationTests/wiremock-recordings/mappings/mapping-commerce-event-purchase.json
index da8a70770..994ce15de 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-commerce-event-purchase.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-commerce-event-purchase.json
@@ -81,7 +81,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"b": "arm64",
"p": "arm64",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-gdpr-consent.json b/IntegrationTests/wiremock-recordings/mappings/mapping-gdpr-consent.json
index e2abfdbe5..09b6d9302 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-gdpr-consent.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-gdpr-consent.json
@@ -59,7 +59,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"tz": "${json-unit.ignore}",
"p": "arm64",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-increment-session-attribute-ss.json b/IntegrationTests/wiremock-recordings/mappings/mapping-increment-session-attribute-ss.json
index df5cd3857..07e50400d 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-increment-session-attribute-ss.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-increment-session-attribute-ss.json
@@ -52,7 +52,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"b": "arm64",
"tz": "${json-unit.ignore}",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-increment-session-attribute.json b/IntegrationTests/wiremock-recordings/mappings/mapping-increment-session-attribute.json
index ca9d31b63..8f86bcb62 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-increment-session-attribute.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-increment-session-attribute.json
@@ -58,7 +58,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"b": "arm64",
"tz": "${json-unit.ignore}",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-increment-user-attribute-set.json b/IntegrationTests/wiremock-recordings/mappings/mapping-increment-user-attribute-set.json
index 7ceccea5d..3b30f6df8 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-increment-user-attribute-set.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-increment-user-attribute-set.json
@@ -56,7 +56,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"p": "arm64",
"b": "arm64",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-increment-user-attribute.json b/IntegrationTests/wiremock-recordings/mappings/mapping-increment-user-attribute.json
index 0b6fdaa8f..c8d78e201 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-increment-user-attribute.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-increment-user-attribute.json
@@ -56,7 +56,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"p": "arm64",
"b": "arm64",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-log-error.json b/IntegrationTests/wiremock-recordings/mappings/mapping-log-error.json
index 0e5f75ecd..159807d00 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-log-error.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-log-error.json
@@ -59,7 +59,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"p": "arm64",
"b": "arm64",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-log-event-with-flags.json b/IntegrationTests/wiremock-recordings/mappings/mapping-log-event-with-flags.json
index ebf3fd1a9..377542504 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-log-event-with-flags.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-log-event-with-flags.json
@@ -65,7 +65,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"tz": "${json-unit.ignore}",
"p": "arm64",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-log-exception.json b/IntegrationTests/wiremock-recordings/mappings/mapping-log-exception.json
index e513bc5a7..22d7f1312 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-log-exception.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-log-exception.json
@@ -57,7 +57,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"tz": "${json-unit.ignore}",
"bid": "${json-unit.ignore}",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-log-idfa.json b/IntegrationTests/wiremock-recordings/mappings/mapping-log-idfa.json
index c7578d667..ea07812dd 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-log-idfa.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-log-idfa.json
@@ -8,7 +8,7 @@
"equalToJson": {
"client_sdk": {
"platform": "ios",
- "sdk_version": "8.42.2",
+ "sdk_version": "8.43.1",
"sdk_vendor": "mparticle"
},
"environment": "development",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-log-screen.json b/IntegrationTests/wiremock-recordings/mappings/mapping-log-screen.json
index 55693cfee..ced3e63cf 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-log-screen.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-log-screen.json
@@ -54,7 +54,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"tz": "${json-unit.ignore}",
"bid": "${json-unit.ignore}",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-rokt-identify.json b/IntegrationTests/wiremock-recordings/mappings/mapping-rokt-identify.json
index 9dcc3c9d2..43b38eb81 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-rokt-identify.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-rokt-identify.json
@@ -8,7 +8,7 @@
"equalToJson": {
"client_sdk": {
"platform": "ios",
- "sdk_version": "8.42.2",
+ "sdk_version": "8.43.1",
"sdk_vendor": "mparticle"
},
"environment": "development",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-rokt-select-placement.json b/IntegrationTests/wiremock-recordings/mappings/mapping-rokt-select-placement.json
index b4a92e710..532dc17b4 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-rokt-select-placement.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-rokt-select-placement.json
@@ -63,7 +63,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"p": "arm64",
"tz": "${json-unit.ignore}",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-set-att-status.json b/IntegrationTests/wiremock-recordings/mappings/mapping-set-att-status.json
index 1fc2478de..7758ea13f 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-set-att-status.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-set-att-status.json
@@ -59,7 +59,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"tz": "${json-unit.ignore}",
"p": "arm64",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-set-session-attribute.json b/IntegrationTests/wiremock-recordings/mappings/mapping-set-session-attribute.json
index 616981035..5b068004e 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-set-session-attribute.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-set-session-attribute.json
@@ -58,7 +58,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"p": "arm64",
"tz": "${json-unit.ignore}",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-set-user-attributes.json b/IntegrationTests/wiremock-recordings/mappings/mapping-set-user-attributes.json
index d6107d888..5011f755b 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-set-user-attributes.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-set-user-attributes.json
@@ -84,7 +84,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"p": "arm64",
"b": "arm64",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-timed-event.json b/IntegrationTests/wiremock-recordings/mappings/mapping-timed-event.json
index a5cdf8f47..c655bc33c 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-timed-event.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-timed-event.json
@@ -59,7 +59,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"tz": "${json-unit.ignore}",
"bid": "${json-unit.ignore}",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-v1-identify.json b/IntegrationTests/wiremock-recordings/mappings/mapping-v1-identify.json
index d5713aa21..b4cca8a69 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-v1-identify.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-v1-identify.json
@@ -8,7 +8,7 @@
"equalToJson": {
"client_sdk": {
"platform": "ios",
- "sdk_version": "8.42.2",
+ "sdk_version": "8.43.1",
"sdk_vendor": "mparticle"
},
"environment": "development",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-v2-events-log-event.json b/IntegrationTests/wiremock-recordings/mappings/mapping-v2-events-log-event.json
index c21fcb481..dadb0437c 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-v2-events-log-event.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-v2-events-log-event.json
@@ -120,7 +120,7 @@
],
"uitl": 60,
"oo": false,
- "sdk": "8.42.2",
+ "sdk": "8.43.1",
"di": {
"p": "arm64",
"bid": "${json-unit.ignore}",
diff --git a/IntegrationTests/wiremock-recordings/mappings/mapping-v4-config-get-config.json b/IntegrationTests/wiremock-recordings/mappings/mapping-v4-config-get-config.json
index 2f4c02fa0..e1c0a5369 100644
--- a/IntegrationTests/wiremock-recordings/mappings/mapping-v4-config-get-config.json
+++ b/IntegrationTests/wiremock-recordings/mappings/mapping-v4-config-get-config.json
@@ -1,7 +1,7 @@
{
"id": "a7d323f1-a729-39c6-86cd-4035c71f7d05",
"request": {
- "urlPattern": "/v4/us1-[a-f0-9]+/config\\?av=1\\.0&sv=8\\.42\\.2",
+ "urlPattern": "/v4/us1-[a-f0-9]+/config\\?av=1\\.0&sv=8.43.1",
"method": "GET"
},
"response": {
diff --git a/Scripts/update_mapping_versions.sh b/Scripts/update_mapping_versions.sh
index 40adf60dc..4008ad721 100755
--- a/Scripts/update_mapping_versions.sh
+++ b/Scripts/update_mapping_versions.sh
@@ -8,28 +8,27 @@ if [[ -z ${VERSION} ]]; then
exit 1
fi
-# Update SDK version in integration test mappings
-# Update "sdk" field in all mapping files
+# Escape dots in version for use in sed patterns
+ESCAPED_VERSION="${VERSION//./\\.}"
+
+# Update SDK version in integration test mappings using sed
+# This preserves the original file formatting (unlike jq which re-serializes)
find IntegrationTests/wiremock-recordings/mappings -name "*.json" -type f | while read -r mapping_file; do
- # Update top-level "sdk" field
- if jq -e '.request.bodyPatterns[0].equalToJson.sdk' "${mapping_file}" >/dev/null 2>&1; then
- tmp_file=$(mktemp)
- jq --indent 2 '.request.bodyPatterns[0].equalToJson.sdk = "'"${VERSION}"'"' "${mapping_file}" >"${tmp_file}" && mv "${tmp_file}" "${mapping_file}"
+ # Update "sdk": "x.y.z" field
+ if grep -q '"sdk":' "${mapping_file}"; then
+ sed -i '' 's/"sdk": "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"/"sdk": "'"${VERSION}"'"/' "${mapping_file}"
fi
- # Update "client_sdk.sdk_version" field (for v1 identify endpoints)
- if jq -e '.request.bodyPatterns[0].equalToJson.client_sdk.sdk_version' "${mapping_file}" >/dev/null 2>&1; then
- tmp_file=$(mktemp)
- jq --indent 2 '.request.bodyPatterns[0].equalToJson.client_sdk.sdk_version = "'"${VERSION}"'"' "${mapping_file}" >"${tmp_file}" && mv "${tmp_file}" "${mapping_file}"
+ # Update "sdk_version": "x.y.z" field (for v1 identify endpoints)
+ if grep -q '"sdk_version":' "${mapping_file}"; then
+ sed -i '' 's/"sdk_version": "[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"/"sdk_version": "'"${VERSION}"'"/' "${mapping_file}"
fi
done || true
-# Update SDK version in config mapping urlPattern (sv=...)
-ESCAPED_VERSION="${VERSION//./\\.}"
+# Update SDK version in config mapping urlPattern (sv=x.y.z with escaped dots)
config_file="IntegrationTests/wiremock-recordings/mappings/mapping-v4-config-get-config.json"
if [[ -f ${config_file} ]]; then
- # Use jq with sub() function to replace version in urlPattern (matching escaped dots \.)
- tmp_file=$(mktemp)
- jq --indent 2 --arg new_version "${ESCAPED_VERSION}" '.request.urlPattern |= sub("sv=\\d+\\\\\\.\\d+\\\\\\.\\d+"; "sv=" + $new_version)' "${config_file}" >"${tmp_file}" && mv "${tmp_file}" "${config_file}"
+ # Match sv=X\.Y\.Z (escaped dots) and replace with new version (also escaped)
+ sed -i '' 's/sv=[0-9][0-9]*\\\\.[0-9][0-9]*\\\\.[0-9][0-9]*/sv='"${ESCAPED_VERSION}"'/' "${config_file}"
echo "Updated SDK version in ${config_file} urlPattern to sv=${ESCAPED_VERSION}"
else
echo "Warning: ${config_file} not found"
diff --git a/UnitTests/ObjCTests/MPBackendControllerTests.m b/UnitTests/ObjCTests/MPBackendControllerTests.m
index 923bc445c..38cf89ec1 100644
--- a/UnitTests/ObjCTests/MPBackendControllerTests.m
+++ b/UnitTests/ObjCTests/MPBackendControllerTests.m
@@ -17,6 +17,7 @@
#import "MPKitConfiguration.h"
#import "MPBaseTestCase.h"
#import "MPUserDefaultsConnector.h"
+#import "MPApplication.h"
#if TARGET_OS_IOS == 1
#import
@@ -94,9 +95,22 @@ - (void)uploadBatchesWithCompletionHandler:(void(^)(BOOL success))completionHand
- (NSMutableArray *> *)userIdentitiesForUserId:(NSNumber *)userId;
- (void)cleanUp:(NSTimeInterval)currentTime;
- (void)processDidFinishLaunching:(NSNotification *)notification;
+- (void)beginBackgroundTask;
+- (void)endBackgroundTask;
+- (void)beginBackgroundTimeCheckLoop;
+- (void)cancelBackgroundTimeCheckLoop;
+- (void)endSessionIfTimedOut;
+@property NSOperationQueue *backgroundCheckQueue;
+@property UIBackgroundTaskIdentifier backendBackgroundTaskIdentifier;
+@property NSTimeInterval timeOfLastEventInBackground;
+@property NSTimeInterval timeAppWentToBackgroundInCurrentSession;
@end
+@interface MPApplication_PRIVATE(Tests)
++ (void)setMockApplication:(id)mockApplication;
+@end
+
#pragma mark - MPBackendControllerTests unit test class
@interface MPBackendControllerTests : MPBaseTestCase {
dispatch_queue_t messageQueue;
@@ -112,11 +126,14 @@ @implementation MPBackendControllerTests
- (void)setUp {
[super setUp];
- messageQueue = [MParticle messageQueue];
[MPPersistenceController_PRIVATE setMpid:@1];
[MParticle sharedInstance].persistenceController = [[MPPersistenceController_PRIVATE alloc] init];
+ // Must read messageQueue AFTER [MParticle sharedInstance] triggers singleton
+ // recreation, otherwise we get the old executor's queue.
+ messageQueue = [MParticle messageQueue];
+
[MParticle sharedInstance].stateMachine.apiKey = @"unit_test_app_key";
[MParticle sharedInstance].stateMachine.secret = @"unit_test_secret";
@@ -124,6 +141,8 @@ - (void)setUp {
[MParticle sharedInstance].backendController = [[MPBackendController_PRIVATE alloc] initWithDelegate:(id)[MParticle sharedInstance]];
self.backendController = [MParticle sharedInstance].backendController;
+ messageQueue = [MParticle messageQueue];
+
[self notificationController];
}
@@ -2306,4 +2325,275 @@ - (void)testProcessDidFinishLaunchingWithWebpageURL {
XCTAssertEqual(instance.stateMachine.launchInfo.url, testURL);
}
+#pragma mark - Background Time Check Loop Tests
+
+- (void)testExpirationHandlerCancelsBackgroundTimeCheckLoop {
+ // Verify that the expiration handler cancels the background check loop
+ // so that backgroundTimeRemaining is not called after the OS begins suspension.
+
+ // 1. Set up a mock UIApplication that tracks backgroundTimeRemaining calls
+ __block NSInteger backgroundTimeRemainingCallCount = 0;
+ __block void (^capturedExpirationHandler)(void) = nil;
+
+ id mockApplication = OCMClassMock([UIApplication class]);
+ OCMStub([mockApplication applicationState]).andReturn(UIApplicationStateBackground);
+ OCMStub([mockApplication backgroundTimeRemaining]).andDo(^(NSInvocation *invocation) {
+ backgroundTimeRemainingCallCount++;
+ NSTimeInterval remaining = 25.0;
+ [invocation setReturnValue:&remaining];
+ });
+ OCMStub([mockApplication beginBackgroundTaskWithExpirationHandler:([OCMArg checkWithBlock:^BOOL(id obj) {
+ capturedExpirationHandler = [obj copy];
+ return YES;
+ }])]).andReturn((UIBackgroundTaskIdentifier)42);
+ OCMStub([mockApplication endBackgroundTask:(UIBackgroundTaskIdentifier)42]);
+
+ [MPApplication_PRIVATE setMockApplication:mockApplication];
+
+ // 2. Start the background task on the main queue (captures the expiration handler)
+ // beginBackgroundTask dispatches to main, so we use an expectation to wait for it
+ XCTestExpectation *taskStarted = [self expectationWithDescription:@"Background task started"];
+ [self.backendController beginBackgroundTask];
+ // Give the main queue time to process the dispatched block
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [taskStarted fulfill];
+ });
+ [self waitForExpectations:@[taskStarted] timeout:2.0];
+
+ XCTAssertNotNil(capturedExpirationHandler, @"Expiration handler should have been captured");
+
+ // 3. Start the background time check loop on the message queue
+ dispatch_async(messageQueue, ^{
+ [self.backendController beginBackgroundTimeCheckLoop];
+ });
+
+ // Let the loop run a few iterations.
+ // Use NSRunLoop instead of sleep so the main queue stays alive
+ // (the loop calls dispatch_sync to the main queue to check app state)
+ [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];
+ NSInteger callsBeforeExpiration = backgroundTimeRemainingCallCount;
+ XCTAssertGreaterThan(callsBeforeExpiration, 0, @"Loop should have called backgroundTimeRemaining at least once");
+
+ // 4. Simulate the OS firing the expiration handler
+ capturedExpirationHandler();
+ // Let the main queue process the cancelBackgroundTimeCheckLoop and endBackgroundTask dispatches
+ [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.5]];
+
+ // 5. Record calls right after expiration and wait to see if any new calls arrive
+ NSInteger callsAtExpiration = backgroundTimeRemainingCallCount;
+ [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];
+ NSInteger callsAfterExpiration = backgroundTimeRemainingCallCount;
+
+ // 6. Assert: no new backgroundTimeRemaining calls after expiration
+ XCTAssertEqual(callsAfterExpiration, callsAtExpiration,
+ @"backgroundTimeRemaining should not be called after expiration handler fires. "
+ "Calls at expiration: %ld, calls after waiting: %ld",
+ (long)callsAtExpiration, (long)callsAfterExpiration);
+
+ // 7. Verify the loop's operation queue is empty (loop exited)
+ XCTAssertEqual(self.backendController.backgroundCheckQueue.operationCount, 0,
+ @"Background check queue should have no running operations after expiration");
+
+ // Clean up
+ [self.backendController cancelBackgroundTimeCheckLoop];
+ [MPApplication_PRIVATE setMockApplication:nil];
+}
+
+- (void)testBackgroundTimeCheckLoopStoresTimeRemainingInLocalVariable {
+ // Verify that backgroundTimeRemaining is called only once per loop iteration
+ // (not twice as in the original code that called it again for logging).
+
+ __block NSInteger backgroundTimeRemainingCallCount = 0;
+ __block NSTimeInterval simulatedTime = 15.0;
+
+ id mockApplication = OCMClassMock([UIApplication class]);
+
+ // First call returns background, subsequent calls return foreground to exit loop after one iteration
+ __block NSInteger stateCallCount = 0;
+ OCMStub([mockApplication applicationState]).andDo(^(NSInvocation *invocation) {
+ UIApplicationState state = (stateCallCount == 0)
+ ? UIApplicationStateBackground
+ : UIApplicationStateActive;
+ stateCallCount++;
+ [invocation setReturnValue:&state];
+ });
+
+ OCMStub([mockApplication backgroundTimeRemaining]).andDo(^(NSInvocation *invocation) {
+ backgroundTimeRemainingCallCount++;
+ [invocation setReturnValue:&simulatedTime];
+ });
+ OCMStub([mockApplication beginBackgroundTaskWithExpirationHandler:OCMOCK_ANY]).andReturn((UIBackgroundTaskIdentifier)42);
+ OCMStub([mockApplication endBackgroundTask:(UIBackgroundTaskIdentifier)42]);
+
+ [MPApplication_PRIVATE setMockApplication:mockApplication];
+
+ dispatch_async(messageQueue, ^{
+ [self.backendController beginBackgroundTimeCheckLoop];
+ });
+
+ // Wait for the loop to run one iteration and exit (it exits because applicationState returns active on second call)
+ // Use NSRunLoop so the main queue stays alive for the loop's dispatch_sync calls
+ [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]];
+
+ // Should be called exactly once per iteration (not twice like the old code)
+ XCTAssertEqual(backgroundTimeRemainingCallCount, 1,
+ @"backgroundTimeRemaining should be called exactly once per loop iteration, got %ld",
+ (long)backgroundTimeRemainingCallCount);
+
+ [self.backendController cancelBackgroundTimeCheckLoop];
+ [MPApplication_PRIVATE setMockApplication:nil];
+}
+
+- (void)testEndSessionIfTimedOutDispatchesToMessageQueue {
+ // Verify that endSessionIfTimedOut called from a non-message-queue thread
+ // does not mutate session properties directly on that thread, but instead
+ // dispatches the work to the message queue.
+
+ // 1. Set up a session and make it eligible for timeout
+ dispatch_sync(messageQueue, ^{
+ [self.backendController beginSessionWithIsManual:NO date:[NSDate date]];
+ });
+ XCTAssertNotNil(self.backendController.session, @"Session should exist");
+
+ NSTimeInterval pastTime = [[NSDate date] timeIntervalSince1970] - 200;
+ dispatch_sync(messageQueue, ^{
+ self.backendController.sessionTimeout = 30;
+ self.backendController.timeOfLastEventInBackground = pastTime;
+ self.backendController.timeAppWentToBackgroundInCurrentSession = pastTime;
+ });
+
+ // 2. Capture the session's endTime before the call
+ __block NSTimeInterval endTimeBefore;
+ dispatch_sync(messageQueue, ^{
+ endTimeBefore = self.backendController.session.endTime;
+ });
+
+ // 3. Call endSessionIfTimedOut from a background thread (not the message queue)
+ XCTestExpectation *bgCallDone = [self expectationWithDescription:@"Background call completed"];
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ [self.backendController endSessionIfTimedOut];
+ [bgCallDone fulfill];
+ });
+ [self waitForExpectations:@[bgCallDone] timeout:2.0];
+
+ // 4. Wait for the message queue to drain and process the dispatched block
+ XCTestExpectation *mqDrained = [self expectationWithDescription:@"Message queue drained"];
+ dispatch_async(messageQueue, ^{
+ [mqDrained fulfill];
+ });
+ [self waitForExpectations:@[mqDrained] timeout:5.0];
+
+ // 5. Verify the session was ended (set to nil by processOpenSessionsEndingCurrent:YES)
+ __block MPSession *sessionAfter;
+ dispatch_sync(messageQueue, ^{
+ sessionAfter = self.backendController.session;
+ });
+ XCTAssertNil(sessionAfter, @"Session should be nil after endSessionIfTimedOut processes on the message queue");
+}
+
+- (void)testEndSessionIfTimedOutDoesNothingWhenAutomaticSessionTrackingDisabled {
+ // Verify endSessionIfTimedOut is a no-op when automaticSessionTracking is disabled.
+
+ dispatch_sync(messageQueue, ^{
+ [self.backendController beginSessionWithIsManual:NO date:[NSDate date]];
+ });
+ XCTAssertNotNil(self.backendController.session, @"Session should exist");
+
+ NSTimeInterval pastTime = [[NSDate date] timeIntervalSince1970] - 200;
+ dispatch_sync(messageQueue, ^{
+ self.backendController.sessionTimeout = 30;
+ self.backendController.timeOfLastEventInBackground = pastTime;
+ self.backendController.timeAppWentToBackgroundInCurrentSession = pastTime;
+ });
+
+ // Disable automatic session tracking without calling startWithOptions (which
+ // would recreate backendController and add unrelated async work).
+ [[MParticle sharedInstance] setValue:@NO forKey:@"automaticSessionTracking"];
+
+ __block NSString *sessionUUIDBefore;
+ __block NSTimeInterval endTimeBefore;
+ __block NSTimeInterval lastEventBefore;
+ dispatch_sync(messageQueue, ^{
+ sessionUUIDBefore = self.backendController.session.uuid;
+ endTimeBefore = self.backendController.session.endTime;
+ lastEventBefore = self.backendController.timeOfLastEventInBackground;
+ });
+
+ XCTestExpectation *bgCallDone = [self expectationWithDescription:@"Background call completed"];
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ [self.backendController endSessionIfTimedOut];
+ [bgCallDone fulfill];
+ });
+ [self waitForExpectations:@[bgCallDone] timeout:2.0];
+
+ // Drain queue to ensure there is no delayed mutation.
+ XCTestExpectation *mqDrained = [self expectationWithDescription:@"Message queue drained"];
+ dispatch_async(messageQueue, ^{
+ [mqDrained fulfill];
+ });
+ [self waitForExpectations:@[mqDrained] timeout:5.0];
+
+ __block MPSession *sessionAfter;
+ __block NSTimeInterval endTimeAfter;
+ __block NSTimeInterval lastEventAfter;
+ dispatch_sync(messageQueue, ^{
+ sessionAfter = self.backendController.session;
+ endTimeAfter = self.backendController.session.endTime;
+ lastEventAfter = self.backendController.timeOfLastEventInBackground;
+ });
+
+ XCTAssertNotNil(sessionAfter, @"Session should not be ended when automaticSessionTracking is disabled");
+ XCTAssertEqualObjects(sessionAfter.uuid, sessionUUIDBefore, @"Session identity should be unchanged");
+ XCTAssertEqualWithAccuracy(endTimeAfter, endTimeBefore, DBL_EPSILON, @"Session endTime should be unchanged");
+ XCTAssertEqualWithAccuracy(lastEventAfter, lastEventBefore, DBL_EPSILON, @"Background last-event time should be unchanged");
+}
+
+- (void)testConcurrentEndSessionIfTimedOutDoesNotCrash {
+ // Verify that calling endSessionIfTimedOut simultaneously from a background
+ // thread and the message queue does not crash.
+
+ // 1. Set up a session eligible for timeout
+ dispatch_sync(messageQueue, ^{
+ [self.backendController beginSessionWithIsManual:NO date:[NSDate date]];
+ });
+ XCTAssertNotNil(self.backendController.session, @"Session should exist");
+
+ NSTimeInterval pastTime = [[NSDate date] timeIntervalSince1970] - 200;
+ dispatch_sync(messageQueue, ^{
+ self.backendController.sessionTimeout = 30;
+ self.backendController.timeOfLastEventInBackground = pastTime;
+ self.backendController.timeAppWentToBackgroundInCurrentSession = pastTime;
+ });
+
+ // 2. Call from both a background thread and the message queue simultaneously
+ XCTestExpectation *bgDone = [self expectationWithDescription:@"Background thread done"];
+ XCTestExpectation *mqDone = [self expectationWithDescription:@"Message queue done"];
+
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+ [self.backendController endSessionIfTimedOut];
+ [bgDone fulfill];
+ });
+
+ dispatch_async(messageQueue, ^{
+ [self.backendController endSessionIfTimedOut];
+ [mqDone fulfill];
+ });
+
+ [self waitForExpectations:@[bgDone, mqDone] timeout:5.0];
+
+ // 3. Wait for the message queue to fully drain
+ XCTestExpectation *drained = [self expectationWithDescription:@"Queue drained"];
+ dispatch_async(messageQueue, ^{
+ [drained fulfill];
+ });
+ [self waitForExpectations:@[drained] timeout:5.0];
+
+ // 4. Verify session ended exactly once (session is nil, no crash)
+ __block MPSession *sessionAfter;
+ dispatch_sync(messageQueue, ^{
+ sessionAfter = self.backendController.session;
+ });
+ XCTAssertNil(sessionAfter, @"Session should be nil after concurrent endSessionIfTimedOut calls");
+}
+
@end
diff --git a/UnitTests/ObjCTests/MPDataModelTests.m b/UnitTests/ObjCTests/MPDataModelTests.m
index d570a6f80..c2ddf5fdf 100644
--- a/UnitTests/ObjCTests/MPDataModelTests.m
+++ b/UnitTests/ObjCTests/MPDataModelTests.m
@@ -226,6 +226,87 @@ - (void)testUploadInstance {
XCTAssertNotEqualObjects(uploadCopy, upload, @"Should not have been equal.");
}
+- (void)testUploadSerializationDeepCopiesValues {
+ // Verify that deep copy produces correct, independent data
+ NSMutableString *mutableString = [NSMutableString stringWithString:@"mutable"];
+ NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:@"a", @"b", nil];
+ NSMutableDictionary *mutableNested = [NSMutableDictionary dictionaryWithDictionary:@{@"key":@"value"}];
+
+ NSDictionary *uploadDictionary = @{
+ kMPMessageIdKey:[[NSUUID UUID] UUIDString],
+ kMPTimestampKey:@(123456789),
+ @"string":mutableString,
+ @"array":mutableArray,
+ @"nested":mutableNested,
+ @"number":@(42),
+ @"null":[NSNull null],
+ kMPMessagesKey:@[]
+ };
+
+ MPUpload *upload = [[MPUpload alloc] initWithSessionId:@1
+ uploadDictionary:uploadDictionary
+ dataPlanId:@"test"
+ dataPlanVersion:@(1)
+ uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
+ XCTAssertNotNil(upload, @"Upload should succeed with valid JSON data.");
+
+ // Mutate originals after creation -- upload should be unaffected
+ [mutableString appendString:@"_changed"];
+ [mutableArray addObject:@"c"];
+ mutableNested[@"key"] = @"changed";
+
+ NSDictionary *serialized = [upload dictionaryRepresentation];
+ XCTAssertNotNil(serialized);
+ XCTAssertEqualObjects(serialized[@"string"], @"mutable");
+ XCTAssertEqual([serialized[@"array"] count], 2);
+ XCTAssertEqualObjects(serialized[@"nested"][@"key"], @"value");
+ XCTAssertEqualObjects(serialized[@"number"], @(42));
+ XCTAssertEqualObjects(serialized[@"null"], [NSNull null]);
+}
+
+- (void)testUploadSerializationDropsNonJSONTypes {
+ // Non-JSON-compatible types (NSDate, NSURL, etc.) are dropped by deep copy.
+ // If the remaining data is still valid JSON, the upload succeeds with those keys missing.
+ NSDictionary *uploadDictionary = @{
+ kMPMessageIdKey:[[NSUUID UUID] UUIDString],
+ kMPTimestampKey:@(123456789),
+ @"valid":@"ok",
+ @"date":[NSDate date],
+ @"url":[NSURL URLWithString:@"https://example.com"],
+ kMPMessagesKey:@[]
+ };
+
+ MPUpload *upload = [[MPUpload alloc] initWithSessionId:@1
+ uploadDictionary:uploadDictionary
+ dataPlanId:@"test"
+ dataPlanVersion:@(1)
+ uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
+ XCTAssertNotNil(upload, @"Upload should succeed; non-JSON values are dropped.");
+
+ NSDictionary *serialized = [upload dictionaryRepresentation];
+ XCTAssertEqualObjects(serialized[@"valid"], @"ok");
+ XCTAssertNil(serialized[@"date"]);
+ XCTAssertNil(serialized[@"url"]);
+}
+
+- (void)testUploadSerializationReturnsNilForInvalidData {
+ // An upload dictionary that cannot produce valid JSON should return nil
+ double four = 4.0;
+ double zed = 0.0;
+ NSDictionary *uploadDictionary = @{
+ kMPMessageIdKey:[[NSUUID UUID] UUIDString],
+ kMPTimestampKey:@(four/zed), // NaN timestamp makes the whole upload invalid
+ kMPMessagesKey:@[]
+ };
+
+ MPUpload *upload = [[MPUpload alloc] initWithSessionId:@1
+ uploadDictionary:uploadDictionary
+ dataPlanId:@"test"
+ dataPlanVersion:@(1)
+ uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
+ XCTAssertNil(upload, @"Upload should be nil when JSON serialization fails.");
+}
+
- (void)testBreadcrumbInstance {
MPSession *session = [[MPSession alloc] initWithStartTime:[[NSDate date] timeIntervalSince1970] userId:[MPPersistenceController_PRIVATE mpId]];
diff --git a/UnitTests/ObjCTests/MPIdentityTests.m b/UnitTests/ObjCTests/MPIdentityTests.m
index cfb8497ba..0ee6fd2d8 100644
--- a/UnitTests/ObjCTests/MPIdentityTests.m
+++ b/UnitTests/ObjCTests/MPIdentityTests.m
@@ -35,7 +35,7 @@ - (void)setUserId:(NSNumber *)userId;
@interface MPIdentityApi ()
@property (nonatomic, strong) MPIdentityApiManager *apiManager;
-@property(nonatomic, strong, readwrite, nonnull) MParticleUser *currentUser;
+@property(strong, readwrite, nonnull) MParticleUser *currentUser;
- (void)onIdentityRequestComplete:(MPIdentityApiRequest *)request identityRequestType:(MPIdentityRequestType)identityRequestType httpResponse:(MPIdentityHTTPSuccessResponse *) httpResponse completion:(MPIdentityApiResultCallback)completion error: (NSError *) error;
- (void)onModifyRequestComplete:(MPIdentityApiRequest *)request httpResponse:(MPIdentityHTTPModifySuccessResponse *) httpResponse completion:(MPModifyApiResultCallback)completion error: (NSError *) error;
diff --git a/UnitTests/ObjCTests/MPKitContainerTests.m b/UnitTests/ObjCTests/MPKitContainerTests.m
index 4415810aa..e19d39792 100644
--- a/UnitTests/ObjCTests/MPKitContainerTests.m
+++ b/UnitTests/ObjCTests/MPKitContainerTests.m
@@ -71,6 +71,8 @@ - (MPKitFilter *)filter:(id)kitRegister forUserAttribute
- (MPKitFilter *)filter:(id)kitRegister forUserIdentityKey:(NSString *)key identityType:(MPUserIdentity)identityType;
- (MPKitFilter *)filter:(id)kitRegister forCommerceEvent:(MPCommerceEvent *const)commerceEvent;
- (void)attemptToLogEventToKit:(id)kitRegister kitFilter:(MPKitFilter *)kitFilter selector:(SEL)selector parameters:(nullable MPForwardQueueParameters *)parameters messageType:(MPMessageType)messageType userInfo:(NSDictionary *)userInfo;
+- (id)bracketForKit:(NSNumber *)integrationId;
+- (void)updateBracketsWithConfiguration:(NSDictionary *)configuration integrationId:(NSNumber *)integrationId;
@end
@@ -3365,4 +3367,151 @@ - (void)testFilterCommerceEvent_TransactionAttributesForSideloadedKit {
#endif
+#pragma mark - Thread Safety Tests
+
+- (void)testActiveKitsRegistryThreadSafety {
+ // This stress test verifies that activeKitsRegistry doesn't crash when
+ // called concurrently with configureKits: modifications.
+ // Race conditions are non-deterministic, so this test increases the
+ // likelihood of catching issues but cannot guarantee detection.
+
+ MPKitContainer_PRIVATE *kitContainer = [[MPKitContainer_PRIVATE alloc] init];
+
+ NSArray *configurations = @[
+ @{
+ @"id": @42,
+ @"as": @{
+ @"appId": @"MyAppId"
+ }
+ },
+ @{
+ @"id": @314,
+ @"as": @{
+ @"appId": @"MyAppId"
+ }
+ }
+ ];
+
+ // Initial configuration
+ [kitContainer configureKits:nil];
+ [kitContainer configureKits:configurations];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Thread safety stress test"];
+
+ dispatch_group_t group = dispatch_group_create();
+ dispatch_queue_t concurrentQueue = dispatch_queue_create("com.mparticle.test.concurrent", DISPATCH_QUEUE_CONCURRENT);
+
+ NSInteger iterations = 100;
+ __block BOOL encounteredError = NO;
+
+ // Multiple threads reading activeKitsRegistry
+ for (NSInteger i = 0; i < 3; i++) {
+ dispatch_group_async(group, concurrentQueue, ^{
+ for (NSInteger j = 0; j < iterations && !encounteredError; j++) {
+ @try {
+ NSArray *activeKits = [kitContainer activeKitsRegistry];
+ // Access the returned array to ensure objects are valid
+ for (id kit in activeKits) {
+ (void)kit.code;
+ }
+ } @catch (NSException *exception) {
+ encounteredError = YES;
+ XCTFail(@"Exception in activeKitsRegistry: %@", exception);
+ }
+ }
+ });
+ }
+
+ // Thread modifying kits configuration
+ dispatch_group_async(group, concurrentQueue, ^{
+ for (NSInteger j = 0; j < iterations && !encounteredError; j++) {
+ @try {
+ [kitContainer configureKits:nil];
+ [kitContainer configureKits:configurations];
+ } @catch (NSException *exception) {
+ encounteredError = YES;
+ XCTFail(@"Exception in configureKits: %@", exception);
+ }
+ }
+ });
+
+ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
+ XCTAssertFalse(encounteredError, @"Thread safety test should complete without errors");
+ [expectation fulfill];
+ });
+
+ [self waitForExpectationsWithTimeout:30 handler:nil];
+}
+
+- (void)testBracketForKitThreadSafety {
+ // This stress test verifies that bracketForKit: doesn't crash when
+ // called concurrently with updateBracketsWithConfiguration: modifications.
+ // Race conditions are non-deterministic, so this test increases the
+ // likelihood of catching issues but cannot guarantee detection.
+
+ MPKitContainer_PRIVATE *kitContainer = [[MPKitContainer_PRIVATE alloc] init];
+
+ NSArray *integrationIds = @[@42, @314, @123, @456];
+ NSDictionary *bracketConfig = @{
+ @"lo": @0,
+ @"hi": @100
+ };
+
+ // Initialize some brackets
+ for (NSNumber *integrationId in integrationIds) {
+ [kitContainer updateBracketsWithConfiguration:bracketConfig integrationId:integrationId];
+ }
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Bracket thread safety stress test"];
+
+ dispatch_group_t group = dispatch_group_create();
+ dispatch_queue_t concurrentQueue = dispatch_queue_create("com.mparticle.test.brackets", DISPATCH_QUEUE_CONCURRENT);
+
+ NSInteger iterations = 100;
+ __block BOOL encounteredError = NO;
+
+ // Multiple threads reading brackets
+ for (NSInteger i = 0; i < 3; i++) {
+ dispatch_group_async(group, concurrentQueue, ^{
+ for (NSInteger j = 0; j < iterations && !encounteredError; j++) {
+ @try {
+ for (NSNumber *integrationId in integrationIds) {
+ id bracket = [kitContainer bracketForKit:integrationId];
+ (void)bracket; // Use the result to prevent optimization
+ }
+ } @catch (NSException *exception) {
+ encounteredError = YES;
+ XCTFail(@"Exception in bracketForKit: %@", exception);
+ }
+ }
+ });
+ }
+
+ // Thread modifying brackets
+ dispatch_group_async(group, concurrentQueue, ^{
+ for (NSInteger j = 0; j < iterations && !encounteredError; j++) {
+ @try {
+ for (NSNumber *integrationId in integrationIds) {
+ // Alternate between adding and removing brackets
+ if (j % 2 == 0) {
+ [kitContainer updateBracketsWithConfiguration:bracketConfig integrationId:integrationId];
+ } else {
+ [kitContainer updateBracketsWithConfiguration:nil integrationId:integrationId];
+ }
+ }
+ } @catch (NSException *exception) {
+ encounteredError = YES;
+ XCTFail(@"Exception in updateBracketsWithConfiguration: %@", exception);
+ }
+ }
+ });
+
+ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
+ XCTAssertFalse(encounteredError, @"Thread safety test should complete without errors");
+ [expectation fulfill];
+ });
+
+ [self waitForExpectationsWithTimeout:30 handler:nil];
+}
+
@end
diff --git a/UnitTests/ObjCTests/MPNetworkCommunicationTests.m b/UnitTests/ObjCTests/MPNetworkCommunicationTests.m
index d0ff043e4..cf64c6c1b 100644
--- a/UnitTests/ObjCTests/MPNetworkCommunicationTests.m
+++ b/UnitTests/ObjCTests/MPNetworkCommunicationTests.m
@@ -31,9 +31,16 @@ @interface MPNetworkCommunication_PRIVATE ()
- (NSNumber *)maxAgeForCache:(nonnull NSString *)cache;
- (BOOL)performMessageUpload:(MPUpload *)upload;
- (BOOL)performAliasUpload:(MPUpload *)upload;
+- (UIBackgroundTaskIdentifier)beginSafeBackgroundTaskWithExpirationHandler:(void(^_Nullable)(void))handler;
+- (void)endSafeBackgroundTask:(UIBackgroundTaskIdentifier)taskId;
+@property (nonatomic) BOOL identifying;
@end
+@interface MPApplication_PRIVATE(Tests)
++ (void)setMockApplication:(id)mockApplication;
+@end
+
@interface MPNetworkCommunicationTests : MPBaseTestCase
@end
@@ -44,10 +51,10 @@ @implementation MPNetworkCommunicationTests
- (void)setUp {
[super setUp];
-
+
[MParticle sharedInstance].stateMachine.apiKey = @"unit_test_app_key";
[MParticle sharedInstance].stateMachine.secret = @"unit_test_secret";
-
+
[MParticle sharedInstance].backendController = [[MPBackendController_PRIVATE alloc] initWithDelegate:(id)[MParticle sharedInstance]];
}
@@ -71,23 +78,23 @@ - (NSDictionary *)infoDictionary {
- (void)testAudienceURL {
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *audienceURL = [networkCommunication audienceURL].url;
-
+
[self deswizzle];
-
+
XCTAssert([audienceURL.absoluteString rangeOfString:@"/unit_test_app_key/audience?mpid=0"].location != NSNotFound);
}
- (void)testConfigURL {
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *configURL = [networkCommunication configURL].url;
-
+
[self deswizzle];
-
+
XCTAssert([configURL.absoluteString rangeOfString:@"/config?av=1.2.3.4.5678%20(bd12345ff)"].location != NSNotFound);
XCTAssert(![configURL.accessibilityHint isEqualToString:@"identity"]);
}
@@ -99,9 +106,9 @@ - (void)testConfigURLWithOptions {
[MParticle sharedInstance].networkOptions = options;
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *configURL = [networkCommunication configURL].url;
-
+
[self deswizzle];
-
+
XCTAssert([configURL.absoluteString rangeOfString:@"config.mpproxy.example.com/v4/"].location != NSNotFound);
XCTAssert(![configURL.accessibilityHint isEqualToString:@"identity"]);
}
@@ -114,9 +121,9 @@ - (void)testConfigURLWithOptionsAndOverride {
[MParticle sharedInstance].networkOptions = options;
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *configURL = [networkCommunication configURL].url;
-
+
[self deswizzle];
-
+
XCTAssert([configURL.absoluteString rangeOfString:@"config.mpproxy.example.com"].location != NSNotFound);
XCTAssert([configURL.absoluteString rangeOfString:@"v4"].location == NSNotFound);
XCTAssert(![configURL.accessibilityHint isEqualToString:@"identity"]);
@@ -124,12 +131,12 @@ - (void)testConfigURLWithOptionsAndOverride {
- (void)testModifyURL {
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *modifyURL = [networkCommunication modifyURL].url;
-
+
[self deswizzle];
-
+
XCTAssert([modifyURL.absoluteString rangeOfString:@"https://identity.us1.mparticle.com/v1/0/modify"].location != NSNotFound);
XCTAssert([modifyURL.accessibilityHint isEqualToString:@"identity"]);
}
@@ -139,12 +146,12 @@ - (void)testModifyURLWithOptions {
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
options.identityHost = @"identity.mpproxy.example.com";
[MParticle sharedInstance].networkOptions = options;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *modifyURL = [networkCommunication modifyURL].url;
-
+
[self deswizzle];
-
+
XCTAssert([modifyURL.absoluteString rangeOfString:@"https://identity.mpproxy.example.com/v1/0/modify"].location != NSNotFound);
XCTAssert([modifyURL.accessibilityHint isEqualToString:@"identity"]);
}
@@ -153,17 +160,17 @@ - (void)testModifyURLWithOptionsAndTrackingOverride {
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
MPStateMachine_PRIVATE *stateMachine = [MParticle sharedInstance].stateMachine;
stateMachine.attAuthorizationStatus = @(MPATTAuthorizationStatusAuthorized);
-
+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
options.identityHost = @"identity.mpproxy.example.com";
options.identityTrackingHost = @"identity-tracking.mpproxy.example.com";
[MParticle sharedInstance].networkOptions = options;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *modifyURL = [networkCommunication modifyURL].url;
-
+
[self deswizzle];
-
+
XCTAssert([modifyURL.absoluteString rangeOfString:@"https://identity-tracking.mpproxy.example.com/v1/0/modify"].location != NSNotFound);
XCTAssert([modifyURL.accessibilityHint isEqualToString:@"identity"]);
}
@@ -172,19 +179,19 @@ - (void)testEventURLWithOptionsAndOverrideAndEventsOnlyAndTrackingHost {
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
MPStateMachine_PRIVATE *stateMachine = [MParticle sharedInstance].stateMachine;
stateMachine.attAuthorizationStatus = @(MPATTAuthorizationStatusAuthorized);
-
+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
options.eventsHost = @"events.mpproxy.example.com";
options.eventsTrackingHost = @"events-tracking.mpproxy.example.com";
options.eventsOnly = true;
[MParticle sharedInstance].networkOptions = options;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSURL *eventURL = [networkCommunication eventURLForUpload:upload].url;
-
+
[self deswizzle];
-
+
XCTAssert([eventURL.absoluteString rangeOfString:@"https://events-tracking.mpproxy.example.com/"].location != NSNotFound);
XCTAssert([eventURL.absoluteString rangeOfString:@"v1"].location == NSNotFound);
XCTAssert([eventURL.absoluteString rangeOfString:@"identity"].location == NSNotFound);
@@ -193,13 +200,13 @@ - (void)testEventURLWithOptionsAndOverrideAndEventsOnlyAndTrackingHost {
- (void)testAliasURL {
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSURL *aliasURL = [networkCommunication aliasURLForUpload:upload].url;
-
+
[self deswizzle];
-
+
XCTAssert([aliasURL.absoluteString rangeOfString:@"https://nativesdks.us1.mparticle.com/v1/identity/"].location != NSNotFound);
XCTAssert([aliasURL.accessibilityHint isEqualToString:@"identity"]);
}
@@ -209,13 +216,13 @@ - (void)testAliasURLWithOptions {
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
options.eventsHost = @"events.mpproxy.example.com";
[MParticle sharedInstance].networkOptions = options;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSURL *aliasURL = [networkCommunication aliasURLForUpload:upload].url;
-
+
[self deswizzle];
-
+
XCTAssert([aliasURL.absoluteString rangeOfString:@"https://events.mpproxy.example.com/v1/identity/"].location != NSNotFound);
XCTAssert([aliasURL.accessibilityHint isEqualToString:@"identity"]);
}
@@ -226,13 +233,13 @@ - (void)testAliasURLWithOptionsAndOverride {
options.eventsHost = @"events.mpproxy.example.com";
options.overridesEventsSubdirectory = true;
[MParticle sharedInstance].networkOptions = options;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSURL *aliasURL = [networkCommunication aliasURLForUpload:upload].url;
-
+
[self deswizzle];
-
+
XCTAssert([aliasURL.absoluteString rangeOfString:@"https://events.mpproxy.example.com/"].location != NSNotFound);
XCTAssert([aliasURL.absoluteString rangeOfString:@"v1"].location == NSNotFound);
XCTAssert([aliasURL.absoluteString rangeOfString:@"identity"].location == NSNotFound);
@@ -245,13 +252,13 @@ - (void)testAliasURLWithEventsOnly {
options.eventsHost = @"events.mpproxy.example.com";
options.eventsOnly = true;
[MParticle sharedInstance].networkOptions = options;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSURL *aliasURL = [networkCommunication aliasURLForUpload:upload].url;
-
+
[self deswizzle];
-
+
XCTAssert([aliasURL.absoluteString rangeOfString:@"https://nativesdks.us1.mparticle.com/v1/identity/"].location != NSNotFound);
XCTAssert([aliasURL.accessibilityHint isEqualToString:@"identity"]);
}
@@ -263,13 +270,13 @@ - (void)testAliasURLWithOptionsAndEventsOnly {
options.aliasHost = @"alias.mpproxy.example.com";
options.eventsOnly = true;
[MParticle sharedInstance].networkOptions = options;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSURL *aliasURL = [networkCommunication aliasURLForUpload:upload].url;
-
+
[self deswizzle];
-
+
XCTAssert([aliasURL.absoluteString rangeOfString:@"https://alias.mpproxy.example.com/v1/identity/"].location != NSNotFound);
XCTAssert([aliasURL.accessibilityHint isEqualToString:@"identity"]);
}
@@ -282,13 +289,13 @@ - (void)testAliasURLWithOptionsAndOverrideAndEventsOnly {
options.overridesAliasSubdirectory = true;
options.eventsOnly = true;
[MParticle sharedInstance].networkOptions = options;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSURL *aliasURL = [networkCommunication aliasURLForUpload:upload].url;
-
+
[self deswizzle];
-
+
XCTAssert([aliasURL.absoluteString rangeOfString:@"https://alias.mpproxy.example.com/"].location != NSNotFound);
XCTAssert([aliasURL.absoluteString rangeOfString:@"v1"].location == NSNotFound);
XCTAssert([aliasURL.absoluteString rangeOfString:@"identity"].location == NSNotFound);
@@ -299,7 +306,7 @@ - (void)testAliasURLWithOptionsAndOverrideAndEventsOnlyAndTrackingHost {
[self swizzleInstanceMethodForInstancesOfClass:[NSBundle class] selector:@selector(infoDictionary)];
MPStateMachine_PRIVATE *stateMachine = [MParticle sharedInstance].stateMachine;
stateMachine.attAuthorizationStatus = @(MPATTAuthorizationStatusAuthorized);
-
+
MPNetworkOptions *options = [[MPNetworkOptions alloc] init];
options.eventsHost = @"events.mpproxy.example.com";
options.eventsTrackingHost = @"events-tracking.mpproxy.example.com";
@@ -308,13 +315,13 @@ - (void)testAliasURLWithOptionsAndOverrideAndEventsOnlyAndTrackingHost {
options.overridesAliasSubdirectory = true;
options.eventsOnly = true;
[MParticle sharedInstance].networkOptions = options;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:nil uploadDictionary:@{} dataPlanId:nil dataPlanVersion:nil uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSURL *aliasURL = [networkCommunication aliasURLForUpload:upload].url;
-
+
[self deswizzle];
-
+
XCTAssert([aliasURL.absoluteString rangeOfString:@"https://alias-tracking.mpproxy.example.com/"].location != NSNotFound);
XCTAssert([aliasURL.absoluteString rangeOfString:@"v1"].location == NSNotFound);
XCTAssert([aliasURL.absoluteString rangeOfString:@"identity"].location == NSNotFound);
@@ -346,7 +353,7 @@ - (void)testUploadsArrayZipFail {
- (void)testUploadsArrayZipSucceedWithATTNotDetermined {
[[MParticle sharedInstance] setATTStatus:MPATTAuthorizationStatusNotDetermined withATTStatusTimestampMillis:nil];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{kMPDeviceInformationKey: @{}} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSArray *uploads = @[upload];
@@ -355,7 +362,7 @@ - (void)testUploadsArrayZipSucceedWithATTNotDetermined {
NSMutableDictionary *uploadDict = [NSJSONSerialization JSONObjectWithData:value options:0 error:nil];
return ([uploadDict[kMPDeviceInformationKey][kMPATT] isEqual: @"not_determined"]);
}]];
-
+
[networkCommunication upload:uploads completionHandler:^{
}];
[mockZip verifyWithDelay:2];
@@ -363,7 +370,7 @@ - (void)testUploadsArrayZipSucceedWithATTNotDetermined {
- (void)testUploadsArrayZipSucceedWithATTRestricted {
[[MParticle sharedInstance] setATTStatus:MPATTAuthorizationStatusRestricted withATTStatusTimestampMillis:nil];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{kMPDeviceInformationKey: @{}} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSArray *uploads = @[upload];
@@ -372,7 +379,7 @@ - (void)testUploadsArrayZipSucceedWithATTRestricted {
NSMutableDictionary *uploadDict = [NSJSONSerialization JSONObjectWithData:value options:0 error:nil];
return ([uploadDict[kMPDeviceInformationKey][kMPATT] isEqual: @"restricted"]);
}]];
-
+
[networkCommunication upload:uploads completionHandler:^{
}];
[mockZip verifyWithDelay:2];
@@ -380,7 +387,7 @@ - (void)testUploadsArrayZipSucceedWithATTRestricted {
- (void)testUploadsArrayZipSucceedWithATTDenied {
[[MParticle sharedInstance] setATTStatus:MPATTAuthorizationStatusDenied withATTStatusTimestampMillis:nil];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{kMPDeviceInformationKey: @{}} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSArray *uploads = @[upload];
@@ -389,7 +396,7 @@ - (void)testUploadsArrayZipSucceedWithATTDenied {
NSMutableDictionary *uploadDict = [NSJSONSerialization JSONObjectWithData:value options:0 error:nil];
return ([uploadDict[kMPDeviceInformationKey][kMPATT] isEqual: @"denied"]);
}]];
-
+
[networkCommunication upload:uploads completionHandler:^{
}];
[mockZip verifyWithDelay:2];
@@ -397,7 +404,7 @@ - (void)testUploadsArrayZipSucceedWithATTDenied {
- (void)testUploadsArrayZipSucceedWithATTAuthorized {
[[MParticle sharedInstance] setATTStatus:MPATTAuthorizationStatusAuthorized withATTStatusTimestampMillis:nil];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPUpload *upload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{kMPDeviceInformationKey: @{}} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
NSArray *uploads = @[upload];
@@ -406,7 +413,7 @@ - (void)testUploadsArrayZipSucceedWithATTAuthorized {
NSMutableDictionary *uploadDict = [NSJSONSerialization JSONObjectWithData:value options:0 error:nil];
return ([uploadDict[kMPDeviceInformationKey][kMPATT] isEqual: @"authorized"]);
}]];
-
+
[networkCommunication upload:uploads completionHandler:^{
}];
[mockZip verifyWithDelay:2];
@@ -430,19 +437,19 @@ - (void)testShouldStopEvents {
- (void)shouldStopEvents:(int)returnCode shouldStop:(BOOL)shouldStop {
id urlResponseMock = OCMClassMock([NSHTTPURLResponse class]);
[[[urlResponseMock stub] andReturnValue:OCMOCK_VALUE(returnCode)] statusCode];
-
+
MPConnectorResponse *response = [[MPConnectorResponse alloc] init];
response.httpResponse = urlResponseMock;
-
+
id mockConnector = OCMClassMock([MPConnector class]);
[[[mockConnector stub] andReturn:response] responseFromPostRequestToURL:OCMOCK_ANY message:OCMOCK_ANY serializedParams:OCMOCK_ANY secret:OCMOCK_ANY];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
id mockNetworkCommunication = OCMPartialMock(networkCommunication);
[[[mockNetworkCommunication stub] andReturn:mockConnector] makeConnector];
-
+
MPUpload *messageUpload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
-
+
BOOL actualShouldStop = [networkCommunication performMessageUpload:messageUpload];
XCTAssertEqual(shouldStop, actualShouldStop, @"Return code assertion: %d", returnCode);
}
@@ -461,24 +468,24 @@ - (void)testShouldStopAlias {
[self shouldStopAlias:500 shouldStop:YES];
[self shouldStopAlias:503 shouldStop:YES];
}
-
+
- (void)shouldStopAlias:(int)returnCode shouldStop:(BOOL)shouldStop {
id urlResponseMock = OCMClassMock([NSHTTPURLResponse class]);
[[[urlResponseMock stub] andReturnValue:OCMOCK_VALUE(returnCode)] statusCode];
-
+
MPConnectorResponse *response = [[MPConnectorResponse alloc] init];
response.httpResponse = urlResponseMock;
-
+
id mockConnector = OCMClassMock([MPConnector class]);
[[[mockConnector stub] andReturn:response] responseFromPostRequestToURL:OCMOCK_ANY message:OCMOCK_ANY serializedParams:OCMOCK_ANY secret:OCMOCK_ANY];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
id mockNetworkCommunication = OCMPartialMock(networkCommunication);
[[[mockNetworkCommunication stub] andReturn:mockConnector] makeConnector];
-
+
MPUpload *aliasUpload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
aliasUpload.uploadType = MPUploadTypeAlias;
-
+
BOOL actualShouldStop = [networkCommunication performAliasUpload:aliasUpload];
XCTAssertEqual(shouldStop, actualShouldStop, @"Return code assertion: %d", returnCode);
}
@@ -486,27 +493,27 @@ - (void)shouldStopAlias:(int)returnCode shouldStop:(BOOL)shouldStop {
- (void)testOfflineUpload {
id urlResponseMock = OCMClassMock([NSHTTPURLResponse class]);
[[[urlResponseMock stub] andReturnValue:OCMOCK_VALUE(0)] statusCode];
-
+
MPConnectorResponse *response = [[MPConnectorResponse alloc] init];
response.httpResponse = urlResponseMock;
-
+
id mockConnector = OCMClassMock([MPConnector class]);
[[[mockConnector stub] andReturn:response] responseFromPostRequestToURL:OCMOCK_ANY message:OCMOCK_ANY serializedParams:OCMOCK_ANY secret:OCMOCK_ANY];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
id mockNetworkCommunication = OCMPartialMock(networkCommunication);
[[[mockNetworkCommunication stub] andReturn:mockConnector] makeConnector];
-
+
id mockPersistenceController = OCMClassMock([MPPersistenceController_PRIVATE class]);
[[mockPersistenceController reject] deleteUpload:OCMOCK_ANY];
-
+
MParticle *instance = [MParticle sharedInstance];
instance.persistenceController = mockPersistenceController;
-
+
MPUpload *eventUpload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
MPUpload *aliasUpload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
aliasUpload.uploadType = MPUploadTypeAlias;
-
+
NSArray *uploads = @[eventUpload, aliasUpload];
XCTestExpectation *expectation = [self expectationWithDescription:@"async work"];
[networkCommunication upload:uploads completionHandler:^{
@@ -518,26 +525,26 @@ - (void)testOfflineUpload {
- (void)testUploadSuccessDeletion {
id urlResponseMock = OCMClassMock([NSHTTPURLResponse class]);
[[[urlResponseMock stub] andReturnValue:OCMOCK_VALUE(202)] statusCode];
-
+
MPConnectorResponse *response = [[MPConnectorResponse alloc] init];
response.httpResponse = urlResponseMock;
-
+
id mockConnector = OCMClassMock([MPConnector class]);
[[[mockConnector stub] andReturn:response] responseFromPostRequestToURL:OCMOCK_ANY message:OCMOCK_ANY serializedParams:OCMOCK_ANY secret:OCMOCK_ANY];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
id mockNetworkCommunication = OCMPartialMock(networkCommunication);
[[[mockNetworkCommunication stub] andReturn:mockConnector] makeConnector];
-
+
id mockPersistenceController = OCMClassMock([MPPersistenceController_PRIVATE class]);
-
+
MPUpload *eventUpload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{kMPDeviceInformationKey: @{}} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
MPUpload *aliasUpload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
aliasUpload.uploadType = MPUploadTypeAlias;
-
+
[[mockPersistenceController expect] deleteUpload:eventUpload];
[[mockPersistenceController expect] deleteUpload:aliasUpload];
-
+
MParticle *instance = [MParticle sharedInstance];
id mockInstance = OCMPartialMock(instance);
[(MParticle *)[mockInstance expect] logKitBatch:[OCMArg checkWithBlock:^BOOL(id obj) {
@@ -547,7 +554,7 @@ - (void)testUploadSuccessDeletion {
return NO;
}]];
((MParticle *)mockInstance).persistenceController = mockPersistenceController;
-
+
NSArray *uploads = @[eventUpload, aliasUpload];
XCTestExpectation *expectation = [self expectationWithDescription:@"async work"];
[networkCommunication upload:uploads completionHandler:^{
@@ -560,33 +567,33 @@ - (void)testUploadSuccessDeletion {
- (void)testUploadInvalidDeletion {
id urlResponseMock = OCMClassMock([NSHTTPURLResponse class]);
[[[urlResponseMock stub] andReturnValue:OCMOCK_VALUE(400)] statusCode];
-
+
MPConnectorResponse *response = [[MPConnectorResponse alloc] init];
response.httpResponse = urlResponseMock;
-
+
id mockConnector = OCMClassMock([MPConnector class]);
[[[mockConnector stub] andReturn:response] responseFromPostRequestToURL:OCMOCK_ANY message:OCMOCK_ANY serializedParams:OCMOCK_ANY secret:OCMOCK_ANY];
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
id mockNetworkCommunication = OCMPartialMock(networkCommunication);
[[[mockNetworkCommunication stub] andReturn:mockConnector] makeConnector];
-
+
id mockPersistenceController = OCMClassMock([MPPersistenceController_PRIVATE class]);
-
+
MPUpload *eventUpload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
MPUpload *aliasUpload = [[MPUpload alloc] initWithSessionId:@1 uploadDictionary:@{} dataPlanId:@"test" dataPlanVersion:@(1) uploadSettings:[MPUploadSettings currentUploadSettingsWithStateMachine:[MParticle sharedInstance].stateMachine networkOptions:[MParticle sharedInstance].networkOptions]];
aliasUpload.uploadType = MPUploadTypeAlias;
-
+
[[mockPersistenceController expect] deleteUpload:eventUpload];
[[mockPersistenceController expect] deleteUpload:aliasUpload];
-
+
MParticle *instance = [MParticle sharedInstance];
id mockInstance = OCMPartialMock(instance);
[(MParticle *)[mockInstance expect] logKitBatch:[OCMArg checkWithBlock:^BOOL(id obj) {
return NO; // reject
}]];
((MParticle *)mockInstance).persistenceController = mockPersistenceController;
-
+
NSArray *uploads = @[eventUpload, aliasUpload];
XCTestExpectation *expectation = [self expectationWithDescription:@"async work"];
[networkCommunication upload:uploads completionHandler:^{
@@ -599,16 +606,16 @@ - (void)testRequestConfigWithDefaultMaxAge {
MPUserDefaults *userDefaults = MPUserDefaultsConnector.userDefaults;
NSNumber *configProvisioned = userDefaults[kMPConfigProvisionedTimestampKey];
NSNumber *maxAge = userDefaults[kMPConfigMaxAgeHeaderKey];
-
+
XCTAssertEqualObjects(configProvisioned, nil);
XCTAssertEqualObjects(maxAge, nil);
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *configURL = [networkCommunication configURL].url;
-
+
MPConnector *connector = [[MPConnector alloc] init];
id mockConnector = OCMPartialMock(connector);
-
+
NSDictionary *httpHeaders = @{@"Age": @"0",
kMPHTTPETagHeaderKey: @"242f22f24c224"
};
@@ -618,39 +625,39 @@ - (void)testRequestConfigWithDefaultMaxAge {
@"appId":@"cool app key"
}
};
-
+
NSDictionary *configuration2 = @{
@"id":@312,
@"as":@{
@"appId":@"cool app key 2"
}
};
-
+
NSArray *kitConfigs = @[configuration1, configuration2];
-
+
NSDictionary *responseConfiguration = @{kMPRemoteConfigKitsKey:kitConfigs,
kMPMessageTypeKey:kMPMessageTypeConfig,
kMPRemoteConfigRampKey:@100,
kMPRemoteConfigExceptionHandlingModeKey:kMPRemoteConfigExceptionHandlingModeForce,
kMPRemoteConfigSessionTimeoutKey:@112};
NSError *error;
-
+
MPConnectorResponse *response = [[MPConnectorResponse alloc] init];
NSHTTPURLResponse *httpResponse = [[NSHTTPURLResponse alloc] initWithURL:configURL statusCode:HTTPStatusCodeSuccess HTTPVersion:@"" headerFields:httpHeaders];
response.httpResponse = httpResponse;
response.data = [NSJSONSerialization dataWithJSONObject:responseConfiguration
options:NSJSONWritingPrettyPrinted
error:&error];
-
+
[[[mockConnector stub] andReturn:response] responseFromGetRequestToURL:[networkCommunication configURL]];
[networkCommunication requestConfig:mockConnector withCompletionHandler:^(BOOL success) {
XCTAssert(success);
}];
-
+
configProvisioned = userDefaults[kMPConfigProvisionedTimestampKey];
maxAge = userDefaults[kMPConfigMaxAgeHeaderKey];
-
+
XCTAssertNotNil(configProvisioned);
XCTAssertNil(maxAge);
}
@@ -658,13 +665,13 @@ - (void)testRequestConfigWithDefaultMaxAge {
- (void)testRequestConfigWithManualMaxAge {
MPUserDefaults *userDefaults = MPUserDefaultsConnector.userDefaults;
userDefaults[kMPConfigProvisionedTimestampKey] = @5555;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *configURL = [networkCommunication configURL].url;
MPConnector *connector = [[MPConnector alloc] init];
id mockConnector = OCMPartialMock(connector);
-
+
NSDictionary *httpHeaders = @{@"Age": @"0",
kMPHTTPETagHeaderKey: @"242f22f24c224",
kMPHTTPCacheControlHeaderKey: @"max-age=43200"
@@ -675,36 +682,36 @@ - (void)testRequestConfigWithManualMaxAge {
@"appId":@"cool app key"
}
};
-
+
NSDictionary *configuration2 = @{
@"id":@312,
@"as":@{
@"appId":@"cool app key 2"
}
};
-
+
NSArray *kitConfigs = @[configuration1, configuration2];
-
+
NSDictionary *responseConfiguration = @{kMPRemoteConfigKitsKey:kitConfigs,
kMPMessageTypeKey:kMPMessageTypeConfig,
kMPRemoteConfigRampKey:@100,
kMPRemoteConfigExceptionHandlingModeKey:kMPRemoteConfigExceptionHandlingModeForce,
kMPRemoteConfigSessionTimeoutKey:@112};
NSError *error;
-
+
MPConnectorResponse *response = [[MPConnectorResponse alloc] init];
NSHTTPURLResponse *httpResponse = [[NSHTTPURLResponse alloc] initWithURL:configURL statusCode:HTTPStatusCodeSuccess HTTPVersion:@"" headerFields:httpHeaders];
response.httpResponse = httpResponse;
response.data = [NSJSONSerialization dataWithJSONObject:responseConfiguration
options:NSJSONWritingPrettyPrinted
error:&error];
-
+
[[[mockConnector stub] andReturn:response] responseFromGetRequestToURL:[networkCommunication configURL]];
[networkCommunication requestConfig:mockConnector withCompletionHandler:^(BOOL success) {
XCTAssert(success);
}];
-
+
NSNumber *maxAge = userDefaults[kMPConfigMaxAgeHeaderKey];
XCTAssertEqualObjects(maxAge, @43200);
@@ -713,39 +720,39 @@ - (void)testRequestConfigWithManualMaxAge {
- (void)testRequestConfigWithManualMaxAgeAndInitialAge {
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *configURL = [networkCommunication configURL].url;
-
+
MPConnector *connector = [[MPConnector alloc] init];
id mockConnector = OCMPartialMock(connector);
-
+
NSDictionary *httpHeaders = @{@"age": @"4000",
kMPMessageTypeKey:kMPMessageTypeConfig,
kMPHTTPETagHeaderKey: @"242f22f24c224",
kMPHTTPCacheControlHeaderKey: @"max-age=43200"
};
-
+
NSDictionary *configuration1 = @{
@"id":@42,
@"as":@{
@"appId":@"cool app key"
}
};
-
+
NSDictionary *configuration2 = @{
@"id":@312,
@"as":@{
@"appId":@"cool app key 2"
}
};
-
+
NSArray *kitConfigs = @[configuration1, configuration2];
-
+
NSDictionary *responseConfiguration = @{kMPRemoteConfigKitsKey:kitConfigs,
kMPRemoteConfigRampKey:@100,
kMPMessageTypeKey:kMPMessageTypeConfig,
kMPRemoteConfigExceptionHandlingModeKey:kMPRemoteConfigExceptionHandlingModeForce,
kMPRemoteConfigSessionTimeoutKey:@112};
NSError *error;
-
+
MPConnectorResponse *response = [[MPConnectorResponse alloc] init];
NSHTTPURLResponse *httpResponse = [[NSHTTPURLResponse alloc] initWithURL:configURL statusCode:HTTPStatusCodeSuccess HTTPVersion:@"" headerFields:httpHeaders];
response.httpResponse = httpResponse;
@@ -753,19 +760,19 @@ - (void)testRequestConfigWithManualMaxAgeAndInitialAge {
options:NSJSONWritingPrettyPrinted
error:&error];
-
+
[[[mockConnector stub] andReturn:response] responseFromGetRequestToURL:[networkCommunication configURL]];
-
+
[networkCommunication requestConfig:mockConnector withCompletionHandler:^(BOOL success) {
XCTAssert(success);
}];
-
+
MPUserDefaults *userDefaults = MPUserDefaultsConnector.userDefaults;
[userDefaults synchronize];
-
+
NSNumber *provisionedInterval = userDefaults[kMPConfigProvisionedTimestampKey];
int approximateAge = ([[NSDate date] timeIntervalSince1970] - [provisionedInterval integerValue]);
-
+
XCTAssertLessThanOrEqual(4000, approximateAge);
XCTAssertLessThan(approximateAge, 4200);
}
@@ -773,13 +780,13 @@ - (void)testRequestConfigWithManualMaxAgeAndInitialAge {
- (void)testRequestConfigWithManualMaxAgeOverMaxAllowed {
MPUserDefaults *userDefaults = MPUserDefaultsConnector.userDefaults;
userDefaults[kMPConfigProvisionedTimestampKey] = @5555;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *configURL = [networkCommunication configURL].url;
-
+
MPConnector *connector = [[MPConnector alloc] init];
id mockConnector = OCMPartialMock(connector);
-
+
NSDictionary *httpHeaders = @{@"Age": @"0",
kMPMessageTypeKey:kMPMessageTypeConfig,
kMPHTTPETagHeaderKey: @"242f22f24c224",
@@ -791,52 +798,52 @@ - (void)testRequestConfigWithManualMaxAgeOverMaxAllowed {
@"appId":@"cool app key"
}
};
-
+
NSDictionary *configuration2 = @{
@"id":@312,
@"as":@{
@"appId":@"cool app key 2"
}
};
-
+
NSArray *kitConfigs = @[configuration1, configuration2];
-
+
NSDictionary *responseConfiguration = @{kMPRemoteConfigKitsKey:kitConfigs,
kMPMessageTypeKey:kMPMessageTypeConfig,
kMPRemoteConfigRampKey:@100,
kMPRemoteConfigExceptionHandlingModeKey:kMPRemoteConfigExceptionHandlingModeForce,
kMPRemoteConfigSessionTimeoutKey:@112};
NSError *error;
-
+
MPConnectorResponse *response = [[MPConnectorResponse alloc] init];
NSHTTPURLResponse *httpResponse = [[NSHTTPURLResponse alloc] initWithURL:configURL statusCode:HTTPStatusCodeSuccess HTTPVersion:@"" headerFields:httpHeaders];
response.httpResponse = httpResponse;
response.data = [NSJSONSerialization dataWithJSONObject:responseConfiguration
options:NSJSONWritingPrettyPrinted
error:&error];
-
+
[[[mockConnector stub] andReturn:response] responseFromGetRequestToURL:[networkCommunication configURL]];
-
+
[networkCommunication requestConfig:mockConnector withCompletionHandler:^(BOOL success) {
XCTAssert(success);
}];
-
+
NSNumber *maxAge = userDefaults[kMPConfigMaxAgeHeaderKey];
NSNumber *maxExpiration = @(60*60*24.0);
-
+
XCTAssertEqualObjects(maxAge, maxExpiration);
}
- (void)testRequestConfigWithComplexCacheControlHeader {
MPUserDefaults *userDefaults = MPUserDefaultsConnector.userDefaults;
userDefaults[kMPConfigProvisionedTimestampKey] = @5555;
-
+
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
NSURL *configURL = [networkCommunication configURL].url;
-
+
MPConnector *connector = [[MPConnector alloc] init];
id mockConnector = OCMPartialMock(connector);
-
+
NSDictionary *httpHeaders = @{@"Age": @"0",
kMPMessageTypeKey:kMPMessageTypeConfig,
kMPHTTPETagHeaderKey: @"242f22f24c224",
@@ -848,38 +855,38 @@ - (void)testRequestConfigWithComplexCacheControlHeader {
@"appId":@"cool app key"
}
};
-
+
NSDictionary *configuration2 = @{
@"id":@312,
@"as":@{
@"appId":@"cool app key 2"
}
};
-
+
NSArray *kitConfigs = @[configuration1, configuration2];
-
+
NSDictionary *responseConfiguration = @{kMPRemoteConfigKitsKey:kitConfigs,
kMPMessageTypeKey:kMPMessageTypeConfig,
kMPRemoteConfigRampKey:@100,
kMPRemoteConfigExceptionHandlingModeKey:kMPRemoteConfigExceptionHandlingModeForce,
kMPRemoteConfigSessionTimeoutKey:@112};
NSError *error;
-
+
MPConnectorResponse *response = [[MPConnectorResponse alloc] init];
NSHTTPURLResponse *httpResponse = [[NSHTTPURLResponse alloc] initWithURL:configURL statusCode:HTTPStatusCodeSuccess HTTPVersion:@"" headerFields:httpHeaders];
response.httpResponse = httpResponse;
response.data = [NSJSONSerialization dataWithJSONObject:responseConfiguration
options:NSJSONWritingPrettyPrinted
error:&error];
-
+
[[[mockConnector stub] andReturn:response] responseFromGetRequestToURL:[networkCommunication configURL]];
-
+
[networkCommunication requestConfig:mockConnector withCompletionHandler:^(BOOL success) {
XCTAssert(success);
}];
-
+
NSNumber *maxAge = userDefaults[kMPConfigMaxAgeHeaderKey];
-
+
XCTAssertEqualObjects(maxAge, @43200);
}
@@ -892,35 +899,35 @@ - (void)testMaxAgeForCacheEmptyString {
- (void)testMaxAgeForCacheSimple {
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
-
+
NSString *test2 = @"max-age=12";
XCTAssertEqualObjects([networkCommunication maxAgeForCache:test2], @12);
}
- (void)testMaxAgeForCacheMultiValue1 {
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
-
+
NSString *test3 = @"max-age=13, max-stale=7";
XCTAssertEqualObjects([networkCommunication maxAgeForCache:test3], @13);
}
- (void)testMaxAgeForCacheMultiValue2 {
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
-
+
NSString *test4 = @"max-stale=34, max-age=14";
XCTAssertEqualObjects([networkCommunication maxAgeForCache:test4], @14);
}
- (void)testMaxAgeForCacheMultiValue3 {
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
-
+
NSString *test4 = @"max-stale=33434344, max-age=15, min-fresh=3553553";
XCTAssertEqualObjects([networkCommunication maxAgeForCache:test4], @15);
}
- (void)testMaxAgeForCacheCapitalization {
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
-
+
NSString *test5 = @"max-stale=34, MAX-age=16, min-fresh=3553553";
XCTAssertEqualObjects([networkCommunication maxAgeForCache:test5], @16);
}
@@ -938,28 +945,215 @@ - (void)testPodURLRoutingAndTrackingURL {
];
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPStateMachine_PRIVATE *stateMachine = [MParticle sharedInstance].stateMachine;
-
+
stateMachine.attAuthorizationStatus = @(MPATTAuthorizationStatusNotDetermined);
for (NSArray *test in testKeys) {
NSString *key = test[0];
stateMachine.apiKey = key;
NSString *eventHost = test[1];
NSString *identityHost = test[2];
-
+
XCTAssertEqualObjects(eventHost, [networkCommunication defaultEventHost]);
XCTAssertEqualObjects(identityHost, [networkCommunication defaultIdentityHost]);
}
-
+
stateMachine.attAuthorizationStatus = @(MPATTAuthorizationStatusAuthorized);
for (NSArray *test in testKeys) {
NSString *key = test[0];
stateMachine.apiKey = key;
NSString *eventHost = test[3];
NSString *identityHost = test[4];
-
+
XCTAssertEqualObjects(eventHost, [networkCommunication defaultEventHost]);
XCTAssertEqualObjects(identityHost, [networkCommunication defaultIdentityHost]);
}
}
+#pragma mark - Background Task Tests
+
+- (void)testBeginSafeBackgroundTaskDispatchesToMainThread {
+ __block BOOL beginCalledOnMainThread = NO;
+
+ id mockApplication = OCMClassMock([UIApplication class]);
+ OCMStub([mockApplication beginBackgroundTaskWithExpirationHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
+ beginCalledOnMainThread = [NSThread isMainThread];
+ UIBackgroundTaskIdentifier taskId = 42;
+ [invocation setReturnValue:&taskId];
+ });
+
+ [MPApplication_PRIVATE setMockApplication:mockApplication];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Background queue completed"];
+
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ MPNetworkCommunication_PRIVATE *nc = [[MPNetworkCommunication_PRIVATE alloc] init];
+ UIBackgroundTaskIdentifier taskId = [nc beginSafeBackgroundTaskWithExpirationHandler:nil];
+ XCTAssertEqual(taskId, (UIBackgroundTaskIdentifier)42);
+ [expectation fulfill];
+ });
+
+ [self waitForExpectations:@[expectation] timeout:5.0];
+
+ XCTAssertTrue(beginCalledOnMainThread, @"beginBackgroundTaskWithExpirationHandler: must be called on the main thread");
+
+ [MPApplication_PRIVATE setMockApplication:nil];
+}
+
+- (void)testEndSafeBackgroundTaskDispatchesToMainThread {
+ __block BOOL endCalledOnMainThread = NO;
+
+ id mockApplication = OCMClassMock([UIApplication class]);
+ OCMStub([mockApplication endBackgroundTask:(UIBackgroundTaskIdentifier)42]).andDo(^(NSInvocation *invocation) {
+ endCalledOnMainThread = [NSThread isMainThread];
+ });
+
+ [MPApplication_PRIVATE setMockApplication:mockApplication];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Background queue completed"];
+
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ MPNetworkCommunication_PRIVATE *nc = [[MPNetworkCommunication_PRIVATE alloc] init];
+ [nc endSafeBackgroundTask:42];
+ [expectation fulfill];
+ });
+
+ [self waitForExpectations:@[expectation] timeout:5.0];
+
+ // Give main queue time to process the async dispatch
+ XCTestExpectation *mainQueueDrained = [self expectationWithDescription:@"Main queue drained"];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [mainQueueDrained fulfill];
+ });
+ [self waitForExpectations:@[mainQueueDrained] timeout:2.0];
+
+ XCTAssertTrue(endCalledOnMainThread, @"endBackgroundTask: must be called on the main thread");
+
+ [MPApplication_PRIVATE setMockApplication:nil];
+}
+
+- (void)testRequestConfigCallsBeginAndEndBackgroundTask {
+ __block BOOL beginCalled = NO;
+ __block BOOL endCalled = NO;
+
+ id mockApplication = OCMClassMock([UIApplication class]);
+ OCMStub([mockApplication beginBackgroundTaskWithExpirationHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) {
+ beginCalled = YES;
+ UIBackgroundTaskIdentifier taskId = 42;
+ [invocation setReturnValue:&taskId];
+ });
+ OCMStub([mockApplication endBackgroundTask:(UIBackgroundTaskIdentifier)42]).andDo(^(NSInvocation *invocation) {
+ endCalled = YES;
+ });
+
+ [MPApplication_PRIVATE setMockApplication:mockApplication];
+
+ int statusCode = 200;
+ id urlResponseMock = OCMClassMock([NSHTTPURLResponse class]);
+ [[[urlResponseMock stub] andReturnValue:OCMOCK_VALUE(statusCode)] statusCode];
+ [[[urlResponseMock stub] andReturn:@{}] allHeaderFields];
+
+ MPConnectorResponse *response = [[MPConnectorResponse alloc] init];
+ response.httpResponse = urlResponseMock;
+
+ id mockConnector = OCMClassMock([MPConnector class]);
+ [[[mockConnector stub] andReturn:response] responseFromGetRequestToURL:OCMOCK_ANY];
+
+ MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
+ id mockNetworkCommunication = OCMPartialMock(networkCommunication);
+ [[[mockNetworkCommunication stub] andReturn:mockConnector] makeConnector];
+
+ [networkCommunication requestConfig:mockConnector withCompletionHandler:^(BOOL success) {}];
+
+ // Give main queue time to process async endBackgroundTask dispatch
+ XCTestExpectation *mainQueueDrained = [self expectationWithDescription:@"Main queue drained"];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [mainQueueDrained fulfill];
+ });
+ [self waitForExpectations:@[mainQueueDrained] timeout:2.0];
+
+ XCTAssertTrue(beginCalled, @"beginBackgroundTaskWithExpirationHandler: should be called during config request");
+ XCTAssertTrue(endCalled, @"endBackgroundTask: should be called after config request completes");
+
+ [MPApplication_PRIVATE setMockApplication:nil];
+}
+
+- (void)testExpirationHandlerEndsTask {
+ __block void (^capturedExpirationHandler)(void) = nil;
+
+ id mockApplication = OCMClassMock([UIApplication class]);
+ OCMStub([mockApplication beginBackgroundTaskWithExpirationHandler:([OCMArg checkWithBlock:^BOOL(id obj) {
+ capturedExpirationHandler = [obj copy];
+ return YES;
+ }])]).andDo(^(NSInvocation *invocation) {
+ UIBackgroundTaskIdentifier taskId = 42;
+ [invocation setReturnValue:&taskId];
+ });
+ OCMStub([mockApplication endBackgroundTask:(UIBackgroundTaskIdentifier)42]);
+
+ [MPApplication_PRIVATE setMockApplication:mockApplication];
+
+ MPNetworkCommunication_PRIVATE *nc = [[MPNetworkCommunication_PRIVATE alloc] init];
+ [nc beginSafeBackgroundTaskWithExpirationHandler:nil];
+
+ XCTAssertNotNil(capturedExpirationHandler, @"Expiration handler should have been captured");
+
+ capturedExpirationHandler();
+
+ OCMVerify([mockApplication endBackgroundTask:(UIBackgroundTaskIdentifier)42]);
+
+ [MPApplication_PRIVATE setMockApplication:nil];
+}
+
+- (void)testIdentityExpirationHandlerSetsIdentifyingNO {
+ __block void (^capturedExpirationHandler)(void) = nil;
+
+ id mockApplication = OCMClassMock([UIApplication class]);
+ OCMStub([mockApplication beginBackgroundTaskWithExpirationHandler:([OCMArg checkWithBlock:^BOOL(id obj) {
+ capturedExpirationHandler = [obj copy];
+ return YES;
+ }])]).andDo(^(NSInvocation *invocation) {
+ UIBackgroundTaskIdentifier taskId = 42;
+ [invocation setReturnValue:&taskId];
+ });
+ OCMStub([mockApplication endBackgroundTask:(UIBackgroundTaskIdentifier)42]);
+
+ [MPApplication_PRIVATE setMockApplication:mockApplication];
+
+ MPNetworkCommunication_PRIVATE *nc = [[MPNetworkCommunication_PRIVATE alloc] init];
+ nc.identifying = YES;
+
+ [nc beginSafeBackgroundTaskWithExpirationHandler:^{
+ nc.identifying = NO;
+ }];
+
+ XCTAssertNotNil(capturedExpirationHandler, @"Expiration handler should have been captured");
+ XCTAssertTrue(nc.identifying, @"identifying should still be YES before expiration");
+
+ capturedExpirationHandler();
+
+ XCTAssertFalse(nc.identifying, @"identifying should be NO after expiration handler fires");
+ OCMVerify([mockApplication endBackgroundTask:(UIBackgroundTaskIdentifier)42]);
+
+ [MPApplication_PRIVATE setMockApplication:nil];
+}
+
+- (void)testBackgroundTaskSkippedInAppExtension {
+ id mockStateMachine = OCMClassMock([MPStateMachine_PRIVATE class]);
+ OCMStub([mockStateMachine isAppExtension]).andReturn(YES);
+
+ id mockApplication = OCMClassMock([UIApplication class]);
+ [[mockApplication reject] beginBackgroundTaskWithExpirationHandler:OCMOCK_ANY];
+
+ [MPApplication_PRIVATE setMockApplication:mockApplication];
+
+ MPNetworkCommunication_PRIVATE *nc = [[MPNetworkCommunication_PRIVATE alloc] init];
+ UIBackgroundTaskIdentifier taskId = [nc beginSafeBackgroundTaskWithExpirationHandler:nil];
+
+ XCTAssertEqual(taskId, UIBackgroundTaskInvalid, @"Should return UIBackgroundTaskInvalid in app extension");
+ OCMVerifyAll(mockApplication);
+
+ [mockStateMachine stopMocking];
+ [MPApplication_PRIVATE setMockApplication:nil];
+}
+
@end
diff --git a/UnitTests/ObjCTests/MPStateMachineTests.m b/UnitTests/ObjCTests/MPStateMachineTests.m
index 00c20e2a5..2785bc21f 100644
--- a/UnitTests/ObjCTests/MPStateMachineTests.m
+++ b/UnitTests/ObjCTests/MPStateMachineTests.m
@@ -213,4 +213,62 @@ - (void)testRequestAttribution {
}
#endif
+#pragma mark - Thread Safety Tests
+
+- (void)testApiKeySecretThreadSafety {
+ MPStateMachine_PRIVATE *stateMachine = [MParticle sharedInstance].stateMachine;
+
+ NSString *originalApiKey = stateMachine.apiKey;
+ NSString *originalSecret = stateMachine.secret;
+ [self addTeardownBlock:^{
+ stateMachine.apiKey = originalApiKey;
+ stateMachine.secret = originalSecret;
+ }];
+
+ stateMachine.apiKey = @"initial_api_key_value_that_is_long_enough_to_force_heap_allocation";
+ stateMachine.secret = @"initial_secret_value_that_is_long_enough_to_force_heap_allocation";
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Thread safety stress test"];
+
+ dispatch_group_t group = dispatch_group_create();
+ dispatch_queue_t concurrentQueue = dispatch_queue_create("com.mparticle.test.statemachine.concurrent", DISPATCH_QUEUE_CONCURRENT);
+
+ NSInteger iterations = 10000;
+ __block BOOL encounteredError = NO;
+
+ for (NSInteger i = 0; i < 4; i++) {
+ dispatch_group_async(group, concurrentQueue, ^{
+ for (NSInteger j = 0; j < iterations && !encounteredError; j++) {
+ @try {
+ NSString *key = stateMachine.apiKey;
+ NSString *sec = stateMachine.secret;
+ (void)[key length];
+ (void)[sec length];
+ } @catch (NSException *exception) {
+ encounteredError = YES;
+ }
+ }
+ });
+ }
+
+ dispatch_group_async(group, concurrentQueue, ^{
+ for (NSInteger j = 0; j < iterations && !encounteredError; j++) {
+ @try {
+ // Use long format strings to force heap-allocated NSString (not tagged pointers)
+ stateMachine.apiKey = [NSString stringWithFormat:@"api_key_value_for_thread_safety_test_iteration_%ld", (long)j];
+ stateMachine.secret = [NSString stringWithFormat:@"secret_value_for_thread_safety_test_iteration_%ld", (long)j];
+ } @catch (NSException *exception) {
+ encounteredError = YES;
+ }
+ }
+ });
+
+ dispatch_group_notify(group, dispatch_get_main_queue(), ^{
+ XCTAssertFalse(encounteredError, @"Thread safety test should complete without errors");
+ [expectation fulfill];
+ });
+
+ [self waitForExpectationsWithTimeout:30 handler:nil];
+}
+
@end
diff --git a/UnitTests/ObjCTests/MPURLRequestBuilderTests.m b/UnitTests/ObjCTests/MPURLRequestBuilderTests.m
index 571fe61d3..8c2239d90 100644
--- a/UnitTests/ObjCTests/MPURLRequestBuilderTests.m
+++ b/UnitTests/ObjCTests/MPURLRequestBuilderTests.m
@@ -436,6 +436,152 @@ - (void)testEventRequest {
}
}
+- (void)testBuildReturnsNilWhenURLPropertyIsNil {
+ NSURL *validURL = [NSURL URLWithString:@"https://config2.mparticle.com/v4/key/config?av=1.0&sv=1.0"];
+ MPURL *mpURL = [[MPURL alloc] initWithURL:(NSURL * _Nonnull)nil defaultURL:validURL];
+ MPURLRequestBuilder *builder = [MPURLRequestBuilder newBuilderWithURL:mpURL message:nil httpMethod:@"GET"];
+
+ NSMutableURLRequest *request;
+ XCTAssertNoThrow(request = [builder build], @"build should not throw when URL is nil");
+ XCTAssertNil(request, @"build should return nil when URL is nil");
+}
+
+- (void)testBuildReturnsNilWhenDefaultURLIsNil {
+ NSURL *validURL = [NSURL URLWithString:@"https://config2.mparticle.com/v4/key/config?av=1.0&sv=1.0"];
+ MPURL *mpURL = [[MPURL alloc] initWithURL:validURL defaultURL:(NSURL * _Nonnull)nil];
+ MPURLRequestBuilder *builder = [MPURLRequestBuilder newBuilderWithURL:mpURL message:nil httpMethod:@"GET"];
+
+ NSMutableURLRequest *request;
+ XCTAssertNoThrow(request = [builder build], @"build should not throw when defaultURL is nil");
+ XCTAssertNil(request, @"build should return nil when defaultURL is nil");
+}
+
+- (void)testBuildConfigRequestWithQuerylessURL {
+ NSURL *noQueryURL = [NSURL URLWithString:@"https://config2.mparticle.com/v4/key/config"];
+ MPURL *mpURL = [[MPURL alloc] initWithURL:noQueryURL defaultURL:noQueryURL];
+ MPURLRequestBuilder *builder = [MPURLRequestBuilder newBuilderWithURL:mpURL message:nil httpMethod:@"GET"];
+ XCTAssertNotNil(builder);
+
+ NSMutableURLRequest *request;
+ XCTAssertNoThrow(request = [builder build], @"build should not throw for a URL without query parameters");
+ XCTAssertNotNil(request);
+
+ NSString *signature = request.allHTTPHeaderFields[@"x-mp-signature"];
+ XCTAssertNotNil(signature, @"Signature should still be generated for queryless URL");
+ XCTAssertTrue(signature.length > 0);
+}
+
+- (void)testDateHeaderIsValidRFC1123 {
+ MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
+ MPURLRequestBuilder *builder = [MPURLRequestBuilder newBuilderWithURL:[networkCommunication configURL] message:nil httpMethod:@"GET"];
+ NSMutableURLRequest *request = [builder build];
+ XCTAssertNotNil(request);
+
+ NSString *dateHeader = request.allHTTPHeaderFields[@"Date"];
+ XCTAssertNotNil(dateHeader, @"Date header should be present");
+ XCTAssertTrue(dateHeader.length > 0, @"Date header should not be empty");
+
+ NSDate *parsedDate = [MPDateFormatter dateFromStringRFC1123:dateHeader];
+ XCTAssertNotNil(parsedDate, @"Date header should be parseable as RFC1123 by MPDateFormatter");
+}
+
+- (void)testConfigSignatureOmitsQuestionMarkForQuerylessURL {
+ NSURL *noQueryURL = [NSURL URLWithString:@"https://config2.mparticle.com/v4/key/config"];
+ MPURL *mpURL = [[MPURL alloc] initWithURL:noQueryURL defaultURL:noQueryURL];
+ MPURLRequestBuilder *builder = [MPURLRequestBuilder newBuilderWithURL:mpURL message:nil httpMethod:@"GET"];
+ XCTAssertNotNil(builder);
+
+ __block NSString *capturedSignature = nil;
+ id partialMock = OCMPartialMock(builder);
+ OCMStub([partialMock hmacSha256Encode:[OCMArg any] key:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
+ __unsafe_unretained NSString *sig;
+ [invocation getArgument:&sig atIndex:2];
+ capturedSignature = sig;
+ });
+
+ [builder build];
+
+ XCTAssertNotNil(capturedSignature, @"Signature message should have been captured");
+ XCTAssertTrue([capturedSignature rangeOfString:@"(null)"].location == NSNotFound,
+ @"Signature should not contain (null): %@", capturedSignature);
+ XCTAssertFalse([capturedSignature hasSuffix:@"?"],
+ @"Signature should not end with a trailing ? for queryless URL: %@", capturedSignature);
+}
+
+- (void)testAudienceSignatureOmitsQuestionMarkForQuerylessURL {
+ NSURL *audienceURL = [NSURL URLWithString:@"https://nativesdks.mparticle.com/v2/audience"];
+ audienceURL.accessibilityHint = @"audience";
+ MPURL *mpURL = [[MPURL alloc] initWithURL:audienceURL defaultURL:audienceURL];
+ MPURLRequestBuilder *builder = [MPURLRequestBuilder newBuilderWithURL:mpURL message:nil httpMethod:@"GET"];
+ XCTAssertNotNil(builder);
+
+ __block NSString *capturedSignature = nil;
+ id partialMock = OCMPartialMock(builder);
+ OCMStub([partialMock hmacSha256Encode:[OCMArg any] key:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
+ __unsafe_unretained NSString *sig;
+ [invocation getArgument:&sig atIndex:2];
+ capturedSignature = sig;
+ });
+
+ [builder build];
+
+ XCTAssertNotNil(capturedSignature, @"Signature message should have been captured");
+ XCTAssertTrue([capturedSignature rangeOfString:@"(null)"].location == NSNotFound,
+ @"Signature should not contain (null): %@", capturedSignature);
+ XCTAssertFalse([capturedSignature hasSuffix:@"?"],
+ @"Signature should not end with a trailing ? for queryless URL: %@", capturedSignature);
+}
+
+- (void)testBuildReturnsNilForIdentityRequestWithNilPostData {
+ NSURL *identityURL = [NSURL URLWithString:@"https://identity.mparticle.com/v1/identify"];
+ identityURL.accessibilityHint = @"identity";
+ MPURL *mpURL = [[MPURL alloc] initWithURL:identityURL defaultURL:identityURL];
+ MPURLRequestBuilder *builder = [MPURLRequestBuilder newBuilderWithURL:mpURL message:nil httpMethod:@"POST"];
+ XCTAssertNotNil(builder);
+
+ NSMutableURLRequest *request;
+ XCTAssertNoThrow(request = [builder build], @"build should not throw for identity request with nil post data");
+ XCTAssertNil(request, @"build should return nil for identity request with nil post data");
+}
+
+- (void)testSecondsFromGMTHeaderIsValidSignedInteger {
+ MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
+ MPURLRequestBuilder *builder = [MPURLRequestBuilder newBuilderWithURL:[networkCommunication configURL] message:nil httpMethod:@"GET"];
+ NSMutableURLRequest *request = [builder build];
+ XCTAssertNotNil(request);
+
+ NSString *secondsHeader = request.allHTTPHeaderFields[@"secondsFromGMT"];
+ XCTAssertNotNil(secondsHeader, @"secondsFromGMT header should be present");
+ XCTAssertTrue(secondsHeader.length > 0, @"secondsFromGMT header should not be empty");
+
+ NSInteger parsedValue = [secondsHeader integerValue];
+ NSString *reformatted = [NSString stringWithFormat:@"%ld", (long)parsedValue];
+ XCTAssertEqualObjects(secondsHeader, reformatted,
+ @"secondsFromGMT should be a valid signed integer, got: %@", secondsHeader);
+
+ XCTAssertTrue(parsedValue >= -43200 && parsedValue <= 50400,
+ @"secondsFromGMT should be within valid UTC offset range (-43200 to 50400), got: %ld", (long)parsedValue);
+}
+
+- (void)testBuildWithNilSecretProducesRequestWithoutSignature {
+ NSURL *validURL = [NSURL URLWithString:@"https://config2.mparticle.com/v4/key/config?av=1.0&sv=1.0"];
+ MPURL *mpURL = [[MPURL alloc] initWithURL:validURL defaultURL:validURL];
+ MPURLRequestBuilder *builder = [MPURLRequestBuilder newBuilderWithURL:mpURL message:nil httpMethod:@"GET"];
+ XCTAssertNotNil(builder);
+
+ NSString *originalSecret = [MParticle sharedInstance].stateMachine.secret;
+ [MParticle sharedInstance].stateMachine.secret = nil;
+
+ NSMutableURLRequest *request;
+ XCTAssertNoThrow(request = [builder build], @"build should not throw when secret is nil");
+ XCTAssertNotNil(request, @"build should still return a request when secret is nil");
+
+ NSString *signature = request.allHTTPHeaderFields[@"x-mp-signature"];
+ XCTAssertNil(signature, @"x-mp-signature should be absent when secret is nil");
+
+ [MParticle sharedInstance].stateMachine.secret = originalSecret;
+}
+
- (void)testSignatureRelativePath {
MPNetworkCommunication_PRIVATE *networkCommunication = [[MPNetworkCommunication_PRIVATE alloc] init];
MPNetworkOptions *networkOptions = [[MPNetworkOptions alloc] init];
diff --git a/mParticle-Apple-SDK-Swift/Sources/Utils/MPDateFormatter.swift b/mParticle-Apple-SDK-Swift/Sources/Utils/MPDateFormatter.swift
index 9c9c9fcb6..1f8f4d5c3 100644
--- a/mParticle-Apple-SDK-Swift/Sources/Utils/MPDateFormatter.swift
+++ b/mParticle-Apple-SDK-Swift/Sources/Utils/MPDateFormatter.swift
@@ -1,6 +1,11 @@
import Foundation
@objc public class MPDateFormatter: NSObject {
+ // MARK: - Serial queue for thread-safe access to DateFormatter instances
+
+ // DateFormatter is NOT thread-safe, so we use a serial queue to synchronize access
+ private static let formatterQueue = DispatchQueue(label: "com.mparticle.dateformatter")
+
private static var dateFormatterRFC3339: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
@@ -30,15 +35,17 @@ import Foundation
return nil
}
- if let date = dateFormatterRFC3339.date(from: dateString) {
- return date
- }
+ return formatterQueue.sync {
+ if let date = dateFormatterRFC3339.date(from: dateString) {
+ return date
+ }
- if let date = dateFormatterRFC1123.date(from: dateString) {
- return date
- }
+ if let date = dateFormatterRFC1123.date(from: dateString) {
+ return date
+ }
- return dateFormatterRFC850.date(from: dateString)
+ return dateFormatterRFC850.date(from: dateString)
+ }
}
@objc public static func date(fromStringRFC1123 dateString: String) -> Date? {
@@ -46,7 +53,9 @@ import Foundation
return nil
}
- return dateFormatterRFC1123.date(from: dateString)
+ return formatterQueue.sync {
+ dateFormatterRFC1123.date(from: dateString)
+ }
}
@objc public static func date(fromStringRFC3339 dateString: String) -> Date? {
@@ -54,14 +63,20 @@ import Foundation
return nil
}
- return dateFormatterRFC3339.date(from: dateString)
+ return formatterQueue.sync {
+ dateFormatterRFC3339.date(from: dateString)
+ }
}
@objc public static func string(fromDateRFC1123 date: Date) -> String? {
- return dateFormatterRFC1123.string(from: date)
+ return formatterQueue.sync {
+ dateFormatterRFC1123.string(from: date)
+ }
}
@objc public static func string(fromDateRFC3339 date: Date) -> String? {
- return dateFormatterRFC3339.string(from: date)
+ return formatterQueue.sync {
+ dateFormatterRFC3339.string(from: date)
+ }
}
}
diff --git a/mParticle-Apple-SDK-Swift/Sources/Utils/MPDevice.swift b/mParticle-Apple-SDK-Swift/Sources/Utils/MPDevice.swift
index 87a636fb4..ce48acd8a 100644
--- a/mParticle-Apple-SDK-Swift/Sources/Utils/MPDevice.swift
+++ b/mParticle-Apple-SDK-Swift/Sources/Utils/MPDevice.swift
@@ -145,7 +145,7 @@ public class MPDevice: NSObject, NSCopying {
@objc public var language: String? {
// Extra logic added to strip out the country code to stay consistent with earlier iOS releases
- guard let subString = Locale.preferredLanguages[0].split(separator: "-").first else {
+ guard let subString = Locale.preferredLanguages.first?.split(separator: "-").first else {
return nil
}
diff --git a/mParticle-Apple-SDK-Swift/Sources/Utils/MPUserDefaults.swift b/mParticle-Apple-SDK-Swift/Sources/Utils/MPUserDefaults.swift
index 5e7bed505..c97793d51 100644
--- a/mParticle-Apple-SDK-Swift/Sources/Utils/MPUserDefaults.swift
+++ b/mParticle-Apple-SDK-Swift/Sources/Utils/MPUserDefaults.swift
@@ -2,6 +2,7 @@ import Foundation
private var userDefaults: MPUserDefaults?
private var sharedGroupID: String?
+private let userDefaultsQueue = DispatchQueue(label: "com.mparticle.userdefaults")
private let NSUserDefaultsPrefix = "mParticle::"
private let userSpecificKeys = ["lud", /* kMPAppLastUseDateKey */
"lc", /* kMPAppLaunchCountKey */
@@ -33,11 +34,12 @@ public protocol MPUserDefaultsProtocol {
}
@objc public class func standardUserDefaults(connector: MPUserDefaultsConnectorProtocol) -> MPUserDefaults {
- if userDefaults == nil {
- userDefaults = MPUserDefaults(connector: connector)
+ return userDefaultsQueue.sync {
+ if userDefaults == nil {
+ userDefaults = MPUserDefaults(connector: connector)
+ }
+ return userDefaults!
}
-
- return userDefaults!
}
@objc public func mpObject(forKey key: String, userId: NSNumber) -> Any? {
@@ -263,7 +265,10 @@ public protocol MPUserDefaultsProtocol {
for key in mParticleKeys {
UserDefaults.standard.removeObject(forKey: key)
}
- userDefaults = nil
+
+ userDefaultsQueue.sync {
+ userDefaults = nil
+ }
UserDefaults.standard.synchronize()
}
@@ -335,9 +340,11 @@ public protocol MPUserDefaultsProtocol {
@objc public class func isOlderThanConfigMaxAgeSeconds() -> Bool {
var shouldConfigurationBeDeleted = false
- if let userDefaults = userDefaults {
- let configProvisioned = userDefaults[Miscellaneous.kMPConfigProvisionedTimestampKey] as? NSNumber
- let maxAgeSeconds = userDefaults.connector.configMaxAgeSeconds()
+ let defaults = userDefaultsQueue.sync { userDefaults }
+
+ if let defaults = defaults {
+ let configProvisioned = defaults[Miscellaneous.kMPConfigProvisionedTimestampKey] as? NSNumber
+ let maxAgeSeconds = defaults.connector.configMaxAgeSeconds()
if let configProvisioned = configProvisioned, let maxAgeSeconds = maxAgeSeconds, maxAgeSeconds.doubleValue > 0 {
let intervalConfigProvisioned: TimeInterval = configProvisioned.doubleValue
@@ -346,7 +353,7 @@ public protocol MPUserDefaultsProtocol {
}
if shouldConfigurationBeDeleted {
- userDefaults.deleteConfiguration()
+ defaults.deleteConfiguration()
}
}
return shouldConfigurationBeDeleted
@@ -359,11 +366,13 @@ public protocol MPUserDefaultsProtocol {
}
@objc public class func restore() -> MPResponseConfig? {
- if let userDefaults = userDefaults {
- if let configuration = userDefaults.getConfiguration(), userDefaults.connector.canCreateConfiguration() {
+ let defaults = userDefaultsQueue.sync { userDefaults }
+
+ if let defaults = defaults {
+ if let configuration = defaults.getConfiguration(), defaults.connector.canCreateConfiguration() {
let responseConfig = MPResponseConfig(
configuration: configuration,
- connector: userDefaults.connector
+ connector: defaults.connector
)
return responseConfig
@@ -374,9 +383,8 @@ public protocol MPUserDefaultsProtocol {
}
@objc public class func deleteConfig() {
- if let userDefaults = userDefaults {
- userDefaults.deleteConfiguration()
- }
+ let defaults = userDefaultsQueue.sync { userDefaults }
+ defaults?.deleteConfiguration()
}
// Private Methods
diff --git a/mParticle-Apple-SDK-Swift/Test/Utils/MPDateFormatterTests.swift b/mParticle-Apple-SDK-Swift/Test/Utils/MPDateFormatterTests.swift
index 6565ebdb0..f44f9524e 100644
--- a/mParticle-Apple-SDK-Swift/Test/Utils/MPDateFormatterTests.swift
+++ b/mParticle-Apple-SDK-Swift/Test/Utils/MPDateFormatterTests.swift
@@ -87,4 +87,98 @@ final class MPDateFormatterTests: XCTestCase {
)
}
}
+
+ // MARK: - Thread Safety Tests
+
+ func testDateFormatterThreadSafety() {
+ // This stress test verifies that MPDateFormatter doesn't crash when
+ // called concurrently from multiple threads. DateFormatter is NOT
+ // thread-safe, so without synchronization this test would likely crash.
+ // Race conditions are non-deterministic, so this test increases the
+ // likelihood of catching issues but cannot guarantee detection.
+
+ let expectation = self.expectation(description: "Thread safety stress test")
+
+ let group = DispatchGroup()
+ let concurrentQueue = DispatchQueue(label: "com.mparticle.test.dateformatter", attributes: .concurrent)
+
+ let iterations = 100
+ var encounteredError = false
+ let errorLock = NSLock()
+
+ let rfc3339Strings = [
+ "2024-01-15T10:30:00+0000",
+ "2023-06-20T15:45:30-0500",
+ "1955-11-05T01:15:00-0800"
+ ]
+
+ let rfc1123Strings = [
+ "Mon, 15 Jan 2024 10:30:00 GMT",
+ "Tue, 20 Jun 2023 15:45:30 GMT",
+ "Sat, 05 Nov 1955 09:15:00 GMT"
+ ]
+
+ // Multiple threads parsing RFC3339 dates
+ for _ in 0..<3 {
+ group.enter()
+ concurrentQueue.async {
+ for j in 0..
@property (nonatomic, strong) MPIdentityApiManager *apiManager;
-@property(nonatomic, strong, readwrite, nonnull) MParticleUser *currentUser;
+@property(strong, readwrite, nonnull) MParticleUser *currentUser;
@end
@@ -300,15 +300,23 @@ - (void)forwardCallToKits:(MPIdentityApiRequest *)request identityRequestType:(M
}
- (MParticleUser *)currentUser {
- if (_currentUser) {
+ @synchronized(self) {
+ if (_currentUser) {
+ return _currentUser;
+ }
+
+ NSNumber *mpid = [MPPersistenceController_PRIVATE mpId];
+ MParticleUser *user = [[MParticleUser alloc] init];
+ user.userId = mpid;
+ _currentUser = user;
return _currentUser;
}
+}
- NSNumber *mpid = [MPPersistenceController_PRIVATE mpId];
- MParticleUser *user = [[MParticleUser alloc] init];
- user.userId = mpid;
- _currentUser = user;
- return _currentUser;
+- (void)setCurrentUser:(MParticleUser *)currentUser {
+ @synchronized(self) {
+ _currentUser = currentUser;
+ }
}
- (MParticleUser *)getUser:(NSNumber *)mpId {
diff --git a/mParticle-Apple-SDK/Include/MPIdentityApi.h b/mParticle-Apple-SDK/Include/MPIdentityApi.h
index 3d460eb9e..7effbc710 100644
--- a/mParticle-Apple-SDK/Include/MPIdentityApi.h
+++ b/mParticle-Apple-SDK/Include/MPIdentityApi.h
@@ -79,7 +79,7 @@ typedef void (^MPModifyApiResultCallback)(MPModifyApiResult *_Nullable apiResult
/**
The current user. All actions taken in the SDK will be associated with this user (e.g. logging events, setting attributes, etc.)
*/
-@property(nonatomic, strong, readonly, nullable) MParticleUser *currentUser;
+@property(strong, readonly, nullable) MParticleUser *currentUser;
/**
The device application stamp. This is a random identifier associated with this particular app as installed on this particular device.
diff --git a/mParticle-Apple-SDK/Include/MPStateMachine.h b/mParticle-Apple-SDK/Include/MPStateMachine.h
index 43c774bd3..3d4373903 100644
--- a/mParticle-Apple-SDK/Include/MPStateMachine.h
+++ b/mParticle-Apple-SDK/Include/MPStateMachine.h
@@ -15,14 +15,14 @@
@property (nonatomic, weak, nullable) MPSession *currentSession;
@property (nonatomic) NSNumber * _Nullable attAuthorizationStatus;
@property (nonatomic) NSNumber * _Nullable attAuthorizationTimestamp;
-@property (nonatomic, strong, nonnull) NSString *apiKey __attribute__((const));
-@property (nonatomic, strong, nonnull) NSString *secret __attribute__((const));
+@property (atomic, strong, nonnull) NSString *apiKey;
+@property (atomic, strong, nonnull) NSString *secret;
@end
@interface MPStateMachine_PRIVATE : NSObject
-@property (nonatomic, strong, nonnull) NSString *apiKey __attribute__((const));
+@property (atomic, strong, nonnull) NSString *apiKey;
@property (nonatomic, strong, nonnull) MPConsumerInfo *consumerInfo;
@property (nonatomic, weak, nullable) MPSession *currentSession;
@property (nonatomic, strong, nullable) NSArray *customModules;
@@ -31,7 +31,7 @@
@property (nonatomic, strong, nullable) NSDictionary *launchOptions;
@property (nonatomic, strong, nullable) NSString *networkPerformanceMeasuringMode;
@property (nonatomic, strong, nullable) NSString *pushNotificationMode;
-@property (nonatomic, strong, nonnull) NSString *secret __attribute__((const));
+@property (atomic, strong, nonnull) NSString *secret;
@property (nonatomic, strong, nonnull) NSDate *startTime;
@property (nonatomic, strong, nullable) MPLaunchInfo *launchInfo;
@property (nonatomic, strong, readonly, nullable) NSString *deviceTokenType;
diff --git a/mParticle-Apple-SDK/Kits/MPKitContainer.m b/mParticle-Apple-SDK/Kits/MPKitContainer.m
index 7148ed395..0d9fa2275 100644
--- a/mParticle-Apple-SDK/Kits/MPKitContainer.m
+++ b/mParticle-Apple-SDK/Kits/MPKitContainer.m
@@ -1984,9 +1984,11 @@ - (void)removeKitsFromRegistryInvalidForWorkspaceSwitch {
return nil;
}
- NSMutableArray > *activeKitsRegistry = [[NSMutableArray alloc] initWithCapacity:kitsRegistry.count];
+ // Copy the registry to avoid race conditions with concurrent modifications
+ NSSet *kitsRegistryCopy = [kitsRegistry copy];
+ NSMutableArray > *activeKitsRegistry = [[NSMutableArray alloc] initWithCapacity:kitsRegistryCopy.count];
- for (idkitRegister in kitsRegistry) {
+ for (idkitRegister in kitsRegistryCopy) {
if ([self isActiveAndNotDisabled:kitRegister]) {
[activeKitsRegistry addObject:kitRegister];
}
diff --git a/mParticle-Apple-SDK/MPBackendController.m b/mParticle-Apple-SDK/MPBackendController.m
index 19a1064dd..6d0c7ef6c 100644
--- a/mParticle-Apple-SDK/MPBackendController.m
+++ b/mParticle-Apple-SDK/MPBackendController.m
@@ -343,13 +343,25 @@ - (void)setPreviousSessionSuccessfullyClosed:(NSNumber *)previousSessionSuccessf
NSString *previousSessionStateFile = [stateMachineDirectoryPath stringByAppendingPathComponent:kMPPreviousSessionStateFileName];
NSDictionary *previousSessionStateDictionary = @{kMPASTPreviousSessionSuccessfullyClosedKey:previousSessionSuccessfullyClosed};
- if (![fileManager fileExistsAtPath:stateMachineDirectoryPath]) {
- [fileManager createDirectoryAtPath:stateMachineDirectoryPath withIntermediateDirectories:YES attributes:nil error:nil];
- } else if ([fileManager fileExistsAtPath:previousSessionStateFile]) {
- [fileManager removeItemAtPath:previousSessionStateFile error:nil];
+ @try {
+ if (![fileManager fileExistsAtPath:stateMachineDirectoryPath]) {
+ NSError *dirError = nil;
+ [fileManager createDirectoryAtPath:stateMachineDirectoryPath withIntermediateDirectories:YES attributes:nil error:&dirError];
+ if (dirError) {
+ MPILogError(@"Failed to create state machine directory: %@", dirError);
+ return;
+ }
+ } else if ([fileManager fileExistsAtPath:previousSessionStateFile]) {
+ [fileManager removeItemAtPath:previousSessionStateFile error:nil];
+ }
+
+ BOOL success = [previousSessionStateDictionary writeToFile:previousSessionStateFile atomically:YES];
+ if (!success) {
+ MPILogError(@"Failed to write previous session state to file");
+ }
+ } @catch (NSException *exception) {
+ MPILogError(@"Exception writing previous session state: %@", exception);
}
-
- [previousSessionStateDictionary writeToFile:previousSessionStateFile atomically:YES];
}
- (void)processDidFinishLaunching:(NSNotification *)notification {
@@ -598,8 +610,10 @@ - (void)prepareBatchesForUpload:(MPUploadSettings *)uploadSettings {
[uploadBuilder withUserAttributes:[self userAttributesForUserId:mpid] deletedUserAttributes:self.deletedUserAttributes];
[uploadBuilder withUserIdentities:[self userIdentitiesForUserId:mpid]];
[uploadBuilder build:^(MPUpload *upload) {
- //Save the Upload to the Database (3)
- [persistence saveUpload:upload];
+ if (upload) {
+ //Save the Upload to the Database (3)
+ [persistence saveUpload:upload];
+ }
}];
}
@@ -1888,6 +1902,7 @@ - (void)beginBackgroundTask {
if (self.backendBackgroundTaskIdentifier == UIBackgroundTaskInvalid) {
self.backendBackgroundTaskIdentifier = [[MPApplication_PRIVATE sharedUIApplication] beginBackgroundTaskWithExpirationHandler:^{
MPILogDebug(@"SDK has ended background activity together with the app.");
+ [self cancelBackgroundTimeCheckLoop];
[self endBackgroundTask];
}];
}
@@ -1934,31 +1949,31 @@ - (void)endSessionIfTimedOut {
return;
}
- if (self.session != nil && [self shouldEndSession]) {
- NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
- NSTimeInterval lastEventTime = self.timeOfLastEventInBackground;
- self.session.endTime = lastEventTime;
-
- [self updateSessionBackgroundTime];
-
- // Since we use the timeAppWentToBackground to calculate background time, but timeOfLastEventInBackground as the endTime,
- // this can result in incorrectly calculated foreground time when ending a session in the background. So subtract the additional
- // time since timeOfLastEventInBackground from the background time to correct this.
- self.session.backgroundTime -= currentTime - self.timeOfLastEventInBackground;
-
- // Reset time of last event to reset the session timeout
- self.timeOfLastEventInBackground = currentTime;
-
- // Reset the time app went to background so that it's correctly calculated in the new session
- self.timeAppWentToBackgroundInCurrentSession = currentTime;
-
- [MParticle executeOnMessage:^{
+ [MParticle executeOnMessage:^{
+ if (self.session != nil && [self shouldEndSession]) {
+ NSTimeInterval currentTime = [[NSDate date] timeIntervalSince1970];
+ NSTimeInterval lastEventTime = self.timeOfLastEventInBackground;
+ self.session.endTime = lastEventTime;
+
+ [self updateSessionBackgroundTime];
+
+ // Since we use the timeAppWentToBackground to calculate background time, but timeOfLastEventInBackground as the endTime,
+ // this can result in incorrectly calculated foreground time when ending a session in the background. So subtract the additional
+ // time since timeOfLastEventInBackground from the background time to correct this.
+ self.session.backgroundTime -= currentTime - self.timeOfLastEventInBackground;
+
+ // Reset time of last event to reset the session timeout
+ self.timeOfLastEventInBackground = currentTime;
+
+ // Reset the time app went to background so that it's correctly calculated in the new session
+ self.timeAppWentToBackgroundInCurrentSession = currentTime;
+
[[MParticle sharedInstance].persistenceController updateSession:self.session];
[self processOpenSessionsEndingCurrent:YES completionHandler:^(void) {
MPILogVerbose(@"Session ended in the background. New session will begin if an mParticle event is logged or app enters foreground.");
}];
- }];
- }
+ }
+ }];
}
#pragma mark Application Lifecycle
@@ -2036,14 +2051,17 @@ - (void)beginBackgroundTimeCheckLoop {
// Loop to check the background state and time remaining to decide when to upload
while (applicationState == UIApplicationStateBackground) {
- // Handle edge case where app leaves and re-enters background during while the thread is asleep
+ [self endSessionIfTimedOut];
+
+ // Check cancellation immediately before accessing backgroundTimeRemaining
+ // to avoid calling it after the OS has begun tearing down XPC connections
if (!weakBlockOperation || weakBlockOperation.isCancelled) {
return;
}
- [self endSessionIfTimedOut];
+ NSTimeInterval timeRemaining = sharedApplication.backgroundTimeRemaining;
- if (sharedApplication.backgroundTimeRemaining <= kMPRemainingBackgroundTimeMinimumThreshold) {
+ if (timeRemaining <= kMPRemainingBackgroundTimeMinimumThreshold) {
// Less than kMPRemainingBackgroundTimeMinimumThreshold seconds left in the background, upload the batch
MPILogVerbose(@"Less than %f time remaining in background, uploading batch and ending background task", kMPRemainingBackgroundTimeMinimumThreshold);
[MParticle executeOnMessage:^{
@@ -2055,7 +2073,7 @@ - (void)beginBackgroundTimeCheckLoop {
}];
return;
}
- MPILogVerbose(@"Background time remaining %f", sharedApplication.backgroundTimeRemaining);
+ MPILogVerbose(@"Background time remaining %f", timeRemaining);
// Short sleep to prevent burning CPU cycles
[NSThread sleepForTimeInterval:1.0];
diff --git a/mParticle-Apple-SDK/MPIConstants.m b/mParticle-Apple-SDK/MPIConstants.m
index 21526daf9..cc5b7c96b 100644
--- a/mParticle-Apple-SDK/MPIConstants.m
+++ b/mParticle-Apple-SDK/MPIConstants.m
@@ -1,7 +1,7 @@
#import "MPIConstants.h"
// mParticle SDK Version
-NSString *const kMParticleSDKVersion = @"8.42.2";
+NSString *const kMParticleSDKVersion = @"8.43.1";
// Message Type (dt)
NSString *const kMPMessageTypeKey = @"dt";
diff --git a/mParticle-Apple-SDK/Network/MPNetworkCommunication.m b/mParticle-Apple-SDK/Network/MPNetworkCommunication.m
index 041687959..6d172972c 100644
--- a/mParticle-Apple-SDK/Network/MPNetworkCommunication.m
+++ b/mParticle-Apple-SDK/Network/MPNetworkCommunication.m
@@ -68,6 +68,8 @@ @interface MParticle ()
@property (nonatomic, strong, readonly) MPBackendController_PRIVATE *backendController;
- (void)logKitBatch:(NSString *)batch;
++ (void)executeOnMain:(void(^)(void))block;
++ (void)executeOnMainSync:(void(^)(void))block;
@end
@@ -109,9 +111,9 @@ - (instancetype)init {
if (!self) {
return nil;
}
-
+
self.identifying = NO;
-
+
return self;
}
@@ -148,11 +150,11 @@ - (MPURL *)configURL {
if (_configURL) {
return _configURL;
}
-
+
MPStateMachine_PRIVATE *stateMachine = [MParticle sharedInstance].stateMachine;
MPApplication_PRIVATE *application = [[MPApplication_PRIVATE alloc] init];
NSString *configHost = [MParticle sharedInstance].networkOptions.configHost ?: kMPURLHostConfig;
-
+
NSString *dataPlanConfigString;
NSString *dataPlanId = MParticle.sharedInstance.dataPlanId;
if (dataPlanId != nil) {
@@ -172,7 +174,7 @@ - (MPURL *)configURL {
NSURL *defaultURL = [NSURL URLWithString:urlString];
urlString = [NSString stringWithFormat:configURLFormat, kMPURLScheme, configHost, kMPConfigVersion, stateMachine.apiKey, kMPConfigURL, [application.version percentEscape], kMParticleSDKVersion];
-
+
if ([MParticle sharedInstance].networkOptions.overridesConfigSubdirectory) {
NSString *configURLFormat = [urlFormatOverride stringByAppendingString:@"?av=%@&sv=%@"];
urlString = [NSString stringWithFormat:configURLFormat, kMPURLScheme, configHost, stateMachine.apiKey, kMPConfigURL, [application.version percentEscape], kMParticleSDKVersion];
@@ -180,7 +182,7 @@ - (MPURL *)configURL {
if (dataPlanConfigString) {
urlString = [NSString stringWithFormat:@"%@%@", urlString, dataPlanConfigString];
}
-
+
NSURL *modifiedURL = [NSURL URLWithString:urlString];
if (modifiedURL && defaultURL) {
_configURL = [[MPURL alloc] initWithURL:modifiedURL defaultURL:defaultURL];
@@ -198,13 +200,13 @@ - (MPURL *)eventURLForUpload:(MPUpload *)mpUpload {
}
NSString *urlString = [NSString stringWithFormat:urlFormat, kMPURLScheme, self.defaultEventHost, kMPEventsVersion, mpUpload.uploadSettings.apiKey, kMPEventsURL];
NSURL *defaultURL = [NSURL URLWithString:urlString];
-
+
if (mpUpload.uploadSettings.overridesEventsSubdirectory) {
urlString = [NSString stringWithFormat:urlFormatOverride, kMPURLScheme, eventHost, mpUpload.uploadSettings.apiKey, kMPEventsURL];
} else {
urlString = [NSString stringWithFormat:urlFormat, kMPURLScheme, eventHost, kMPEventsVersion, mpUpload.uploadSettings.apiKey, kMPEventsURL];
}
-
+
NSURL *modifiedURL = [NSURL URLWithString:urlString];
MPURL *eventURL;
if (modifiedURL && defaultURL) {
@@ -215,7 +217,7 @@ - (MPURL *)eventURLForUpload:(MPUpload *)mpUpload {
- (MPURL *)audienceURL {
MPStateMachine_PRIVATE *stateMachine = [MParticle sharedInstance].stateMachine;
-
+
NSString *eventHost = [MParticle sharedInstance].networkOptions.eventsHost ?: self.defaultEventHost;
NSString *audienceURLFormat = [audienceFormat stringByAppendingString:@"?mpid=%@"];
NSString *urlString = [NSString stringWithFormat:audienceURLFormat, kMPURLScheme, self.defaultEventHost, kMPAudienceVersion, stateMachine.apiKey, kMPAudienceURL, [MPPersistenceController_PRIVATE mpId]];
@@ -228,16 +230,16 @@ - (MPURL *)audienceURL {
audienceURLFormat = [urlFormat stringByAppendingString:@"?mpid=%@"];
urlString = [NSString stringWithFormat:audienceURLFormat, kMPURLScheme, eventHost, kMPAudienceVersion, stateMachine.apiKey, kMPAudienceURL, [MPPersistenceController_PRIVATE mpId]];
}
-
+
NSURL *modifiedURL = [NSURL URLWithString:urlString];
defaultURL.accessibilityHint = @"audience";
modifiedURL.accessibilityHint = @"audience";
-
+
MPURL *audienceURL;
if (modifiedURL && defaultURL) {
audienceURL = [[MPURL alloc] initWithURL:modifiedURL defaultURL:defaultURL];
}
-
+
return audienceURL;
}
@@ -245,9 +247,9 @@ - (MPURL *)identifyURL {
if (_identifyURL) {
return _identifyURL;
}
-
+
_identifyURL = [self identityURL:@"identify"];
-
+
return _identifyURL;
}
@@ -255,9 +257,9 @@ - (MPURL *)loginURL {
if (_loginURL) {
return _loginURL;
}
-
+
_loginURL = [self identityURL:@"login"];
-
+
return _loginURL;
}
@@ -265,9 +267,9 @@ - (MPURL *)logoutURL {
if (_logoutURL) {
return _logoutURL;
}
-
+
_logoutURL = [self identityURL:@"logout"];
-
+
return _logoutURL;
}
@@ -281,22 +283,22 @@ - (MPURL *)identityURL:(NSString *)pathComponent {
}
NSString *urlString = [NSString stringWithFormat:identityURLFormat, kMPURLScheme, self.defaultIdentityHost, kMPIdentityVersion, pathComponent];
NSURL *defaultURL = [NSURL URLWithString:urlString];
-
+
if ([MParticle sharedInstance].networkOptions.overridesIdentitySubdirectory) {
urlString = [NSString stringWithFormat:identityURLFormatOverride, kMPURLScheme, identityHost, pathComponent];
} else {
urlString = [NSString stringWithFormat:identityURLFormat, kMPURLScheme, identityHost, kMPIdentityVersion, pathComponent];
}
-
+
NSURL *modifiedURL = [NSURL URLWithString:urlString];
defaultURL.accessibilityHint = @"identity";
modifiedURL.accessibilityHint = @"identity";
-
+
MPURL *identityURL;
if (modifiedURL && defaultURL) {
identityURL = [[MPURL alloc] initWithURL:modifiedURL defaultURL:defaultURL];
}
-
+
return identityURL;
}
@@ -321,18 +323,18 @@ - (MPURL *)modifyURL {
NSURL *modifiedURL = [NSURL URLWithString:urlString];
defaultURL.accessibilityHint = @"identity";
modifiedURL.accessibilityHint = @"identity";
-
+
MPURL *modifyURL;
if (modifiedURL && defaultURL) {
modifyURL = [[MPURL alloc] initWithURL:modifiedURL defaultURL:defaultURL];
}
-
+
return modifyURL;
}
- (MPURL *)aliasURLForUpload:(MPUpload *)mpUpload {
NSString *pathComponent = @"alias";
-
+
NSString *eventHost;
if (mpUpload.uploadSettings.aliasTrackingHost && [MParticle sharedInstance].stateMachine.attAuthorizationStatus.integerValue == MPATTAuthorizationStatusAuthorized) {
eventHost = mpUpload.uploadSettings.aliasTrackingHost;
@@ -341,28 +343,28 @@ - (MPURL *)aliasURLForUpload:(MPUpload *)mpUpload {
}
NSString *urlString = [NSString stringWithFormat:aliasURLFormat, kMPURLScheme, self.defaultEventHost, kMPIdentityVersion, kMPIdentityKey, mpUpload.uploadSettings.apiKey, pathComponent];
NSURL *defaultURL = [NSURL URLWithString:urlString];
-
+
BOOL overrides = mpUpload.uploadSettings.overridesAliasSubdirectory;
if (!mpUpload.uploadSettings.eventsOnly && !mpUpload.uploadSettings.aliasHost) {
eventHost = mpUpload.uploadSettings.eventsHost ?: self.defaultEventHost;
overrides = mpUpload.uploadSettings.overridesEventsSubdirectory;
}
-
+
if (overrides) {
urlString = [NSString stringWithFormat:aliasURLFormatOverride, kMPURLScheme, eventHost, mpUpload.uploadSettings.apiKey, pathComponent];
} else {
urlString = [NSString stringWithFormat:aliasURLFormat, kMPURLScheme, eventHost, kMPIdentityVersion, kMPIdentityKey, mpUpload.uploadSettings.apiKey, pathComponent];
}
-
+
NSURL *modifiedURL = [NSURL URLWithString:urlString];
defaultURL.accessibilityHint = @"identity";
modifiedURL.accessibilityHint = @"identity";
-
+
MPURL *aliasURL;
if (modifiedURL && defaultURL) {
aliasURL = [[MPURL alloc] initWithURL:modifiedURL defaultURL:defaultURL];
}
-
+
return aliasURL;
}
@@ -386,7 +388,7 @@ - (void)throttleWithHTTPResponse:(NSHTTPURLResponse *)httpResponse uploadType:(M
NSTimeInterval retryAfter = 7200; // Default of 2 hours
NSTimeInterval maxRetryAfter = 86400; // Maximum of 24 hours
id suggestedRetryAfter = httpHeaders[@"Retry-After"];
-
+
if (!MPIsNull(suggestedRetryAfter)) {
if ([suggestedRetryAfter isKindOfClass:[NSString class]]) {
if ([suggestedRetryAfter containsString:@":"]) { // Date
@@ -409,7 +411,7 @@ - (void)throttleWithHTTPResponse:(NSHTTPURLResponse *)httpResponse uploadType:(M
retryAfter = MIN([(NSNumber *)suggestedRetryAfter doubleValue], maxRetryAfter);
}
}
-
+
NSDate *minUploadDate = [MParticle.sharedInstance.stateMachine minUploadDateForUploadType:uploadType];
if ([minUploadDate compare:now] == NSOrderedAscending) {
[MParticle.sharedInstance.stateMachine setMinUploadDate:[now dateByAddingTimeInterval:retryAfter] uploadType:uploadType];
@@ -424,16 +426,16 @@ - (void)throttleWithHTTPResponse:(NSHTTPURLResponse *)httpResponse uploadType:(M
- (NSNumber *)maxAgeForCache:(nonnull NSString *)cache {
NSNumber *maxAge;
cache = cache.lowercaseString;
-
+
if ([cache containsString: @"max-age="]) {
NSArray *maxAgeComponents = [cache componentsSeparatedByString:@"max-age="];
NSString *beginningOfMaxAgeString = [maxAgeComponents objectAtIndex:1];
NSArray *components = [beginningOfMaxAgeString componentsSeparatedByString:@","];
NSString *maxAgeValue = [components objectAtIndex:0];
-
+
maxAge = [NSNumber numberWithDouble:MIN([maxAgeValue doubleValue], CONFIG_REQUESTS_MAX_EXPIRATION_AGE)];
}
-
+
return maxAge;
}
@@ -445,65 +447,76 @@ - (NSNumber *)maxAgeForCache:(nonnull NSString *)cache {
return [[MPConnector alloc] init];
}
+- (UIBackgroundTaskIdentifier)beginSafeBackgroundTaskWithExpirationHandler:(void(^_Nullable)(void))handler {
+ if ([MPStateMachine_PRIVATE isAppExtension]) {
+ return UIBackgroundTaskInvalid;
+ }
+ __block UIBackgroundTaskIdentifier taskId = UIBackgroundTaskInvalid;
+ [MParticle executeOnMainSync:^{
+ taskId = [[MPApplication_PRIVATE sharedUIApplication] beginBackgroundTaskWithExpirationHandler:^{
+ if (taskId != UIBackgroundTaskInvalid) {
+ MPILogDebug(@"Background task expiration handler invoked");
+ if (handler) handler();
+ [[MPApplication_PRIVATE sharedUIApplication] endBackgroundTask:taskId];
+ taskId = UIBackgroundTaskInvalid;
+ }
+ }];
+ }];
+ return taskId;
+}
+
+- (void)endSafeBackgroundTask:(UIBackgroundTaskIdentifier)taskId {
+ if (taskId == UIBackgroundTaskInvalid) return;
+ [MParticle executeOnMain:^{
+ [[MPApplication_PRIVATE sharedUIApplication] endBackgroundTask:taskId];
+ }];
+}
+
- (void)requestConfig:(nullable NSObject *)connector withCompletionHandler:(void(^)(BOOL success))completionHandler {
MPUserDefaults *userDefaults = MPUserDefaultsConnector.userDefaults;
BOOL shouldSendRequest = [userDefaults isConfigurationExpired];
-
+
if (!shouldSendRequest) {
completionHandler(YES);
return;
}
-
+
MPILogVerbose(@"Starting config request");
NSTimeInterval start = [[NSDate date] timeIntervalSince1970];
-
- __block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid;
-
- if (![MPStateMachine_PRIVATE isAppExtension]) {
- backgroundTaskIdentifier = [[MPApplication_PRIVATE sharedUIApplication] beginBackgroundTaskWithExpirationHandler:^{
- if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
- [[MPApplication_PRIVATE sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier];
- backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }];
- }
-
+
+ UIBackgroundTaskIdentifier backgroundTaskIdentifier = [self beginSafeBackgroundTaskWithExpirationHandler:nil];
+
connector = connector ? connector : [self makeConnector];
NSObject *response = [connector responseFromGetRequestToURL:self.configURL];
NSData *data = response.data;
NSHTTPURLResponse *httpResponse = response.httpResponse;
-
+
NSString *cacheControl = httpResponse.allHeaderFields[kMPHTTPCacheControlHeaderKey];
NSString *ageString = httpResponse.allHeaderFields[kMPHTTPAgeHeaderKey];
NSNumber *maxAge = [self maxAgeForCache:cacheControl];
-
- if (![MPStateMachine_PRIVATE isAppExtension]) {
- if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
- [[MPApplication_PRIVATE sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier];
- backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }
-
+
+ [self endSafeBackgroundTask:backgroundTaskIdentifier];
+
NSInteger responseCode = [httpResponse statusCode];
MPILogVerbose(@"Config Response Code: %ld, Execution Time: %.2fms", (long)responseCode, ([[NSDate date] timeIntervalSince1970] - start) * 1000.0);
-
+
if (responseCode == HTTPStatusCodeNotModified) {
MPILogDebug(@"Config response 304 Not Modified - using cached config");
MPUserDefaults *userDefaults = MPUserDefaultsConnector.userDefaults;
[userDefaults setConfiguration:[userDefaults getConfiguration] eTag:userDefaults[kMPHTTPETagHeaderKey] requestTimestamp:[[NSDate date] timeIntervalSince1970] currentAge:ageString.doubleValue maxAge:maxAge];
-
+
completionHandler(YES);
return;
}
-
+
BOOL success = responseCode == HTTPStatusCodeSuccess || responseCode == HTTPStatusCodeAccepted;
-
+
if (!data && success) {
completionHandler(NO);
MPILogError(@"Config request failed - no data received (responseCode: %ld). Kits may not initialize.", (long)responseCode);
return;
}
-
+
success = success && [data length] > 0;
NSDictionary *configurationDictionary = nil;
@@ -517,7 +530,7 @@ - (void)requestConfig:(nullable NSObject *)connector withCo
responseCode = HTTPStatusCodeNoContent;
}
}
-
+
if (success && configurationDictionary) {
NSDictionary *headersDictionary = [httpResponse allHeaderFields];
NSString *eTag = headersDictionary[kMPHTTPETagHeaderKey];
@@ -528,7 +541,7 @@ - (void)requestConfig:(nullable NSObject *)connector withCo
MPUserDefaults *userDefaults = MPUserDefaultsConnector.userDefaults;
[userDefaults setConfiguration:configurationDictionary eTag:eTag requestTimestamp:[[NSDate date] timeIntervalSince1970] currentAge:ageString.doubleValue maxAge:maxAge];
}
-
+
completionHandler(success);
} else {
MPILogError(@"Config request failed - could not parse response or wrong message type (responseCode: %ld). Kits may not initialize.", (long)responseCode);
@@ -537,17 +550,8 @@ - (void)requestConfig:(nullable NSObject *)connector withCo
}
- (void)requestAudiencesWithCompletionHandler:(MPAudienceResponseHandler)completionHandler {
- __block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid;
-
- if (![MPStateMachine_PRIVATE isAppExtension]) {
- backgroundTaskIdentifier = [[MPApplication_PRIVATE sharedUIApplication] beginBackgroundTaskWithExpirationHandler:^{
- if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
- [[MPApplication_PRIVATE sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier];
- backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }];
- }
-
+ UIBackgroundTaskIdentifier backgroundTaskIdentifier = [self beginSafeBackgroundTaskWithExpirationHandler:nil];
+
__weak MPNetworkCommunication_PRIVATE *weakSelf = self;
NSObject *connector = [self makeConnector];
@@ -559,14 +563,9 @@ - (void)requestAudiencesWithCompletionHandler:(MPAudienceResponseHandler)complet
completionHandler(NO, nil, nil);
return;
}
-
- if (![MPStateMachine_PRIVATE isAppExtension]) {
- if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
- [[MPApplication_PRIVATE sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier];
- backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }
-
+
+ [self endSafeBackgroundTask:backgroundTaskIdentifier];
+
if (!data) {
NSError *audienceError = [NSError errorWithDomain:@"mParticle Audiences"
code:httpResponse.statusCode
@@ -574,18 +573,18 @@ - (void)requestAudiencesWithCompletionHandler:(MPAudienceResponseHandler)complet
completionHandler(NO, nil, audienceError);
return;
}
-
+
NSMutableArray *currentAudiences = nil;
BOOL success = NO;
-
+
NSArray *audiencesList = nil;
NSInteger responseCode = [httpResponse statusCode];
success = (responseCode == HTTPStatusCodeSuccess || responseCode == HTTPStatusCodeAccepted) && [data length] > 0;
-
+
if (success) {
NSError *serializationError = nil;
NSDictionary *audiencesDictionary = nil;
-
+
@try {
audiencesDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&serializationError];
success = serializationError == nil;
@@ -594,29 +593,29 @@ - (void)requestAudiencesWithCompletionHandler:(MPAudienceResponseHandler)complet
success = NO;
MPILogError(@"Audiences Error: %@", [exception reason]);
}
-
+
if (success) {
audiencesList = audiencesDictionary[kMPAudienceMembershipKey];
}
-
+
if (audiencesList.count > 0) {
currentAudiences = [[NSMutableArray alloc] init];
-
+
for (NSDictionary *audienceDictionary in audiencesList) {
MPAudience *audience = [[MPAudience alloc] initWithAudienceId:audienceDictionary[kMPAudienceIdKey]];
[currentAudiences addObject:audience];
}
-
+
MPILogVerbose(@"Audiences Response Code: %ld", (long)responseCode);
} else {
MPILogWarning(@"Audiences Error - Response Code: %ld", (long)responseCode);
}
}
-
+
if (currentAudiences.count == 0) {
currentAudiences = nil;
}
-
+
NSError *audienceError = nil;
if (responseCode == HTTPStatusCodeForbidden) {
@@ -624,7 +623,7 @@ - (void)requestAudiencesWithCompletionHandler:(MPAudienceResponseHandler)complet
code:responseCode
userInfo:@{@"message":@"Audiences not enabled for this org."}];
}
-
+
completionHandler(success, currentAudiences, audienceError);
}
@@ -633,24 +632,24 @@ - (BOOL)performMessageUpload:(MPUpload *)upload {
if ([minUploadDate compare:[NSDate date]] == NSOrderedDescending) {
return YES; //stop upload loop
}
-
+
MPURL *eventURL = [self eventURLForUpload:upload];
-
+
NSString *uploadString = [upload serializedString];
NSObject *connector = [self makeConnector];
-
+
MPILogVerbose(@"Beginning upload for upload ID: %@", upload.uuid);
-
+
NSData *zipUploadData;
NSNumber *authTimestamp = [MParticle sharedInstance].stateMachine.attAuthorizationTimestamp;
NSNumber *authStatus = [MParticle sharedInstance].stateMachine.attAuthorizationStatus;
-
+
if (authStatus != nil && authTimestamp != nil) {
NSDictionary *uploadDictionary = [NSJSONSerialization JSONObjectWithData:upload.uploadData options:0 error:nil];
NSMutableDictionary *uploadDict = [uploadDictionary mutableCopy];
-
+
NSMutableDictionary *deviceDict = [uploadDict[kMPDeviceInformationKey] mutableCopy];
-
+
switch (authStatus.integerValue) {
case MPATTAuthorizationStatusNotDetermined:
deviceDict[kMPATT] = @"not_determined";
@@ -670,11 +669,11 @@ - (BOOL)performMessageUpload:(MPUpload *)upload {
default:
break;
}
-
+
deviceDict[kMPATTTimestamp] = authTimestamp;
-
+
uploadDict[kMPDeviceInformationKey] = [deviceDict copy];
-
+
NSData *updatedData = [NSJSONSerialization dataWithJSONObject:[uploadDict copy] options:0 error:nil];
uploadString = [[NSString alloc] initWithData:updatedData encoding:NSUTF8StringEncoding];
@@ -682,20 +681,20 @@ - (BOOL)performMessageUpload:(MPUpload *)upload {
} else {
zipUploadData = [MPZipPRIVATE compressedDataFromData:upload.uploadData];
}
-
+
if (zipUploadData == nil || zipUploadData.length <= 0) {
[[MParticle sharedInstance].persistenceController deleteUpload:upload];
return NO;
}
NSTimeInterval start = [[NSDate date] timeIntervalSince1970];
-
+
NSObject *response = [connector responseFromPostRequestToURL:eventURL
message:uploadString
serializedParams:zipUploadData
secret:upload.uploadSettings.secret];
NSData *data = response.data;
NSHTTPURLResponse *httpResponse = response.httpResponse;
-
+
NSInteger responseCode = [httpResponse statusCode];
MPILogVerbose(@"Upload response code: %ld", (long)responseCode);
BOOL isSuccessCode = responseCode >= 200 && responseCode < 300;
@@ -706,7 +705,7 @@ - (BOOL)performMessageUpload:(MPUpload *)upload {
[[MParticle sharedInstance] logKitBatch:uploadString];
}
}
-
+
BOOL success = isSuccessCode && data && [data length] > 0;
if (success) {
@try {
@@ -718,25 +717,25 @@ - (BOOL)performMessageUpload:(MPUpload *)upload {
[MPNetworkCommunication_PRIVATE parseConfiguration:responseDictionary];
}
MPILogVerbose(@"Upload complete: %@\n", uploadString);
-
+
} @catch (NSException *exception) {
MPILogError(@"Upload error: %@", [exception reason]);
}
}
-
+
MPILogVerbose(@"Upload execution time: %.2fms", ([[NSDate date] timeIntervalSince1970] - start) * 1000.0);
-
+
// 429, 503
if (responseCode == HTTPStatusCodeServiceUnavailable || responseCode == HTTPStatusCodeTooManyRequests) {
[self throttleWithHTTPResponse:httpResponse uploadType:MPUploadTypeMessage];
return YES;
}
-
+
//5xx, 0, 999, -1, etc
if (!isSuccessCode && !isInvalidCode) {
return YES;
}
-
+
return NO;
}
@@ -745,47 +744,47 @@ - (BOOL)performAliasUpload:(MPUpload *)upload {
if ([minUploadDate compare:[NSDate date]] == NSOrderedDescending) {
return YES; //stop upload loop
}
-
+
MPURL *aliasURL = [self aliasURLForUpload:upload];
-
+
NSString *uploadString = [upload serializedString];
NSObject *connector = [self makeConnector];
-
+
MPILogVerbose(@"Beginning alias request with upload ID: %@", upload.uuid);
-
+
if (upload.uploadData == nil || upload.uploadData.length <= 0) {
[[MParticle sharedInstance].persistenceController deleteUpload:upload];
return NO;
}
NSTimeInterval start = [[NSDate date] timeIntervalSince1970];
-
+
MPILogVerbose(@"Alias request:\nURL: %@ \nBody:%@", aliasURL.url, uploadString);
-
+
NSObject *response = [connector responseFromPostRequestToURL:aliasURL
message:uploadString
serializedParams:upload.uploadData
secret:upload.uploadSettings.secret];
NSData *data = response.data;
NSHTTPURLResponse *httpResponse = response.httpResponse;
-
+
NSInteger responseCode = [httpResponse statusCode];
MPILogVerbose(@"Alias response code: %ld", (long)responseCode);
-
+
BOOL isSuccessCode = responseCode >= 200 && responseCode < 300;
BOOL isInvalidCode = responseCode != 429 && responseCode >= 400 && responseCode < 500;
if (isSuccessCode || isInvalidCode) {
[[MParticle sharedInstance].persistenceController deleteUpload:upload];
}
-
+
NSString *responseString = [[NSString alloc] initWithData:response.data encoding:NSUTF8StringEncoding];
if (responseString != nil && responseString.length > 0) {
MPILogVerbose(@"Alias response:\n%@", responseString);
}
-
+
MPAliasResponse *aliasResponse = [[MPAliasResponse alloc] init];
aliasResponse.responseCode = responseCode;
aliasResponse.willRetry = NO;
-
+
NSDictionary *requestDictionary = [NSJSONSerialization JSONObjectWithData:upload.uploadData options:0 error:nil];
NSNumber *sourceMPID = requestDictionary[@"source_mpid"];
NSNumber *destinationMPID = requestDictionary[@"destination_mpid"];
@@ -795,7 +794,7 @@ - (BOOL)performAliasUpload:(MPUpload *)upload {
NSDate *endTime = [NSDate dateWithTimeIntervalSince1970:endTimeNumber.doubleValue/1000];
aliasResponse.requestID = requestDictionary[@"request_id"];
aliasResponse.request = [MPAliasRequest requestWithSourceMPID:sourceMPID destinationMPID:destinationMPID startTime:startTime endTime:endTime];
-
+
if (!isSuccessCode && data && data.length > 0) {
@try {
NSError *serializationError = nil;
@@ -810,36 +809,27 @@ - (BOOL)performAliasUpload:(MPUpload *)upload {
MPILogError(@"Alias error: %@", [exception reason]);
}
}
-
+
MPILogVerbose(@"Alias execution time: %.2fms", ([[NSDate date] timeIntervalSince1970] - start) * 1000.0);
-
+
// 429, 503
if (responseCode == HTTPStatusCodeServiceUnavailable || responseCode == HTTPStatusCodeTooManyRequests) {
aliasResponse.willRetry = YES;
[self throttleWithHTTPResponse:httpResponse uploadType:upload.uploadType];
return YES;
}
-
+
//5xx, 0, 999, -1, etc
if (!isSuccessCode && !isInvalidCode) {
return YES;
}
-
+
return NO;
}
- (void)upload:(NSArray *)uploads completionHandler:(MPUploadsCompletionHandler)completionHandler {
- __block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid;
-
- if (![MPStateMachine_PRIVATE isAppExtension]) {
- backgroundTaskIdentifier = [[MPApplication_PRIVATE sharedUIApplication] beginBackgroundTaskWithExpirationHandler:^{
- if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
- [[MPApplication_PRIVATE sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier];
- backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }];
- }
-
+ UIBackgroundTaskIdentifier backgroundTaskIdentifier = [self beginSafeBackgroundTaskWithExpirationHandler:nil];
+
for (int index = 0; index < uploads.count; index++) {
@autoreleasepool {
MPUpload *upload = uploads[index];
@@ -854,18 +844,13 @@ - (void)upload:(NSArray *)uploads completionHandler:(MPUploadsComple
}
}
}
-
- if (![MPStateMachine_PRIVATE isAppExtension]) {
- if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
- [[MPApplication_PRIVATE sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier];
- backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }
+
+ [self endSafeBackgroundTask:backgroundTaskIdentifier];
completionHandler();
}
- (void)identityApiRequestWithURL:(NSURL*)url identityRequest:(MPIdentityHTTPBaseRequest *_Nonnull)identityRequest blockOtherRequests: (BOOL) blockOtherRequests completion:(nullable MPIdentityApiManagerCallback)completion {
-
+
if (self.identifying) {
MPILogWarning(@"Identity API request blocked - another identity request is already in progress");
if (completion) {
@@ -873,7 +858,7 @@ - (void)identityApiRequestWithURL:(NSURL*)url identityRequest:(MPIdentityHTTPBas
}
return;
}
-
+
if ([MParticle sharedInstance].stateMachine.optOut) {
MPILogWarning(@"Identity API request blocked - SDK is opted out");
if (completion) {
@@ -881,11 +866,11 @@ - (void)identityApiRequestWithURL:(NSURL*)url identityRequest:(MPIdentityHTTPBas
}
return;
}
-
+
if (blockOtherRequests) {
self.identifying = YES;
}
-
+
MPEndpoint endpointType;
MPURL *mpURL;
if ([self.identifyURL.url.absoluteString isEqualToString:url.absoluteString]) {
@@ -901,26 +886,25 @@ - (void)identityApiRequestWithURL:(NSURL*)url identityRequest:(MPIdentityHTTPBas
endpointType = MPEndpointIdentityModify;
mpURL = self.modifyURL;
}
-
+
NSTimeInterval start = [[NSDate date] timeIntervalSince1970];
-
+
NSDictionary *dictionary = [identityRequest dictionaryRepresentation];
NSData *data = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:nil];
NSString *jsonRequest = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
-
+
MPILogVerbose(@"Identity request:\nURL: %@ \nBody:%@", url, jsonRequest);
-
-
+
BOOL success = NO;
NSError *error = nil;
NSDictionary *responseDictionary = nil;
NSString *responseString = nil;
NSInteger responseCode = 0;
-
+
BOOL enableIdentityCaching = MParticle.sharedInstance.stateMachine.enableIdentityCaching;
BOOL usedCachedResponse = NO;
-
+
// Try to use the cache if enabled
if (enableIdentityCaching) {
MPIdentityCachedResponse *cachedResponse = [MPIdentityCaching getCachedIdentityResponseForEndpoint:endpointType identityRequest:identityRequest];
@@ -929,7 +913,7 @@ - (void)identityApiRequestWithURL:(NSURL*)url identityRequest:(MPIdentityHTTPBas
NSError *serializationError = nil;
responseString = [[NSString alloc] initWithData:cachedResponse.bodyData encoding:NSUTF8StringEncoding];
responseDictionary = [NSJSONSerialization JSONObjectWithData:cachedResponse.bodyData options:0 error:&serializationError];
-
+
if (serializationError) {
responseDictionary = nil;
success = NO;
@@ -947,52 +931,38 @@ - (void)identityApiRequestWithURL:(NSURL*)url identityRequest:(MPIdentityHTTPBas
MPILogError(@"Identity response serialization error: %@", [exception reason]);
}
}
- }
-
+ }
+
if (!usedCachedResponse) {
- __block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid;
-
- if (![MPStateMachine_PRIVATE isAppExtension]) {
- backgroundTaskIdentifier = [[MPApplication_PRIVATE sharedUIApplication] beginBackgroundTaskWithExpirationHandler:^{
- if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
- self.identifying = NO;
-
- [[MPApplication_PRIVATE sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier];
- backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }];
- }
-
+ UIBackgroundTaskIdentifier backgroundTaskIdentifier = [self beginSafeBackgroundTaskWithExpirationHandler:^{
+ self.identifying = NO;
+ }];
+
NSObject *connector = [self makeConnector];
NSObject *response = [connector responseFromPostRequestToURL:mpURL
message:nil
- serializedParams:data
+ serializedParams:data
secret:nil];
-
+
NSData *responseData = response.data;
error = response.error;
NSHTTPURLResponse *httpResponse = response.httpResponse;
-
- if (![MPStateMachine_PRIVATE isAppExtension]) {
- if (backgroundTaskIdentifier != UIBackgroundTaskInvalid) {
- [[MPApplication_PRIVATE sharedUIApplication] endBackgroundTask:backgroundTaskIdentifier];
- backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }
-
+
+ [self endSafeBackgroundTask:backgroundTaskIdentifier];
+
responseCode = [httpResponse statusCode];
success = responseCode == HTTPStatusCodeSuccess || responseCode == HTTPStatusCodeAccepted;
success = success && [responseData length] > 0;
-
-
+
+
MPILogVerbose(@"Identity response code: %ld", (long)responseCode);
-
+
if (success) {
@try {
NSError *serializationError = nil;
responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
responseDictionary = [NSJSONSerialization JSONObjectWithData:responseData options:0 error:&serializationError];
-
+
if (responseDictionary && !serializationError) {
// Cache response if it contains the custom max age header and the feature is enabled
if (enableIdentityCaching) {
@@ -1018,11 +988,11 @@ - (void)identityApiRequestWithURL:(NSURL*)url identityRequest:(MPIdentityHTTPBas
}
}
}
-
+
MPILogVerbose(@"Identity execution time: %.2fms", ([[NSDate date] timeIntervalSince1970] - start) * 1000.0);
-
+
self.identifying = NO;
-
+
if (success) {
if (responseString) {
MPILogVerbose(@"Identity response:\n%@", responseString);
@@ -1068,7 +1038,7 @@ - (void)identify:(MPIdentityApiRequest *_Nonnull)identifyRequest completion:(nul
if (!userDefaults[kMPATT] && identifyRequest.identities[@(MPIdentityIOSAdvertiserId)]) {
MPILogDebug(@"The IDFA was supplied but the App Tracking Transparency Status not set with [[MParticle sharedInstance] setATTStatus:withATTStatusTimestampMillis:]");
}
-
+
MPIdentifyHTTPRequest *request = [[MPIdentifyHTTPRequest alloc] initWithIdentityApiRequest:identifyRequest];
[self identityApiRequestWithURL:self.identifyURL.url identityRequest:request blockOtherRequests: YES completion:completion];
}
@@ -1078,7 +1048,7 @@ - (void)login:(MPIdentityApiRequest *_Nullable)loginRequest completion:(nullable
if (!userDefaults[kMPATT] && loginRequest.identities[@(MPIdentityIOSAdvertiserId)]) {
MPILogDebug(@"The IDFA was supplied but the App Tracking Transparency Status not set with [[MParticle sharedInstance] setATTStatus:withATTStatusTimestampMillis:]");
}
-
+
MPIdentifyHTTPRequest *request = [[MPIdentifyHTTPRequest alloc] initWithIdentityApiRequest:loginRequest];
[self identityApiRequestWithURL:self.loginURL.url identityRequest:request blockOtherRequests: YES completion:completion];
}
@@ -1089,7 +1059,7 @@ - (void)logout:(MPIdentityApiRequest *_Nullable)logoutRequest completion:(nullab
if (!userDefaults[kMPATT] && logoutRequest.identities[@(MPIdentityIOSAdvertiserId)]) {
MPILogDebug(@"The IDFA was supplied but the App Tracking Transparency Status not set with [[MParticle sharedInstance] setATTStatus:withATTStatusTimestampMillis:]");
}
-
+
MPIdentifyHTTPRequest *request = [[MPIdentifyHTTPRequest alloc] initWithIdentityApiRequest:logoutRequest];
[self identityApiRequestWithURL:self.logoutURL.url identityRequest:request blockOtherRequests: YES completion:completion];
}
@@ -1099,19 +1069,19 @@ - (void)modify:(MPIdentityApiRequest *_Nonnull)modifyRequest completion:(nullabl
if (!userDefaults[kMPATT] && modifyRequest.identities[@(MPIdentityIOSAdvertiserId)]) {
MPILogDebug(@"The IDFA was supplied but the App Tracking Transparency Status not set with [[MParticle sharedInstance] setATTStatus:withATTStatusTimestampMillis:]");
}
-
+
NSMutableArray *identityChanges = [NSMutableArray array];
-
+
NSDictionary *identitiesDictionary = modifyRequest.identities;
NSDictionary *existingIdentities = [MParticle sharedInstance].identity.currentUser.identities;
-
+
[identitiesDictionary enumerateKeysAndObjectsUsingBlock:^(NSNumber * _Nonnull identityType, NSString *value, BOOL * _Nonnull stop) {
NSString *oldValue = existingIdentities[identityType];
-
+
if ((NSNull *)value == [NSNull null]) {
value = nil;
}
-
+
if (!oldValue || ![value isEqualToString:oldValue]) {
MPIdentity userIdentity = (MPIdentity)[identityType intValue];
NSString *stringType = [MPIdentityHTTPIdentities stringForIdentityType:userIdentity];
@@ -1119,9 +1089,9 @@ - (void)modify:(MPIdentityApiRequest *_Nonnull)modifyRequest completion:(nullabl
[identityChanges addObject:identityChange];
}
}];
-
+
[self modifyWithIdentityChanges:identityChanges blockOtherRequests:YES completion:completion];
-
+
}
- (void)modifyDeviceID:(NSString *_Nonnull)deviceIdType value:(NSString *_Nonnull)value oldValue:(NSString *_Nonnull)oldValue {
@@ -1132,7 +1102,7 @@ - (void)modifyDeviceID:(NSString *_Nonnull)deviceIdType value:(NSString *_Nonnul
}
- (void)modifyWithIdentityChanges:(NSArray *)identityChanges blockOtherRequests:(BOOL)blockOtherRequests completion:(nullable MPIdentityApiManagerModifyCallback)completion {
-
+
if (identityChanges == nil || identityChanges.count == 0) {
if (completion) {
completion([[MPIdentityHTTPModifySuccessResponse alloc] init], nil);
@@ -1160,7 +1130,7 @@ + (void)parseConfiguration:(nonnull NSDictionary *)configuration {
if (MPIsNull(configuration) || MPIsNull(configuration[kMPMessageTypeKey])) {
return;
}
-
+
MPPersistenceController_PRIVATE *persistence = [MParticle sharedInstance].persistenceController;
// Consumer Information
diff --git a/mParticle-Apple-SDK/Network/MPURL.h b/mParticle-Apple-SDK/Network/MPURL.h
index 36694cb14..5fd951cf6 100644
--- a/mParticle-Apple-SDK/Network/MPURL.h
+++ b/mParticle-Apple-SDK/Network/MPURL.h
@@ -4,8 +4,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface MPURL : NSObject
-@property (nonatomic, strong, nonnull) NSURL *url;
-@property (nonatomic, strong, nonnull) NSURL *defaultURL;
+@property (nonatomic, strong, readonly, nonnull) NSURL *url;
+@property (nonatomic, strong, readonly, nonnull) NSURL *defaultURL;
- (nonnull instancetype)initWithURL:(nonnull NSURL *)url defaultURL:(nonnull NSURL *)defaultURL;
diff --git a/mParticle-Apple-SDK/Network/MPURLRequestBuilder.h b/mParticle-Apple-SDK/Network/MPURLRequestBuilder.h
index 884962d30..8542e54f5 100644
--- a/mParticle-Apple-SDK/Network/MPURLRequestBuilder.h
+++ b/mParticle-Apple-SDK/Network/MPURLRequestBuilder.h
@@ -16,6 +16,6 @@
- (nonnull MPURLRequestBuilder *)withHttpMethod:(nonnull NSString *)httpMethod;
- (nonnull MPURLRequestBuilder *)withPostData:(nullable NSData *)postData;
- (nonnull MPURLRequestBuilder *)withSecret:(nullable NSString *)secret;
-- (nonnull NSMutableURLRequest *)build;
+- (nullable NSMutableURLRequest *)build;
@end
diff --git a/mParticle-Apple-SDK/Network/MPURLRequestBuilder.m b/mParticle-Apple-SDK/Network/MPURLRequestBuilder.m
index edfa04b7d..06afeba78 100644
--- a/mParticle-Apple-SDK/Network/MPURLRequestBuilder.m
+++ b/mParticle-Apple-SDK/Network/MPURLRequestBuilder.m
@@ -11,8 +11,6 @@
#import "MPUserDefaultsConnector.h"
@import mParticle_Apple_SDK_Swift;
-static NSDateFormatter *RFC1123DateFormatter;
-
@interface MParticle ()
@property (nonatomic, strong, readonly) MPStateMachine_PRIVATE *stateMachine;
@@ -34,15 +32,6 @@ @interface MPURLRequestBuilder() {
@implementation MPURLRequestBuilder
-+ (void)initialize {
- if (self == [MPURLRequestBuilder class]) {
- RFC1123DateFormatter = [[NSDateFormatter alloc] init];
- RFC1123DateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
- RFC1123DateFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"GMT"];
- RFC1123DateFormatter.dateFormat = @"EEE',' dd MMM yyyy HH':'mm':'ss 'GMT'";
- }
-}
-
- (instancetype)initWithURL:(MPURL *)url {
self = [super init];
if (!self || !url) {
@@ -144,6 +133,16 @@ - (MPURLRequestBuilder *)withSecret:(nullable NSString *)secret {
}
- (NSMutableURLRequest *)build {
+ if (!_url.url) {
+ MPILogError(@"Cannot build URL request — URL is nil");
+ return nil;
+ }
+
+ if (!_url.defaultURL) {
+ MPILogError(@"Cannot build URL request — defaultURL is nil");
+ return nil;
+ }
+
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:_url.url];
[urlRequest setCachePolicy:NSURLRequestReloadIgnoringLocalCacheData];
[urlRequest setTimeoutInterval:[MPURLRequestBuilder requestTimeout]];
@@ -152,18 +151,30 @@ - (NSMutableURLRequest *)build {
BOOL isIdentityRequest = [urlRequest.URL.accessibilityHint isEqualToString:@"identity"];
BOOL isAudienceRequest = [urlRequest.URL.accessibilityHint isEqualToString:@"audience"];
- NSString *date = [RFC1123DateFormatter stringFromDate:[NSDate date]];
+ NSString *date = [MPDateFormatter stringFromDateRFC1123:[NSDate date]] ?: @"";
NSString *secret = _secret ?: [MParticle sharedInstance].stateMachine.secret;
+ NSString *apiKey = [MParticle sharedInstance].stateMachine.apiKey;
if (isAudienceRequest) {
- NSString *audienceSignature = [NSString stringWithFormat:@"%@\n%@\n%@?%@", _httpMethod, date, [urlRequest.URL relativePath], [urlRequest.URL query]];
+ NSString *audienceRelativePath = [urlRequest.URL relativePath];
+ if (!audienceRelativePath) {
+ MPILogError(@"Cannot build URL request — audience relative path is nil");
+ return nil;
+ }
+ NSString *audienceQuery = [urlRequest.URL query];
+ NSString *audienceSignature;
+ if (audienceQuery) {
+ audienceSignature = [NSString stringWithFormat:@"%@\n%@\n%@?%@", _httpMethod, date, audienceRelativePath, audienceQuery];
+ } else {
+ audienceSignature = [NSString stringWithFormat:@"%@\n%@\n%@", _httpMethod, date, audienceRelativePath];
+ }
MPILogVerbose(@"Audience Signature:\n%@", audienceSignature);
NSString *hmacSha256Encode = [self hmacSha256Encode:audienceSignature key:secret];
if (hmacSha256Encode) {
[urlRequest setValue:hmacSha256Encode forHTTPHeaderField:@"x-mp-signature"];
}
[urlRequest setValue:date forHTTPHeaderField:@"Date"];
- [urlRequest setValue:[MParticle sharedInstance].stateMachine.apiKey forHTTPHeaderField:@"x-mp-key"];
+ [urlRequest setValue:apiKey forHTTPHeaderField:@"x-mp-key"];
NSString *userAgent = [self userAgent];
if (userAgent) {
[urlRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
@@ -175,16 +186,28 @@ - (NSMutableURLRequest *)build {
NSString *contentType = nil;
NSString *kits = nil;
NSString *relativePath = [_url.defaultURL relativePath];
+ if (!relativePath) {
+ MPILogError(@"Cannot build URL request — relative path is nil");
+ return nil;
+ }
NSString *signatureMessage;
NSTimeZone *timeZone = [NSTimeZone defaultTimeZone];
- NSString *secondsFromGMT = [NSString stringWithFormat:@"%ld", (unsigned long)[timeZone secondsFromGMT]];
+ NSString *secondsFromGMT = [NSString stringWithFormat:@"%ld", (long)[timeZone secondsFromGMT]];
NSRange range;
BOOL containsMessage = _message != nil;
if (isIdentityRequest) { // /identify, /login, /logout, //modify
contentType = @"application/json";
- [urlRequest setValue:[MParticle sharedInstance].stateMachine.apiKey forHTTPHeaderField:@"x-mp-key"];
+ [urlRequest setValue:apiKey forHTTPHeaderField:@"x-mp-key"];
+ if (!_postData) {
+ MPILogError(@"Cannot build URL request — post data is nil for identity request");
+ return nil;
+ }
NSString *postDataString = [[NSString alloc] initWithData:_postData encoding:NSUTF8StringEncoding];
+ if (!postDataString) {
+ MPILogError(@"Cannot build URL request — failed to encode post data as UTF-8");
+ return nil;
+ }
signatureMessage = [NSString stringWithFormat:@"%@\n%@\n%@%@", _httpMethod, date, relativePath, postDataString];
} else if (containsMessage) { // /events
contentType = @"application/json";
@@ -221,7 +244,11 @@ - (NSMutableURLRequest *)build {
}
NSString *query = [_url.defaultURL query];
- signatureMessage = [NSString stringWithFormat:@"%@\n%@\n%@?%@", _httpMethod, date, relativePath, query];
+ if (query) {
+ signatureMessage = [NSString stringWithFormat:@"%@\n%@\n%@?%@", _httpMethod, date, relativePath, query];
+ } else {
+ signatureMessage = [NSString stringWithFormat:@"%@\n%@\n%@", _httpMethod, date, relativePath];
+ }
}
NSString *hmacSha256Encode = [self hmacSha256Encode:signatureMessage key:secret];
diff --git a/mParticle_Apple_SDK.json b/mParticle_Apple_SDK.json
index 9c4461e4b..485f9be55 100644
--- a/mParticle_Apple_SDK.json
+++ b/mParticle_Apple_SDK.json
@@ -125,5 +125,6 @@
"8.41.1": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.41.1/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.41.1/mParticle_Apple_SDK.xcframework.zip",
"8.42.0": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.0/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.0/mParticle_Apple_SDK.xcframework.zip",
"8.42.1": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.1/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.1/mParticle_Apple_SDK.xcframework.zip",
- "8.42.2": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.2/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.2/mParticle_Apple_SDK.xcframework.zip"
+ "8.42.2": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.2/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.42.2/mParticle_Apple_SDK.xcframework.zip",
+ "8.43.1": "https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.43.1/mParticle_Apple_SDK.framework.zip?alt=https://github.com/mParticle/mparticle-apple-sdk/releases/download/v8.43.1/mParticle_Apple_SDK.xcframework.zip"
}