diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..516eb24a1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,90 @@ +# Contributing to mParticle Apple SDK + +Thanks for contributing! Please read this document to follow our conventions for contributing to the mParticle SDK. + +## Setting Up + +1. Fork the repository and then clone down your fork +2. Commit your code per the conventions below, and PR into the mParticle SDK main branch +3. Your PR title will be checked automatically against the below convention (view the commit history to see examples of a proper commit/PR title). If it fails, you must update your title +4. Our engineers will work with you to get your code change implemented once a PR is up + +## Development Process + +1. Create your branch from `main` +2. Make your changes +3. Add tests for any new functionality +4. Run the test suite to ensure tests (both new and old) all pass +5. Update the documentation +6. Create a Pull Request + +### Pull Requests + +* Fill in the required template +* Follow the [Swift API Design Guidelines](https://swift.org/documentation/api-design-guidelines/) +* Include screenshots and animated GIFs in your pull request whenever possible +* End all files with a newline + +### PR Title and Commit Convention + +PR titles should follow conventional commit standards. This helps automate the release process. + +The standard format for commit messages is as follows: + +``` +[optional scope]: + +[optional body] + +[optional footer] +``` + +The following lists the different types allowed in the commit message: + +- **feat**: A new feature (automatic minor release) +- **fix**: A bug fix (automatic patch release) +- **docs**: Documentation only changes +- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) +- **refactor**: A code change that neither fixes a bug nor adds a feature +- **perf**: A code change that improves performance +- **test**: Adding missing or correcting existing tests +- **chore**: Changes that don't modify src or test files, such as automatic documentation generation, or building latest assets +- **ci**: Changes to CI configuration files/scripts +- **revert**: Revert commit +- **build**: Changes that affect the build system or other dependencies + +### Testing + +We use XCTest framework for our testing. Please write tests for new code you create. Before submitting your PR, ensure all tests pass by running: + +#### Build and Test +```bash +xcodebuild -workspace mParticle-Apple-SDK.xcworkspace -scheme mParticle-Apple-SDK-iOS test +``` + +#### SwiftLint +```bash +swiftlint +``` + +Make sure all tests pass successfully before submitting your PR. If you encounter any test failures, investigate and fix the issues before proceeding. + +### Reporting Bugs + +This section guides you through submitting a bug report for the mParticle Apple SDK. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports. + +To notify our team about an issue, please submit a ticket through our [mParticles support page](https://support.mparticle.com/hc/en-us/requests/new). + +**When you are creating a ticket, please include as many details as possible:** + +* Use a clear and descriptive title +* Describe the exact steps which reproduce the problem +* Provide specific examples to demonstrate the steps +* Describe the behavior you observed after following the steps +* Explain which behavior you expected to see instead and why +* Include console output and stack traces if applicable +* Include your SDK version and iOS/macOS version + +## License + +By contributing to the mParticle Apple SDK, you agree that your contributions will be licensed under its [Apache License 2.0](LICENSE). diff --git a/README.md b/README.md index 11054847e..b93340640 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ # mParticle Apple SDK +A single SDK to collect analytics data and send it to 100+ marketing, analytics, and data platforms. Simplify your data integration with a single API. + This is the mParticle Apple SDK for iOS and tvOS. At mParticle our mission is straightforward: make it really easy for apps and app services to connect and allow you to take ownership of your 1st party data. @@ -232,6 +234,10 @@ Just by initializing the SDK you'll be set up to track user installs, engagement - [SDK Documentation](http://docs.mparticle.com/#mobile-sdk-guide) +## Contributing + +We welcome contributions! If you're interested in contributing to the mParticle Apple SDK, please read our [Contributing Guidelines](CONTRIBUTING.md). + ## Support Questions? Have an issue? Read the [docs](https://docs.mparticle.com/developers/sdk/ios/) or contact our **Customer Success** team at . diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000..61b1bf808 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,76 @@ +# Release Process + +This document outlines the process for creating a new release of the mParticle Apple SDK. + +## Automated Release Process + +We use GitHub Actions to automate our release process. Follow these steps to create a new release: + +### Pre-release Checklist +- Ensure all commits are in the public main branch +- Review `sdk-release.yml` in the repo for specific workflow details +- The release job deploys the most current snapshot of main branch release tag to main branch + +## Step 2: Release via GitHub Actions + +### What the GitHub Release Job Does + +1. **Initial Setup** + - Verifies job is running from public repo and on main branch + - Creates temporary `release/{run_number}` branch + +2. **Testing Phase** + - Runs unit tests for iOS and tvOS platforms + - Validates CocoaPods spec + - Validates Swift Package Manager build + - Updates kits and runs additional tests + +3. **Version Management** + - Runs semantic version action + - Automatically bumps version based on commit messages + - No version bump if no new commits (e.g., feat/fix) + - Generates release notes automatically + +4. **Artifact Publishing** + - Publishes to package managers: + - Pushes to CocoaPods trunk + - Updates Swift Package Manager + - Creates GitHub release with artifacts + + + +### How to Release + +1. Navigate to the Actions tab in GitHub +2. Select "iOS SDK Release" workflow +3. Run the workflow from main branch with "true" first to perform a dry run + > Important: Always start with a dry run to validate the release process. This will perform all steps up to semantic release without actually publishing, helping catch potential issues early. +4. If the dry run succeeds, run the workflow again with "false" option to perform the actual release + > Note: Only proceed with the actual release after confirming a successful dry run + +### Important Notes + +- **Release Duration**: Expect ~30 minutes due to comprehensive test suite across platforms +- **Platform Requirements**: + - Tests run on macOS runners + - Multiple Xcode versions may be tested + - Both iOS and tvOS platforms are validated +- **Code Reusability**: + - Reusable GitHub Actions are defined in the [mparticle-workflows repo](https://github.com/mParticle/mparticle-workflows) + - This enables other platforms to reuse similar jobs + +## Post-Release Verification + +After a successful build through GitHub Actions, verify: +1. Public repo has a new semantic release tag +2. New version is available on: + - [CocoaPods](https://cocoapods.org/pods/mParticle-Apple-SDK) + - Swift Package Manager + +## Troubleshooting + +If you encounter issues during testing, check: +- Xcode version compatibility +- Platform-specific test failures (iOS vs tvOS) +- GitHub Actions logs for specific error messages +- CocoaPods trunk status \ No newline at end of file diff --git a/UnitTests/MParticleTestsSwift.swift b/UnitTests/MParticleTestsSwift.swift index 7c3c13792..3c0831ccf 100644 --- a/UnitTests/MParticleTestsSwift.swift +++ b/UnitTests/MParticleTestsSwift.swift @@ -308,7 +308,7 @@ class MParticleTestsSwift: XCTestCase { } """) - } +\ } func testLogNetworkPerformanceCallbackSuccess() { mparticle.logNetworkPerformanceCallback(.success) @@ -814,11 +814,10 @@ class MParticleTestsSwift: XCTestCase { func testLogBaseEventWithFilterReturningEvent_forwardsTransformedEvent() { let event = MPBaseEvent(eventType: .other)! let transformedEvent = MPBaseEvent(eventType: .addToCart)! - let dataPlanFilter = MPDataPlanFilterMock() dataPlanFilter.transformEventForBaseEventReturnValue = transformedEvent mparticle.dataPlanFilter = dataPlanFilter - + mparticle.logEvent(event) // Verify listener was called @@ -831,15 +830,15 @@ class MParticleTestsSwift: XCTestCase { let completion = backendController.logBaseEventCompletionHandler! XCTAssertNotNil(completion) completion(event, .success) - + // Verify executor usage XCTAssertTrue(executor.executeOnMessageQueueAsync) XCTAssertTrue(executor.executeOnMainAsync) - + // Verify filter transformed event XCTAssertTrue(dataPlanFilter.transformEventForBaseEventCalled) XCTAssertTrue(dataPlanFilter.transformEventForBaseEventParam === event) - + // Verify kit container forwarded transformed event XCTAssertTrue(kitContainer.forwardSDKCallCalled) XCTAssertEqual(kitContainer.forwardSDKCallSelectorParam?.description, "logBaseEvent:") @@ -971,6 +970,172 @@ class MParticleTestsSwift: XCTestCase { XCTAssertEqual(receivedMessage, "mParticle -> Blocked commerce event from kits: \(commerceEvent)") } + + func testLogCommerceEventWithFilterReturningEvent_forwardsTransformedEvent() { + let commerceEvent = MPCommerceEvent(eventType: .other)! + let transformedCommerceEvent = MPCommerceEvent(eventType: .viewDetail)! + + let executor = ExecutorMock() + mparticle.setExecutor(executor) + + let backendController = MPBackendControllerMock() + mparticle.backendController = backendController + + let dataPlanFilter = MPDataPlanFilterMock() + dataPlanFilter.transformEventForCommerceEventReturnValue = transformedCommerceEvent + mparticle.dataPlanFilter = dataPlanFilter + + let kitContainer = MPKitContainerMock() + mparticle.setKitContainer(kitContainer) + + mparticle.logCommerceEvent(commerceEvent) + + // Verify event timestamp added + XCTAssertNotNil(commerceEvent.timestamp) + + // Verify listener was called + XCTAssertEqual(listenerController.onAPICalledApiName?.description, "logCommerceEvent:") + XCTAssertTrue(listenerController.onAPICalledParameter1 === commerceEvent) + + // Verify backend was called + XCTAssertTrue(backendController.logCommerceEventCalled) + XCTAssertTrue(backendController.logCommerceEventParam === commerceEvent) + let completion = backendController.logCommerceEventCompletionHandler! + XCTAssertNotNil(completion) + completion(commerceEvent, .success) + + // Verify executor usage + XCTAssertTrue(executor.executeOnMessageQueueAsync) + + // Verify filter transformed event + XCTAssertTrue(dataPlanFilter.transformEventForCommerceEventCalled) + XCTAssertTrue(dataPlanFilter.transformEventForCommerceEventParam === commerceEvent) + + // Verify kit container forwarded transformed event + XCTAssertTrue(kitContainer.forwardCommerceEventCallCalled) + XCTAssertTrue(kitContainer.forwardCommerceEventCallCommerceEventParam === transformedCommerceEvent) + } + + // MARK: - logLTVIncrease + + func testLogLTVIncrease_withNameAndInfo_createsEventAndCallsBackend() { + let amount = 42.0 + let name = "name" + let info: [String: Any] = ["source": "in_app", "currency": "USD"] + + let backendController = MPBackendControllerMock() + mparticle.backendController = backendController + + mparticle.logLTVIncrease(amount, eventName: name, eventInfo: info) + + // Assert event was passed through + let loggedEvent = backendController.logEventEventParam! + XCTAssertNotNil(loggedEvent) + XCTAssertEqual(loggedEvent.name, name) + XCTAssertEqual(loggedEvent.type, .transaction) + + // Custom attributes should include amount and method name + let attrs = loggedEvent.customAttributes! + XCTAssertEqual(attrs["$Amount"] as? Double, amount) + XCTAssertEqual(attrs["$MethodName"] as? String, "LogLTVIncrease") + + // Check that the eventInfo entries were added + XCTAssertEqual(attrs["source"] as? String, "in_app") + XCTAssertEqual(attrs["currency"] as? String, "USD") + XCTAssertEqual(attrs.count, 4) + + // Listener controller should be notified + XCTAssertEqual(listenerController.onAPICalledApiName?.description, "logLTVIncrease:eventName:eventInfo:") + + // Backend completion handler should be stored + XCTAssertTrue(backendController.logEventCalled) + let completion = backendController.logEventCompletionHandler! + XCTAssertNotNil(completion) + completion(loggedEvent, .success) + } + + func testLogLTVIncrease_withoutEventInfo_defaultsToNilInfo() { + let amount = 12.5 + let name = "name" + + let backendController = MPBackendControllerMock() + mparticle.backendController = backendController + + mparticle.logLTVIncrease(amount, eventName: name) + + // Assert event was passed through + let loggedEvent = backendController.logEventEventParam! + XCTAssertNotNil(loggedEvent) + XCTAssertEqual(loggedEvent.name, name) + XCTAssertEqual(loggedEvent.type, .transaction) + + // Custom attributes should only be amount and method name + let attrs = loggedEvent.customAttributes! + XCTAssertEqual(attrs["$Amount"] as? Double, amount) + XCTAssertEqual(attrs["$MethodName"] as? String, "LogLTVIncrease") + XCTAssertEqual(attrs.count, 2) + + // Listener controller should be notified + XCTAssertEqual(listenerController.onAPICalledApiName?.description, "logLTVIncrease:eventName:eventInfo:") + XCTAssertEqual(listenerController.onAPICalledParameter1 as? Double, amount) + XCTAssertEqual(listenerController.onAPICalledParameter2 as? String, name) + XCTAssertNil(listenerController.onAPICalledParameter3) + + // Backend completion handler should be stored + XCTAssertTrue(backendController.logEventCalled) + let completion = backendController.logEventCompletionHandler! + XCTAssertNotNil(completion) + completion(loggedEvent, .success) + } + + func testLogLTVIncreaseCallback_withSuccessExecStatus_noDataPlanFilter_forwardsEvent() { + let event = MPEvent(name: "ltv", type: .transaction)! + + let dataPlanFilter = MPDataPlanFilterMock() + dataPlanFilter.transformEventReturnValue = nil + mparticle.dataPlanFilter = dataPlanFilter + + let kitContainer = MPKitContainerMock() + mparticle.setKitContainer(kitContainer) + + mparticle.logLTVIncreaseCallback(event, execStatus: .success) + + XCTAssertTrue(dataPlanFilter.transformEventCalled) + XCTAssertTrue(dataPlanFilter.transformEventEventParam === event) + + XCTAssertEqual(receivedMessage, "mParticle -> Blocked LTV increase event from kits: \(event)") + } + + func testLogLTVIncreaseCallback_withSuccessExecStatus_filterReturnsTransformedEvent_forwardsTransformedEvent() { + let event = MPEvent(name: "ltv", type: .transaction)! + let transformedEvent = MPEvent(name: "transformed-ltv", type: .other)! + + let dataPlanFilter = MPDataPlanFilterMock() + dataPlanFilter.transformEventReturnValue = transformedEvent + mparticle.dataPlanFilter = dataPlanFilter + + let executor = ExecutorMock() + mparticle.setExecutor(executor) + + let kitContainer = MPKitContainerMock() + mparticle.setKitContainer(kitContainer) + + mparticle.logLTVIncreaseCallback(event, execStatus: .success) + + // Verify filter transformed event + XCTAssertTrue(dataPlanFilter.transformEventCalled) + XCTAssertTrue(dataPlanFilter.transformEventEventParam === event) + + // Verify executor usage + XCTAssertTrue(executor.executeOnMainAsync) + + // Verify kit container forwarded transformed event + XCTAssertTrue(kitContainer.forwardSDKCallCalled) + XCTAssertEqual(kitContainer.forwardSDKCallSelectorParam?.description, "logLTVIncrease:event:") + XCTAssertEqual(kitContainer.forwardSDKCallMessageTypeParam, .unknown) + XCTAssertNil(kitContainer.forwardSDKCallEventParam) + } + func testLogCommerceEventWithFilterReturningEvent_forwardsTransformedEvent() { let commerceEvent = MPCommerceEvent(eventType: .other)! let transformedCommerceEvent = MPCommerceEvent(eventType: .viewDetail)! diff --git a/UnitTests/Mocks/MPDataPlanFilterMock.swift b/UnitTests/Mocks/MPDataPlanFilterMock.swift index 591fc2533..125130be0 100644 --- a/UnitTests/Mocks/MPDataPlanFilterMock.swift +++ b/UnitTests/Mocks/MPDataPlanFilterMock.swift @@ -57,6 +57,7 @@ class MPDataPlanFilterMock: NSObject, MPDataPlanFilterProtocol { return transformEventForCommerceEventReturnValue } + var transformEventForBaseEventCalled = false var transformEventForBaseEventParam: MPBaseEvent? var transformEventForBaseEventReturnValue: MPBaseEvent?