├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── pull_request_template.md └── workflows │ ├── build.yml │ ├── create_jira.yml │ ├── release.yml │ └── snapshot.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── JAVA_COMPAT.md ├── LICENSE ├── README.md ├── RELEASING.md ├── android ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── gradle.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── segment │ │ └── analytics │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── segment │ │ └── analytics │ │ └── kotlin │ │ └── android │ │ ├── AndroidAnalytics.kt │ │ ├── Storage.kt │ │ ├── plugins │ │ ├── AndroidContextPlugin.kt │ │ └── AndroidLifecyclePlugin.kt │ │ └── utilities │ │ ├── AndroidKVS.kt │ │ ├── DeepLinkUtils.kt │ │ └── JSON.kt │ └── test │ ├── java │ └── com │ │ └── segment │ │ └── analytics │ │ └── kotlin │ │ └── android │ │ ├── AndroidAnalyticsKtTest.kt │ │ ├── AndroidContextCollectorTests.kt │ │ ├── AndroidLifecyclePluginTests.kt │ │ ├── EventsFileTests.kt │ │ ├── StorageTests.kt │ │ ├── utilities │ │ ├── AndroidKVSTest.kt │ │ └── DeepLinkUtilsTests.kt │ │ └── utils │ │ ├── MockPreferences.kt │ │ ├── Mocks.kt │ │ └── Plugins.kt │ └── resources │ └── robolectric.properties ├── build.gradle ├── core ├── .gitignore ├── build.gradle ├── gradle.properties └── src │ ├── main │ └── java │ │ └── com │ │ └── segment │ │ └── analytics │ │ └── kotlin │ │ └── core │ │ ├── Analytics.kt │ │ ├── Annotations.kt │ │ ├── Configuration.kt │ │ ├── Constants.kt │ │ ├── Errors.kt │ │ ├── Events.kt │ │ ├── HTTPClient.kt │ │ ├── Settings.kt │ │ ├── State.kt │ │ ├── Storage.kt │ │ ├── Telemetry.kt │ │ ├── compat │ │ ├── Builders.kt │ │ ├── ConfigurationBuilder.kt │ │ ├── JavaAnalytics.kt │ │ └── JsonSerializable.kt │ │ ├── platform │ │ ├── EventPipeline.kt │ │ ├── Mediator.kt │ │ ├── Plugin.kt │ │ ├── Timeline.kt │ │ ├── plugins │ │ │ ├── ContextPlugin.kt │ │ │ ├── DestinationMetadataPlugin.kt │ │ │ ├── DeviceToken.kt │ │ │ ├── SegmentDestination.kt │ │ │ ├── StartupQueue.kt │ │ │ ├── UserInfoPlugin.kt │ │ │ └── logger │ │ │ │ ├── ConsoleLogger.kt │ │ │ │ ├── Logger.kt │ │ │ │ └── SegmentLog.kt │ │ └── policies │ │ │ ├── CountBasedFlushPolicy.kt │ │ │ ├── FlushPolicy.kt │ │ │ ├── FrequencyFlushPolicy.kt │ │ │ └── StartupFlushPolicy.kt │ │ └── utilities │ │ ├── AnySerializer.kt │ │ ├── Base64Utils.kt │ │ ├── DateTimeUtils.kt │ │ ├── EventManipulationFunctions.kt │ │ ├── EventStream.kt │ │ ├── EventsFileManager.kt │ │ ├── FileUtils.kt │ │ ├── JSON.kt │ │ ├── KVS.kt │ │ ├── PropertiesFile.kt │ │ └── StorageImpl.kt │ └── test │ └── kotlin │ └── com │ └── segment │ └── analytics │ └── kotlin │ └── core │ ├── AnalyticsTests.kt │ ├── ErrorsTest.kt │ ├── EventTests.kt │ ├── HTTPClientTests.kt │ ├── JSONTests.kt │ ├── PluginTests.kt │ ├── SerializingTests.kt │ ├── SettingsTests.kt │ ├── StateTest.kt │ ├── StorageTest.kt │ ├── TelemetryTest.kt │ ├── compat │ ├── BuildersTest.kt │ ├── ConfigurationBuilderTest.kt │ └── JavaAnalyticsTest.kt │ ├── platform │ ├── EventPipelineTest.kt │ ├── MediatorTest.kt │ ├── plugins │ │ ├── DestinationMetadataPluginTests.kt │ │ ├── DestinationPluginTests.kt │ │ ├── DeviceTokenPluginTests.kt │ │ ├── LoggerTest.kt │ │ ├── SegmentDestinationTests.kt │ │ ├── SegmentLogTest.kt │ │ ├── StartupQueueTest.kt │ │ └── UserInfoPluginTests.kt │ └── policies │ │ ├── CountBasedFlushPolicyTests.kt │ │ ├── FrequencyFlushPolicyTests.kt │ │ └── StartupFlushPolicyTests.kt │ ├── utilities │ ├── Base64UtilsTest.kt │ ├── DateTimeUtilsTest.kt │ ├── EventManipulationFunctionsTest.kt │ ├── EventStreamTest.kt │ ├── InMemoryStorageTest.kt │ ├── KVSTest.kt │ ├── PropertiesFileTest.kt │ └── StorageImplTest.kt │ └── utils │ ├── Mocks.kt │ └── Plugins.kt ├── gradle.properties ├── gradle ├── artifacts-android.gradle ├── artifacts-core.gradle ├── codecov.gradle ├── mvn-publish.gradle ├── promote.gradle ├── versioning.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── samples ├── README.md ├── java-android-app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── segment │ │ │ └── analytics │ │ │ └── javaandroidapp │ │ │ └── ExampleInstrumentedTest.java │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── segment │ │ │ │ └── analytics │ │ │ │ └── javaandroidapp │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ └── activity_main.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-night │ │ │ └── themes.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── segment │ │ └── analytics │ │ └── javaandroidapp │ │ └── ExampleUnitTest.java ├── kotlin-android-app-destinations │ ├── .gitignore │ ├── README.md │ ├── build.gradle │ ├── google-services.json │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── segment │ │ │ └── analytics │ │ │ └── next │ │ │ └── ExampleInstrumentedTest.kt │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── segment │ │ │ └── analytics │ │ │ └── kotlin │ │ │ └── destinations │ │ │ ├── EventFragment.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainApplication.kt │ │ │ └── plugins │ │ │ └── WebhookPlugin.kt │ │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── border.xml │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_sample.xml │ │ ├── fragment_group.xml │ │ ├── fragment_identify.xml │ │ ├── fragment_screen.xml │ │ ├── fragment_track.xml │ │ ├── property.xml │ │ └── trait.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── themes.xml ├── kotlin-android-app │ ├── .gitignore │ ├── README.md │ ├── build.gradle │ ├── google-services.json │ ├── proguard-rules.pro │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── segment │ │ │ └── analytics │ │ │ └── next │ │ │ └── ExampleInstrumentedTest.kt │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── segment │ │ │ │ └── analytics │ │ │ │ └── next │ │ │ │ ├── ConsentActivity.kt │ │ │ │ ├── EncryptedEventStream.kt │ │ │ │ ├── EventFragment.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MainApplication.kt │ │ │ │ ├── MyFirebaseService.kt │ │ │ │ ├── UnmeteredFlushPolicy.kt │ │ │ │ └── plugins │ │ │ │ ├── AndroidRecordScreenPlugin.kt │ │ │ │ ├── ConsentTracking.kt │ │ │ │ ├── InjectTraitsPlugin.kt │ │ │ │ ├── PassiveLocationPlugin.kt │ │ │ │ └── PushNotificationTracking.kt │ │ └── res │ │ │ ├── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── drawable │ │ │ ├── border.xml │ │ │ └── ic_launcher_background.xml │ │ │ ├── layout │ │ │ ├── activity_sample.xml │ │ │ ├── fragment_group.xml │ │ │ ├── fragment_identify.xml │ │ │ ├── fragment_screen.xml │ │ │ ├── fragment_track.xml │ │ │ ├── property.xml │ │ │ └── trait.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── values-night │ │ │ └── themes.xml │ │ │ └── values │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ ├── styles.xml │ │ │ └── themes.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── segment │ │ └── analytics │ │ └── next │ │ └── ExampleUnitTest.kt └── kotlin-jvm-app │ ├── .gitignore │ ├── build.gradle │ └── src │ └── main │ └── kotlin │ └── main.kt └── settings.gradle /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: triage 6 | assignees: wenxi-zeng 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Do '...' 16 | 2. '....' 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Platform (please complete the following information):** 26 | - Library Version in use: [e.g. 0.0.5] 27 | - Platform being tested: [e.g. Android] 28 | - Integrations in use: [e.g. Firebase, Amplitude] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: triage 6 | assignees: prayansh 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | _Describe the problem or feature in addition to a link to the issues._ 3 | 4 | ## Approach 5 | _How does this change address the problem?_ 6 | 7 | #### Open Questions and Pre-Merge TODOs 8 | - [ ] Use github checklists. When solved, check the box and explain the answer. 9 | 10 | ## Learning 11 | _Describe the research stage_ 12 | 13 | _Links to blog posts, patterns, libraries or addons used to solve this problem_ -------------------------------------------------------------------------------- /.github/workflows/create_jira.yml: -------------------------------------------------------------------------------- 1 | name: Create Jira Ticket 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | 8 | jobs: 9 | create_jira: 10 | name: Create Jira Ticket 11 | runs-on: ubuntu-22.04 12 | environment: IssueTracker 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@master 16 | - name: Login 17 | uses: atlassian/gajira-login@master 18 | env: 19 | JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 20 | JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 21 | JIRA_API_TOKEN: ${{ secrets.JIRA_TOKEN }} 22 | JIRA_EPIC_KEY: ${{ secrets.JIRA_EPIC_KEY }} 23 | JIRA_PROJECT: ${{ secrets.JIRA_PROJECT }} 24 | 25 | - name: Create 26 | id: create 27 | uses: atlassian/gajira-create@master 28 | with: 29 | project: ${{ secrets.JIRA_PROJECT }} 30 | issuetype: Bug 31 | summary: | 32 | [${{ github.event.repository.name }}] (${{ github.event.issue.number }}): ${{ github.event.issue.title }} 33 | description: | 34 | Github Link: ${{ github.event.issue.html_url }} 35 | ${{ github.event.issue.body }} 36 | fields: '{"parent": {"key": "${{ secrets.JIRA_EPIC_KEY }}"}}' 37 | 38 | - name: Log created issue 39 | run: echo "Issue ${{ steps.create.outputs.issue }} was created" -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-22.04 11 | environment: deployment 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Get tag 15 | id: vars 16 | run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} 17 | - name: Verify tag 18 | run: | 19 | VERSION=$(grep VERSION_NAME gradle.properties | awk -F= '{ print $2 }' | sed "s/-SNAPSHOT//") 20 | if [ "${{ steps.vars.outputs.tag }}" != "$VERSION" ]; then { 21 | echo "Tag ${{ steps.vars.outputs.tag }} does not match the package version ($VERSION)" 22 | exit 1 23 | } fi 24 | 25 | - name: Grant execute permission for gradlew 26 | run: chmod +x gradlew 27 | - name: cache gradle dependencies 28 | uses: actions/cache@v3 29 | with: 30 | path: | 31 | ~/.gradle/caches 32 | ~/.gradle/wrapper 33 | key: ${{ runner.os }}-gradle-core-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 34 | restore-keys: | 35 | ${{ runner.os }}-gradle-core- 36 | - name: Publush release to sonatype 37 | run: ./gradlew clean build publish publishToSonatype -Prelease closeAndReleaseSonatypeStagingRepository 38 | env: 39 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 40 | SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} 41 | ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.NEXUS_USERNAME }} 42 | ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.NEXUS_PASSWORD }} 43 | SIGNING_PRIVATE_KEY_BASE64: ${{ secrets.SIGNING_PRIVATE_KEY_BASE64 }} 44 | 45 | - name: create release 46 | run: | 47 | curl \ 48 | -X POST \ 49 | -H "Authorization: token $RELEASE_TOKEN" \ 50 | https://api.github.com/repos/${{github.repository}}/releases \ 51 | -d '{"tag_name": "${{ env.RELEASE_VERSION }}", "name": "${{ env.RELEASE_VERSION }}", "body": "Release of version ${{ env.RELEASE_VERSION }}", "draft": false, "prerelease": false, "generate_release_notes": true}' 52 | env: 53 | RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} 54 | RELEASE_VERSION: ${{ steps.vars.outputs.tag }} -------------------------------------------------------------------------------- /.github/workflows/snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Snapshot 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | snapshot: 9 | runs-on: ubuntu-22.04 10 | environment: deployment 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Grant execute permission for gradlew 14 | run: chmod +x gradlew 15 | - name: cache gradle dependencies 16 | uses: actions/cache@v3 17 | with: 18 | path: | 19 | ~/.gradle/caches 20 | ~/.gradle/wrapper 21 | key: ${{ runner.os }}-gradle-core-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 22 | restore-keys: | 23 | ${{ runner.os }}-gradle-core- 24 | - name: Publush snapshot to sonatype 25 | run: ./gradlew clean build publish publishToSonatype 26 | env: 27 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 28 | SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} 29 | ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.NEXUS_USERNAME }} 30 | ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.NEXUS_PASSWORD }} 31 | SIGNING_PRIVATE_KEY_BASE64: ${{ secrets.SIGNING_PRIVATE_KEY_BASE64 }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | .idea 17 | /core/bin 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. 4 | 5 | ## Development workflow 6 | 7 | We recommend using the Android Studio IDE to jumpstart your development workflow 8 | 9 | To get started with the project 10 | - clone this repository 11 | - import the project into Android Studio 12 | 13 | While developing, you can run the [example app](/example/) to test your changes. 14 | - You can do this via the IDE itself using the `Run app` button in the toolbar 15 | - You can install the debug version of the app using the gradle command: 16 | ``` 17 | ./gradlew installDebug 18 | ``` 19 | 20 | Remember to add tests for your change if possible. Run the unit tests: 21 | - Running the tests via the IDE 22 | - via the command line 23 | ``` 24 | ./gradlew test 25 | ``` 26 | 27 | ### Commit message convention 28 | 29 | We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: 30 | 31 | - `fix`: bug fixes, e.g. fix crash due to deprecated method. 32 | - `feat`: new features, e.g. add new method to the module. 33 | - `refactor`: code refactor, e.g. migrate from class components to hooks. 34 | - `docs`: changes into documentation, e.g. add usage example for the module.. 35 | - `test`: adding or updating tests, eg add integration tests using detox. 36 | - `chore`: tooling changes, e.g. change CI config. 37 | 38 | ### Linting and tests 39 | 40 | ./gradlew test 41 | 42 | Our pre-commit hooks verify that the linter and tests pass when committing. 43 | 44 | ### Scripts 45 | 46 | ### Sending a pull request 47 | 48 | > **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). 49 | 50 | When you're sending a pull request: 51 | 52 | - Prefer small pull requests focused on one change. 53 | - Verify that linters and tests are passing. 54 | - Review the documentation to make sure it looks good. 55 | - Follow the pull request template when opening a pull request. 56 | - For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Segment 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Update Version 2 | ========= 3 | 1. Update VERSION_CODE in `gradle.properties` 4 | 2. Update VERSION_NAME in `gradle.properties` 5 | 3. Update LIBRARY_VERSION in `core/Constants.kt` 6 | 7 | Releasing 8 | ========= 9 | 10 | 1. Create a new branch called `release/X.Y.Z` 11 | 2. `git checkout -b release/X.Y.Z` 12 | 3. Change the version in `gradle.properties` to your desired release version (see `Update Version`) 13 | 4. `git commit -am "Create release X.Y.Z."` (where X.Y.Z is the new version) 14 | 5. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version) 15 | 6. Upgrade to next version by changing version in `gradle.properties` 16 | 7. `git commit -am "Prepare snapshot X.Y.Z-SNAPSHOT"` 17 | 8. `git push && git push --tags` 18 | 9. Create a PR to merge the new branch into `master` 19 | 10. The CI pipeline will recognize the tag and upload, close and promote the artifacts automatically, and generate changelog automatically 20 | 21 | Example (stable release) 22 | ======== 23 | 1. Current VERSION_NAME in `gradle.properties` = 1.3.0 24 | 2. `git checkout -b release/1.3.1` 25 | 3. Change VERSION_NAME = 1.3.1 (next higher version) 26 | 4. Update CHANGELOG.md 27 | 5. `git commit -am "Create release 1.3.1` 28 | 6. `git tag -a 1.3.1 -m "Version 1.3.1"` 29 | 6. `git push && git push --tags` 30 | 7. Change VERSION_NAME = 1.3.2 (next higher version) 31 | 8. `git commit -am "Prepare snapshot 1.3.2-SNAPSHOT"` 32 | 9. `git push && git push --tags` 33 | 10. Merging PR master will create a snapshot release 1.3.2-SNAPSHOT and tag push will create stable release 1.3.1 -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | id 'org.jetbrains.kotlin.plugin.serialization' 5 | id 'de.mannodermaus.android-junit5' 6 | } 7 | 8 | android { 9 | compileSdkVersion 33 10 | 11 | defaultConfig { 12 | // Required when setting minSdkVersion to 20 or lower 13 | multiDexEnabled true 14 | 15 | minSdkVersion 16 16 | targetSdkVersion 33 17 | versionCode VERSION_CODE.toInteger() 18 | versionName VERSION_NAME 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | consumerProguardFiles "consumer-rules.pro" 22 | 23 | buildConfigField("int", "SEGMENT_VERSION_CODE", "${versionCode}") 24 | buildConfigField("String", "SEGMENT_VERSION_NAME", "\"${versionName}\"") 25 | } 26 | 27 | buildTypes { 28 | release { 29 | minifyEnabled false 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | compileOptions { 34 | sourceCompatibility JavaVersion.VERSION_1_8 35 | targetCompatibility JavaVersion.VERSION_1_8 36 | } 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | testOptions { 41 | unitTests { 42 | includeAndroidResources = true 43 | } 44 | } 45 | 46 | } 47 | 48 | dependencies { 49 | // MAIN DEPS 50 | api project(':core') 51 | api 'com.segment:sovran-kotlin:1.2.2' 52 | api "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1" 53 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' 54 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1' 55 | implementation 'androidx.lifecycle:lifecycle-process:2.6.1' 56 | implementation 'androidx.lifecycle:lifecycle-common-java8:2.6.1' 57 | 58 | 59 | // TESTING 60 | testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2' 61 | testImplementation 'io.mockk:mockk:1.12.2' 62 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1' 63 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 64 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 65 | 66 | // Add JUnit5 dependencies. 67 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' 68 | testImplementation 'org.junit.jupiter:junit-jupiter-params:5.8.2' 69 | 70 | // Add JUnit4 legacy dependencies. 71 | testImplementation 'junit:junit:4.13.2' 72 | testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.8.2' 73 | 74 | // Add Roboelectric dependencies. 75 | testImplementation 'org.robolectric:robolectric:4.7.3' 76 | testImplementation 'androidx.test:core:1.5.0' 77 | } 78 | 79 | apply from: rootProject.file('gradle/artifacts-android.gradle') 80 | apply from: rootProject.file('gradle/mvn-publish.gradle') 81 | apply from: rootProject.file('gradle/codecov.gradle') -------------------------------------------------------------------------------- /android/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | # kotlinx-serialization-json specific. Add this if you have java.lang.NoClassDefFoundError kotlinx.serialization.json.JsonObjectSerializer 2 | -keepclassmembers class kotlinx.serialization.json.** { 3 | *** Companion; 4 | } 5 | -keepclasseswithmembers class kotlinx.serialization.json.** { 6 | kotlinx.serialization.KSerializer serializer(...); 7 | } 8 | 9 | # These rules will ensure that our generated serializers dont get obfuscated 10 | -keep,includedescriptorclasses class com.segment.analytics.kotlin.**$$serializer { *; } 11 | -keepclassmembers class com.segment.analytics.kotlin.** { 12 | *** Companion; 13 | } 14 | -keepclasseswithmembers class com.segment.analytics.kotlin.** { 15 | kotlinx.serialization.KSerializer serializer(...); 16 | } -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Segment Kotlin Analytics 2 | POM_ARTIFACT_ID=android 3 | POM_PACKAGING=aar 4 | -------------------------------------------------------------------------------- /android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /android/src/androidTest/java/com/segment/analytics/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import com.segment.analytics.kotlin.core.Telemetry 6 | 7 | import org.junit.Test 8 | import org.junit.runner.RunWith 9 | 10 | import org.junit.Assert.* 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * See [testing documentation](http://d.android.com/tools/testing). 16 | */ 17 | @RunWith(AndroidJUnit4::class) 18 | class ExampleInstrumentedTest { 19 | init { 20 | Telemetry.enable = false 21 | } 22 | 23 | @Test 24 | fun useAppContext() { 25 | // Context of the app under test. 26 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 27 | assertEquals("com.segment.analytics.test", appContext.packageName) 28 | } 29 | } -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/java/com/segment/analytics/kotlin/android/utilities/AndroidKVS.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.android.utilities 2 | 3 | import android.content.SharedPreferences 4 | import com.segment.analytics.kotlin.core.utilities.KVS 5 | 6 | /** 7 | * A key-value store wrapper for sharedPreferences on Android 8 | */ 9 | class AndroidKVS(val sharedPreferences: SharedPreferences): KVS { 10 | 11 | 12 | override fun get(key: String, defaultVal: Int) = 13 | sharedPreferences.getInt(key, defaultVal) 14 | 15 | override fun get(key: String, defaultVal: String?) = 16 | sharedPreferences.getString(key, defaultVal) ?: defaultVal 17 | 18 | override fun put(key: String, value: Int) = 19 | sharedPreferences.edit().putInt(key, value).commit() 20 | 21 | override fun put(key: String, value: String) = 22 | sharedPreferences.edit().putString(key, value).commit() 23 | 24 | override fun remove(key: String): Boolean = 25 | sharedPreferences.edit().remove(key).commit() 26 | 27 | override fun contains(key: String) = sharedPreferences.contains(key) 28 | } -------------------------------------------------------------------------------- /android/src/main/java/com/segment/analytics/kotlin/android/utilities/DeepLinkUtils.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.android.utilities 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import com.segment.analytics.kotlin.core.Analytics 6 | import kotlinx.serialization.json.JsonObject 7 | import kotlinx.serialization.json.buildJsonObject 8 | import kotlinx.serialization.json.put 9 | 10 | class DeepLinkUtils(val analytics: Analytics) { 11 | 12 | fun trackDeepLinkFrom(referrer: String?, intent: Intent?) { 13 | 14 | if (intent == null || intent.data == null) { 15 | return 16 | } 17 | 18 | val properties = extractLinkProperties(referrer, intent.data) 19 | analytics.track("Deep Link Opened", properties) 20 | } 21 | 22 | /** 23 | * Builds a JsonObject with the parameters of a given Uri. 24 | * 25 | * Note: The Uri must be hierarchical (myUri.isHierarchical == true) for parameters to be 26 | * extracted. 27 | * 28 | * Example hierarchical Uri: http://example.com/ 29 | * Example non-hierarchical Uri: mailto:me@email.com 30 | * 31 | * Note: we return the given Uri as a property named: "url" since this is what is expected 32 | * upstream. 33 | */ 34 | fun extractLinkProperties( 35 | referrer: String?, 36 | uri: Uri? 37 | ): JsonObject { 38 | val properties = buildJsonObject { 39 | referrer?.let { 40 | put("referrer", it) 41 | } 42 | 43 | uri?.let { 44 | if (uri.isHierarchical) { 45 | for (parameter in uri.queryParameterNames) { 46 | val value = uri.getQueryParameter(parameter) 47 | if (value != null && value.trim().isNotEmpty()) { 48 | put(parameter, value) 49 | } 50 | } 51 | } 52 | put("url", uri.toString()) 53 | } 54 | } 55 | 56 | return properties 57 | } 58 | } -------------------------------------------------------------------------------- /android/src/main/java/com/segment/analytics/kotlin/android/utilities/JSON.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.android.utilities 2 | 3 | import com.segment.analytics.kotlin.core.utilities.toContent 4 | import kotlinx.serialization.json.JsonArray 5 | import kotlinx.serialization.json.JsonElement 6 | import kotlinx.serialization.json.JsonObject 7 | import kotlinx.serialization.json.JsonPrimitive 8 | import org.json.JSONArray 9 | import org.json.JSONObject 10 | 11 | // Transform kotlinx.JsonArray to org.json.JSONArray 12 | fun List.toJSONArray(): JSONArray { 13 | val constructed = JSONArray() 14 | for (item in this) { 15 | when (item) { 16 | is JsonPrimitive -> { 17 | constructed.put(item.toContent()) 18 | } 19 | is JsonObject -> { 20 | constructed.put(item.toJSONObject()) 21 | } 22 | is JsonArray -> { 23 | constructed.put(item.toJSONArray()) 24 | } 25 | } 26 | } 27 | return constructed 28 | } 29 | 30 | // Transform kotlinx.JsonArray to org.json.JSONArray 31 | fun Map.toJSONObject(): JSONObject { 32 | val constructed = JSONObject() 33 | for ((k, v) in entries) { 34 | when (v) { 35 | is JsonPrimitive -> { 36 | constructed.put(k, v.toContent()) 37 | } 38 | is JsonObject -> { 39 | constructed.put(k, v.toJSONObject()) 40 | } 41 | is JsonArray -> { 42 | constructed.put(k, v.toJSONArray()) 43 | } 44 | } 45 | } 46 | return constructed 47 | } -------------------------------------------------------------------------------- /android/src/test/java/com/segment/analytics/kotlin/android/AndroidAnalyticsKtTest.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.android 2 | 3 | import com.segment.analytics.kotlin.core.Telemetry 4 | import org.junit.jupiter.api.Assertions.* 5 | 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.assertThrows 8 | 9 | internal class AndroidAnalyticsKtTest { 10 | init { 11 | Telemetry.enable = false 12 | } 13 | 14 | @Test 15 | fun `jvm initializer in android platform should failed`() { 16 | val exception = assertThrows { 17 | com.segment.analytics.kotlin.core.Analytics("123") { 18 | application = "Test" 19 | } 20 | } 21 | 22 | assertEquals(exception.message?.contains("Android"), true) 23 | } 24 | } -------------------------------------------------------------------------------- /android/src/test/java/com/segment/analytics/kotlin/android/utilities/AndroidKVSTest.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.android.utilities 2 | 3 | import com.segment.analytics.kotlin.android.utils.MemorySharedPreferences 4 | import com.segment.analytics.kotlin.core.utilities.KVS 5 | import org.junit.jupiter.api.Assertions 6 | import org.junit.jupiter.api.BeforeEach 7 | import org.junit.jupiter.api.Test 8 | 9 | class AndroidKVSTest { 10 | 11 | private lateinit var prefs: KVS 12 | 13 | @BeforeEach 14 | fun setup(){ 15 | val sharedPreferences = MemorySharedPreferences() 16 | prefs = AndroidKVS(sharedPreferences) 17 | prefs.put("int", 1) 18 | prefs.put("string", "string") 19 | } 20 | 21 | @Test 22 | fun getTest() { 23 | Assertions.assertEquals(1, prefs.get("int", 0)) 24 | Assertions.assertEquals("string", prefs.get("string", null)) 25 | Assertions.assertEquals(0, prefs.get("keyNotExists", 0)) 26 | Assertions.assertEquals(null, prefs.get("keyNotExists", null)) 27 | } 28 | 29 | @Test 30 | fun putTest() { 31 | prefs.put("int", 2) 32 | prefs.put("string", "stringstring") 33 | 34 | Assertions.assertEquals(2, prefs.get("int", 0)) 35 | Assertions.assertEquals("stringstring", prefs.get("string", null)) 36 | } 37 | 38 | @Test 39 | fun containsAndRemoveTest() { 40 | Assertions.assertTrue(prefs.contains("int")) 41 | prefs.remove("int") 42 | Assertions.assertFalse(prefs.contains("int")) 43 | } 44 | } -------------------------------------------------------------------------------- /android/src/test/java/com/segment/analytics/kotlin/android/utils/Plugins.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.android.utils 2 | 3 | import com.segment.analytics.kotlin.core.* 4 | import com.segment.analytics.kotlin.core.platform.* 5 | 6 | class TestRunPlugin(var closure: (BaseEvent?) -> Unit): EventPlugin { 7 | override val type: Plugin.Type = Plugin.Type.Before 8 | override lateinit var analytics: Analytics 9 | var ran = false 10 | 11 | override fun reset() { 12 | ran = false 13 | } 14 | 15 | fun updateState(ran: Boolean) { 16 | this.ran = ran 17 | } 18 | 19 | override fun execute(event: BaseEvent): BaseEvent { 20 | super.execute(event) 21 | updateState(true) 22 | return event 23 | } 24 | 25 | override fun track(payload: TrackEvent): BaseEvent { 26 | closure(payload) 27 | updateState(true) 28 | return payload 29 | } 30 | 31 | override fun identify(payload: IdentifyEvent): BaseEvent { 32 | closure(payload) 33 | updateState(true) 34 | return payload 35 | } 36 | 37 | override fun screen(payload: ScreenEvent): BaseEvent { 38 | closure(payload) 39 | updateState(true) 40 | return payload 41 | } 42 | 43 | override fun group(payload: GroupEvent): BaseEvent { 44 | closure(payload) 45 | updateState(true) 46 | return payload 47 | } 48 | 49 | override fun alias(payload: AliasEvent): BaseEvent { 50 | closure(payload) 51 | updateState(true) 52 | return payload 53 | } 54 | } 55 | 56 | class StubPlugin : EventPlugin { 57 | override val type: Plugin.Type = Plugin.Type.Before 58 | override lateinit var analytics: Analytics 59 | } -------------------------------------------------------------------------------- /android/src/test/resources/robolectric.properties: -------------------------------------------------------------------------------- 1 | sdk=31 2 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.8.0" 4 | ext.writeKey = "hAh3QxoRCsQsnyabTLMpWbpxoIN4O2mU" 5 | repositories { 6 | google() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.4.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" 12 | 13 | // needed for android unit tests 14 | classpath("de.mannodermaus.gradle.plugins:android-junit5:1.9.3.0") 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | classpath 'com.google.gms:google-services:4.3.15' 18 | } 19 | } 20 | 21 | plugins { 22 | id "io.snyk.gradle.plugin.snykplugin" version "0.4" 23 | id "io.github.gradle-nexus.publish-plugin" version "1.1.0" 24 | } 25 | 26 | allprojects { 27 | repositories { 28 | google() 29 | mavenCentral() 30 | maven { url "https://kotlin.bintray.com/kotlinx" } 31 | maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } 32 | } 33 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 34 | kotlinOptions { 35 | freeCompilerArgs = ['-Xjvm-default=enable'] 36 | jvmTarget = "1.8" 37 | } 38 | } 39 | group GROUP 40 | version getVersionName() 41 | } 42 | 43 | snyk { 44 | // severity = 'high' 45 | autoDownload = true 46 | autoUpdate = true 47 | arguments = '--all-sub-projects --fail-on=upgradable' 48 | } 49 | 50 | task clean(type: Delete) { 51 | delete rootProject.buildDir 52 | } 53 | 54 | def getVersionName() { // If not release build add SNAPSHOT suffix 55 | return hasProperty('release') ? VERSION_NAME : VERSION_NAME+"-SNAPSHOT" 56 | } 57 | 58 | apply from: rootProject.file('gradle/promote.gradle') 59 | apply from: rootProject.file('gradle/codecov.gradle') -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'kotlin' 4 | id 'org.jetbrains.kotlin.plugin.serialization' 5 | } 6 | 7 | java { 8 | sourceCompatibility = JavaVersion.VERSION_1_8 9 | targetCompatibility = JavaVersion.VERSION_1_8 10 | } 11 | 12 | compileKotlin { 13 | kotlinOptions { 14 | freeCompilerArgs += [ 15 | "-Xopt-in=kotlin.RequiresOptIn" 16 | ] 17 | } 18 | } 19 | 20 | test { 21 | useJUnitPlatform() 22 | } 23 | 24 | dependencies { 25 | // MAIN DEPS 26 | api 'com.segment:sovran-kotlin:1.2.2' 27 | api "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1" 28 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1' 29 | 30 | // TESTING 31 | repositories { mavenCentral() } 32 | testImplementation 'io.mockk:mockk:1.10.6' 33 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1' 34 | 35 | testImplementation platform("org.junit:junit-bom:5.7.2") 36 | testImplementation "org.junit.jupiter:junit-jupiter" 37 | } 38 | 39 | apply from: rootProject.file('gradle/artifacts-core.gradle') 40 | apply from: rootProject.file('gradle/mvn-publish.gradle') 41 | apply from: rootProject.file('gradle/codecov.gradle') -------------------------------------------------------------------------------- /core/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Segment Kotlin Analytics 2 | POM_ARTIFACT_ID=core 3 | POM_PACKAGING=jar 4 | -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/Annotations.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core 2 | 3 | @RequiresOptIn( 4 | level = RequiresOptIn.Level.WARNING, 5 | message = "This method invokes `runBlocking` internal, it's not recommended to be used in coroutines." 6 | ) 7 | annotation class BlockingApi -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/Configuration.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core 2 | 3 | import com.segment.analytics.kotlin.core.Constants.DEFAULT_API_HOST 4 | import com.segment.analytics.kotlin.core.Constants.DEFAULT_CDN_HOST 5 | import com.segment.analytics.kotlin.core.platform.policies.FlushPolicy 6 | import com.segment.analytics.kotlin.core.utilities.ConcreteStorageProvider 7 | import kotlinx.coroutines.* 8 | import sovran.kotlin.Store 9 | 10 | /** 11 | * Configuration that analytics can use 12 | * @property writeKey the Segment writeKey 13 | * @property application defaults to `null` 14 | * @property storageProvider Provider for storage class, defaults to `ConcreteStorageProvider` 15 | * @property collectDeviceId collect deviceId, defaults to `false` 16 | * @property trackApplicationLifecycleEvents automatically send track for Lifecycle events (eg: Application Opened, Application Backgrounded, etc.), defaults to `false` 17 | * @property useLifecycleObserver enables the use of LifecycleObserver to track Application lifecycle events. Defaults to `false`. 18 | * @property trackDeepLinks automatically track [Deep link][https://developer.android.com/training/app-links/deep-linking] opened based on intents, defaults to `false` 19 | * @property flushAt count of events at which we flush events, defaults to `20` 20 | * @property flushInterval interval in seconds at which we flush events, defaults to `30 seconds` 21 | * @property defaultSettings Settings object that will be used as fallback in case of network failure, defaults to empty 22 | * @property autoAddSegmentDestination automatically add SegmentDestination plugin, defaults to `true` 23 | * @property apiHost set a default apiHost to which Segment sends events, defaults to `api.segment.io/v1` 24 | */ 25 | data class Configuration( 26 | val writeKey: String, 27 | var application: Any? = null, 28 | var storageProvider: StorageProvider = ConcreteStorageProvider, 29 | var collectDeviceId: Boolean = false, 30 | var trackApplicationLifecycleEvents: Boolean = false, 31 | var useLifecycleObserver: Boolean = false, 32 | var trackDeepLinks: Boolean = false, 33 | var flushAt: Int = 20, 34 | var flushInterval: Int = 30, 35 | var flushPolicies: List = emptyList(), 36 | var defaultSettings: Settings? = null, 37 | var autoAddSegmentDestination: Boolean = true, 38 | var apiHost: String = DEFAULT_API_HOST, 39 | var cdnHost: String = DEFAULT_CDN_HOST, 40 | var requestFactory: RequestFactory = RequestFactory(), 41 | var errorHandler: ErrorHandler? = null 42 | ) { 43 | fun isValid(): Boolean { 44 | return writeKey.isNotBlank() && application != null 45 | } 46 | } 47 | 48 | interface CoroutineConfiguration { 49 | val store: Store 50 | 51 | val analyticsScope: CoroutineScope 52 | 53 | val analyticsDispatcher: CoroutineDispatcher 54 | 55 | val networkIODispatcher: CoroutineDispatcher 56 | 57 | val fileIODispatcher: CoroutineDispatcher 58 | } 59 | 60 | typealias ErrorHandler = (Throwable) -> Unit -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core 2 | 3 | object Constants { 4 | const val LIBRARY_VERSION = "1.19.2" 5 | const val DEFAULT_API_HOST = "api.segment.io/v1" 6 | const val DEFAULT_CDN_HOST = "cdn-settings.segment.com/v1" 7 | } 8 | -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/Errors.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core 2 | 3 | import com.segment.analytics.kotlin.core.platform.plugins.logger.segmentLog 4 | import java.net.URL 5 | 6 | sealed class AnalyticsError(): Throwable() { 7 | data class StorageUnableToCreate(override val message: String?): AnalyticsError() 8 | data class StorageUnableToWrite(override val message: String?): AnalyticsError() 9 | data class StorageUnableToRename(override val message: String?): AnalyticsError() 10 | data class StorageUnableToOpen(override val message: String?): AnalyticsError() 11 | data class StorageUnableToClose(override val message: String?): AnalyticsError() 12 | data class StorageInvalid(override val message: String?): AnalyticsError() 13 | data class StorageUnknown(override val cause: Throwable?): AnalyticsError() 14 | 15 | data class NetworkUnexpectedHTTPCode(val uri: URL?, val code: Int): AnalyticsError() 16 | data class NetworkServerLimited(val uri: URL?, val code: Int): AnalyticsError() 17 | data class NetworkServerRejected(val uri: URL?, val code: Int): AnalyticsError() 18 | data class NetworkUnknown(val uri: URL?, override val cause: Throwable?): AnalyticsError() 19 | data class NetworkInvalidData(override val message: String?): AnalyticsError() 20 | 21 | data class JsonUnableToSerialize(override val cause: Throwable?): AnalyticsError() 22 | data class JsonUnableToDeserialize(override val cause: Throwable?): AnalyticsError() 23 | data class JsonUnknown(override val cause: Throwable?): AnalyticsError() 24 | 25 | data class PluginError(override val cause: Throwable?): AnalyticsError() 26 | 27 | data class EnrichmentError(override val message: String): AnalyticsError() 28 | 29 | data class SettingsFail(override val cause: AnalyticsError): AnalyticsError() 30 | data class BatchUploadFail(override val cause: AnalyticsError): AnalyticsError() 31 | } 32 | 33 | /** 34 | * Reports an internal error to the user-defined error handler. 35 | */ 36 | fun Analytics.reportInternalError(error: Throwable) { 37 | configuration.errorHandler?.invoke(error) 38 | Analytics.reportInternalError(error) 39 | } 40 | 41 | fun reportErrorWithMetrics(analytics: Analytics?, error: Throwable, 42 | message: String, metric:String, 43 | log: String, buildTags: (MutableMap) -> Unit) { 44 | analytics?.configuration?.errorHandler?.invoke(error) 45 | var fullMessage = message 46 | error.message?.let { fullMessage += ": $it"} 47 | Analytics.segmentLog(fullMessage) 48 | Telemetry.error(metric, log, buildTags) 49 | } 50 | 51 | fun Analytics.Companion.reportInternalError(error: Throwable) { 52 | error.message?.let { 53 | Analytics.segmentLog(it) 54 | } 55 | } 56 | 57 | fun Analytics.Companion.reportInternalError(error: String) { 58 | Analytics.segmentLog(error) 59 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/compat/ConfigurationBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.compat 2 | 3 | import com.segment.analytics.kotlin.core.Configuration 4 | import com.segment.analytics.kotlin.core.RequestFactory 5 | 6 | /** 7 | * This class serves as a helper class for Java compatibility, which makes the 8 | * @see Configuration buildable through a builder pattern. 9 | * It's strongly discouraged to use this builder in a Kotlin based project, since 10 | * the optional parameters is the way to go in Kotlin. 11 | */ 12 | class ConfigurationBuilder (writeKey: String) { 13 | 14 | private val configuration: Configuration = Configuration(writeKey) 15 | 16 | fun setApplication(application: Any?) = apply { configuration.application = application } 17 | 18 | fun setCollectDeviceId(collectDeviceId: Boolean) = apply { configuration.collectDeviceId = collectDeviceId } 19 | 20 | fun setTrackApplicationLifecycleEvents(trackApplicationLifecycleEvents: Boolean) = apply { configuration.trackApplicationLifecycleEvents = trackApplicationLifecycleEvents } 21 | 22 | fun setUseLifecycleObserver(useLifecycleObserver: Boolean) = apply { configuration.useLifecycleObserver = useLifecycleObserver } 23 | 24 | fun setTrackDeepLinks(trackDeepLinks: Boolean) = apply { configuration.trackDeepLinks = trackDeepLinks } 25 | 26 | fun setFlushAt(flushAt: Int) = apply { configuration.flushAt = flushAt } 27 | 28 | fun setFlushInterval(flushInterval: Int) = apply { configuration.flushInterval = flushInterval } 29 | 30 | fun setAutoAddSegmentDestination(autoAddSegmentDestination: Boolean) = apply { configuration.autoAddSegmentDestination = autoAddSegmentDestination} 31 | 32 | fun setApiHost(apiHost: String) = apply { configuration.apiHost = apiHost} 33 | 34 | fun setCdnHost(cdnHost: String) = apply { configuration.cdnHost = cdnHost} 35 | 36 | fun setRequestFactory(requestFactory: RequestFactory) = apply { configuration.requestFactory = requestFactory } 37 | 38 | fun build() = configuration 39 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/compat/JsonSerializable.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.compat 2 | 3 | import kotlinx.serialization.json.JsonObject 4 | 5 | interface JsonSerializable { 6 | fun serialize() : JsonObject 7 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/plugins/ContextPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.plugins 2 | 3 | import com.segment.analytics.kotlin.core.Analytics 4 | import com.segment.analytics.kotlin.core.BaseEvent 5 | import com.segment.analytics.kotlin.core.Constants.LIBRARY_VERSION 6 | import com.segment.analytics.kotlin.core.platform.Plugin 7 | import com.segment.analytics.kotlin.core.utilities.putAll 8 | import kotlinx.serialization.json.JsonObject 9 | import kotlinx.serialization.json.buildJsonObject 10 | import kotlinx.serialization.json.put 11 | import java.util.* 12 | 13 | /** 14 | * Analytics plugin used to populate events with basic context data. 15 | * Auto-added to analytics client on construction 16 | */ 17 | class ContextPlugin : Plugin { 18 | override val type: Plugin.Type = Plugin.Type.Before 19 | override lateinit var analytics: Analytics 20 | 21 | private lateinit var library: JsonObject 22 | private val instanceId = UUID.randomUUID().toString() 23 | 24 | companion object { 25 | // Library 26 | const val LIBRARY_KEY = "library" 27 | const val LIBRARY_NAME_KEY = "name" 28 | const val LIBRARY_VERSION_KEY = "version" 29 | const val INSTANCE_ID_KEY = "instanceId" 30 | } 31 | 32 | override fun setup(analytics: Analytics) { 33 | super.setup(analytics) 34 | library = buildJsonObject { 35 | put(LIBRARY_NAME_KEY, "analytics-kotlin") 36 | put(LIBRARY_VERSION_KEY, LIBRARY_VERSION) 37 | } 38 | } 39 | 40 | private fun applyContextData(event: BaseEvent) { 41 | val newContext = buildJsonObject { 42 | // copy existing context 43 | putAll(event.context) 44 | 45 | // putLibrary 46 | put(LIBRARY_KEY, library) 47 | put(INSTANCE_ID_KEY, instanceId) 48 | } 49 | event.context = newContext 50 | } 51 | 52 | override fun execute(event: BaseEvent): BaseEvent { 53 | applyContextData(event) 54 | return event 55 | } 56 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/plugins/DestinationMetadataPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.plugins 2 | 3 | import com.segment.analytics.kotlin.core.Analytics 4 | import com.segment.analytics.kotlin.core.BaseEvent 5 | import com.segment.analytics.kotlin.core.DestinationMetadata 6 | import com.segment.analytics.kotlin.core.Settings 7 | import com.segment.analytics.kotlin.core.platform.DestinationPlugin 8 | import com.segment.analytics.kotlin.core.platform.Plugin 9 | import com.segment.analytics.kotlin.core.utilities.safeJsonArray 10 | import com.segment.analytics.kotlin.core.utilities.safeJsonObject 11 | import kotlinx.serialization.json.JsonPrimitive 12 | 13 | /** 14 | * DestinationMetadataPlugin adds `_metadata` information to payloads that Segment uses to 15 | * determine delivery of events to cloud/device-mode destinations 16 | */ 17 | class DestinationMetadataPlugin : Plugin { 18 | override val type: Plugin.Type = Plugin.Type.Enrichment 19 | override lateinit var analytics: Analytics 20 | private var analyticsSettings: Settings = Settings() 21 | 22 | override fun update(settings: Settings, type: Plugin.UpdateType) { 23 | super.update(settings, type) 24 | analyticsSettings = settings 25 | } 26 | 27 | override fun execute(event: BaseEvent): BaseEvent { 28 | // Using this over `findAll` for that teensy performance benefit 29 | val enabledDestinations = analytics.timeline.plugins[Plugin.Type.Destination]?.plugins 30 | ?.map { it as DestinationPlugin } 31 | ?.filter { it.enabled && it !is SegmentDestination } 32 | val metadata = DestinationMetadata().apply { 33 | // Mark all loaded destinations as bundled 34 | val bundled = buildSet { enabledDestinations?.forEach { add(it.key) } } 35 | 36 | // All active integrations, not in `bundled` are put in `unbundled` 37 | // All unbundledIntegrations not in `bundled` are put in `unbundled` 38 | val unbundled = buildSet { 39 | analyticsSettings.integrations.keys.forEach { 40 | if (it != "Segment.io" && !bundled.contains(it)) { 41 | add(it) 42 | } 43 | } 44 | 45 | analyticsSettings.integrations["Segment.io"]?.safeJsonObject 46 | ?.get("unbundledIntegrations")?.safeJsonArray 47 | ?.forEach { 48 | val content = (it as JsonPrimitive).content 49 | if (!bundled.contains(content)) { 50 | add(content) 51 | } 52 | } 53 | } 54 | 55 | // `bundledIds` for mobile is empty 56 | this.bundledIds = emptyList() 57 | this.bundled = bundled.toList() 58 | this.unbundled = unbundled.toList() 59 | } 60 | 61 | val payload = event.copy().apply { 62 | this._metadata = metadata 63 | } 64 | 65 | return payload 66 | } 67 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/plugins/DeviceToken.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.plugins 2 | 3 | import com.segment.analytics.kotlin.core.Analytics 4 | import com.segment.analytics.kotlin.core.BaseEvent 5 | import com.segment.analytics.kotlin.core.platform.Plugin 6 | import com.segment.analytics.kotlin.core.utilities.putInContextUnderKey 7 | 8 | /** 9 | * Analytics plugin to add device token to events 10 | */ 11 | class DeviceToken(var token: String) : Plugin { 12 | override var type = Plugin.Type.Before 13 | override lateinit var analytics: Analytics 14 | 15 | override fun execute(event: BaseEvent): BaseEvent { 16 | event.putInContextUnderKey("device", "token", token) 17 | return event 18 | } 19 | } 20 | 21 | /** 22 | * Set a device token in your payload's context 23 | * @param token [String] Device Token to add to payloads 24 | */ 25 | fun Analytics.setDeviceToken(token: String) { 26 | var tokenPlugin = find(DeviceToken::class) 27 | if (tokenPlugin != null) { 28 | tokenPlugin.token = token 29 | } else { 30 | tokenPlugin = DeviceToken(token) 31 | add(tokenPlugin) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/plugins/StartupQueue.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.plugins 2 | 3 | import com.segment.analytics.kotlin.core.Analytics 4 | import com.segment.analytics.kotlin.core.BaseEvent 5 | import com.segment.analytics.kotlin.core.System 6 | import com.segment.analytics.kotlin.core.platform.Plugin 7 | import kotlinx.coroutines.launch 8 | import com.segment.analytics.kotlin.core.platform.plugins.logger.* 9 | import sovran.kotlin.Subscriber 10 | import java.util.Queue 11 | import java.util.concurrent.ConcurrentLinkedQueue 12 | import java.util.concurrent.atomic.AtomicBoolean 13 | 14 | /** 15 | * Analytics plugin to manage started state of analytics client 16 | * All events will be held in an in-memory queue until started state is enabled, and once enabled 17 | * events will be replayed into the analytics timeline 18 | */ 19 | class StartupQueue : Plugin, Subscriber { 20 | override val type: Plugin.Type = Plugin.Type.Before 21 | override lateinit var analytics: Analytics 22 | 23 | private val maxSize = 1000 24 | private val started: AtomicBoolean = AtomicBoolean(false) 25 | private val queuedEvents: Queue = ConcurrentLinkedQueue() 26 | 27 | override fun setup(analytics: Analytics) { 28 | super.setup(analytics) 29 | with(analytics) { 30 | analyticsScope.launch(analyticsDispatcher) { 31 | store.subscribe( 32 | subscriber = this@StartupQueue, 33 | stateClazz = System::class, 34 | initialState = true, 35 | handler = this@StartupQueue::runningUpdate 36 | ) 37 | } 38 | } 39 | } 40 | 41 | override fun execute(event: BaseEvent): BaseEvent? { 42 | if (!started.get()) { 43 | analytics.log("SegmentStartupQueue queueing event") 44 | // timeline hasn't started, so queue it up. 45 | if (queuedEvents.size >= maxSize) { 46 | // if we've exceeded the max queue size start dropping events 47 | queuedEvents.remove() 48 | } 49 | queuedEvents.offer(event) 50 | return null 51 | } 52 | // the timeline has started, so let the event pass. 53 | return event 54 | } 55 | 56 | // Handler to manage system update 57 | private fun runningUpdate(state: System) { 58 | analytics.log("Analytics starting = ${state.running}") 59 | started.set(state.running) 60 | if (started.get()) { 61 | replayEvents() 62 | } 63 | } 64 | 65 | private fun replayEvents() { 66 | // replay the queued events to the instance of Analytics we're working with. 67 | while (!queuedEvents.isEmpty()) { 68 | 69 | val event = queuedEvents.poll() 70 | 71 | // It is possible that event might actually be null due to time-slicing 72 | // after checking if the queue is empty so we only process if the event 73 | // if it is indeed not NULL. 74 | event?.let { 75 | analytics.process(it, it.enrichment) 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/plugins/UserInfoPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.plugins 2 | 3 | import com.segment.analytics.kotlin.core.* 4 | import com.segment.analytics.kotlin.core.platform.Plugin 5 | 6 | /** 7 | * Analytics plugin used to populate events with basic UserInfo data. 8 | * Auto-added to analytics client on construction 9 | */ 10 | class UserInfoPlugin : Plugin { 11 | 12 | override val type: Plugin.Type = Plugin.Type.Before 13 | override lateinit var analytics: Analytics 14 | 15 | override fun execute(event: BaseEvent): BaseEvent { 16 | 17 | if (event.type == EventType.Identify) { 18 | 19 | analytics.userInfo.userId = event.userId 20 | analytics.userInfo.anonymousId = event.anonymousId 21 | analytics.userInfo.traits = (event as IdentifyEvent).traits 22 | 23 | } else if (event.type === EventType.Alias) { 24 | 25 | analytics.userInfo.anonymousId = event.anonymousId 26 | } else { 27 | 28 | analytics.userInfo.userId?.let { 29 | event.userId = analytics.userInfo.userId.toString() 30 | } 31 | analytics.userInfo.anonymousId?.let { 32 | event.anonymousId = analytics.userInfo.anonymousId.toString() 33 | } 34 | } 35 | 36 | return event 37 | } 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/plugins/logger/ConsoleLogger.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.plugins.logger 2 | 3 | class ConsoleLogger: Logger { 4 | override fun parseLog(log: LogMessage) { 5 | println("[Segment ${log.kind.toString()} ${log.message}") 6 | } 7 | 8 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/plugins/logger/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.plugins.logger 2 | 3 | import com.segment.analytics.kotlin.core.Analytics 4 | import java.util.* 5 | 6 | /** 7 | * The foundation for building out a special logger. If logs need to be directed to a certain area, this is the 8 | * interface to start off with. For instance a console logger, a networking logger or offline storage logger 9 | * would all start off with LogTarget. 10 | */ 11 | interface Logger { 12 | 13 | /** 14 | * Implement this method to process logging messages. This is where the logic for the target will be 15 | * added. Feel free to add your own data queueing and offline storage. 16 | * - important: Use the Segment Network stack for Segment library compatibility and simplicity. 17 | */ 18 | fun parseLog(log: LogMessage) 19 | } 20 | 21 | /** 22 | * Used for analytics.log() types. This lets the system know what to filter on and how to set priorities. 23 | */ 24 | enum class LogKind { 25 | ERROR, 26 | WARNING, 27 | DEBUG; 28 | 29 | override fun toString(): String { 30 | return when(this) { 31 | ERROR -> "ERROR" // Not Verbose (fail cases | non-recoverable errors) 32 | WARNING -> "Warning" // Semi-verbose (deprecations | potential issues) 33 | DEBUG -> "Debug" // Verbose (everything of interest) 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * The interface to the message being returned to `LogTarget` -> `parseLog()`. 40 | */ 41 | data class LogMessage( 42 | val kind: LogKind, 43 | val message: String, 44 | val dateTime: Date = Date()) 45 | 46 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 47 | /** 48 | * The public logging method for capturing all general types of log messages related to Segment. 49 | * 50 | * @property message The main message of the log to be captured. 51 | * @property kind Usually .error, .warning or .debug, in order of severity. This helps filter logs based on 52 | * this added metadata. 53 | * @property function The name of the function the log came from. This will be captured automatically. 54 | * @property line The line number in the function the log came from. This will be captured automatically. 55 | */ 56 | @JvmOverloads 57 | fun Analytics.log(message: String, kind: LogKind = LogKind.DEBUG) { 58 | Analytics.segmentLog(message, kind) 59 | } 60 | -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/plugins/logger/SegmentLog.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.plugins.logger 2 | 3 | import com.segment.analytics.kotlin.core.Analytics 4 | 5 | // Internal log usage 6 | fun Analytics.Companion.segmentLog(message: String, kind: LogKind = LogKind.ERROR) { 7 | val logger = logger 8 | val logMessage = LogMessage(kind, message=message) 9 | when (kind){ 10 | LogKind.DEBUG -> { 11 | if (debugLogsEnabled) { 12 | logger.parseLog(logMessage) 13 | } 14 | } 15 | else -> logger.parseLog(logMessage) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/policies/CountBasedFlushPolicy.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.policies 2 | 3 | import com.segment.analytics.kotlin.core.BaseEvent 4 | 5 | /** 6 | * A Count based Flush Policy that instructs the EventPipeline to flush at the 7 | * given @param[flushAt]. The default value is 20. @param[flushAt] values should 8 | * be >= 1 or they'll get the default value. 9 | */ 10 | class CountBasedFlushPolicy(var flushAt: Int = 20): FlushPolicy { 11 | 12 | 13 | init { 14 | // Make sure to only take valid counts or fallback to our default. 15 | flushAt = when { 16 | flushAt >= 1 -> flushAt 17 | else -> 20 18 | } 19 | } 20 | 21 | private var count: Int = 0 22 | 23 | override fun shouldFlush(): Boolean { 24 | return count >= flushAt 25 | } 26 | 27 | override fun updateState(event: BaseEvent) { 28 | count++ 29 | } 30 | 31 | override fun reset() { 32 | count = 0 33 | } 34 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/policies/FlushPolicy.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.policies 2 | 3 | import com.segment.analytics.kotlin.core.Analytics 4 | import com.segment.analytics.kotlin.core.BaseEvent 5 | import kotlinx.coroutines.CoroutineScope 6 | 7 | interface FlushPolicy { 8 | 9 | /** 10 | * Called when the policy becomes active. We should start any periodic flushing 11 | * we want here. 12 | */ 13 | fun schedule(analytics: Analytics) = Unit 14 | 15 | /** 16 | * Called when policy should stop running any scheduled flushes 17 | */ 18 | fun unschedule() = Unit 19 | 20 | /** 21 | * Called to check whether or not the events should be flushed. 22 | */ 23 | fun shouldFlush(): Boolean 24 | 25 | /** 26 | * Called as events are added to the timeline and JSON Stringified. 27 | */ 28 | fun updateState(event: BaseEvent) = Unit 29 | 30 | /** 31 | * Called after the events are flushed. 32 | */ 33 | fun reset() = Unit 34 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/policies/FrequencyFlushPolicy.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.policies 2 | 3 | import com.segment.analytics.kotlin.core.Analytics 4 | import kotlinx.coroutines.Job 5 | import kotlinx.coroutines.delay 6 | import kotlinx.coroutines.isActive 7 | import kotlinx.coroutines.launch 8 | 9 | class FrequencyFlushPolicy(var flushIntervalInMillis: Long = 30_000): FlushPolicy { 10 | 11 | var flushJob: Job? = null 12 | var jobStarted: Boolean = false 13 | 14 | override fun schedule(analytics: Analytics) { 15 | 16 | if (!jobStarted) { 17 | jobStarted = true 18 | 19 | flushJob = analytics.analyticsScope.launch(analytics.fileIODispatcher) { 20 | 21 | if (flushIntervalInMillis > 0) { 22 | 23 | while (isActive) { 24 | analytics.flush() 25 | 26 | // use delay to do periodical task 27 | // this is doable in coroutine, since delay only suspends, allowing thread to 28 | // do other work and then come back. see: 29 | // https://github.com/Kotlin/kotlinx.coroutines/issues/1632#issuecomment-545693827 30 | delay(flushIntervalInMillis) 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | override fun unschedule() { 38 | if (jobStarted) { 39 | jobStarted = false 40 | flushJob?.cancel() 41 | } 42 | } 43 | 44 | override fun shouldFlush(): Boolean = false // Always return false; Scheduler will call flush. 45 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/platform/policies/StartupFlushPolicy.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.policies 2 | 3 | /** 4 | * Flush policy that dictates flushing events at app startup. 5 | */ 6 | class StartupFlushPolicy: FlushPolicy { 7 | 8 | private var flushedAtStartup = false 9 | 10 | override fun shouldFlush(): Boolean { 11 | return when { 12 | flushedAtStartup -> false 13 | else -> { 14 | // Set to 'true' so we never flush again 15 | flushedAtStartup = true 16 | true 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/utilities/AnySerializer.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.utilities 2 | 3 | import kotlinx.serialization.* 4 | import kotlinx.serialization.descriptors.SerialDescriptor 5 | import kotlinx.serialization.encoding.Decoder 6 | import kotlinx.serialization.encoding.Encoder 7 | import kotlinx.serialization.json.Json 8 | import kotlinx.serialization.modules.SerializersModule 9 | import kotlinx.serialization.serializer 10 | 11 | object AnySerializer: KSerializer { 12 | override fun deserialize(decoder: Decoder): Any { 13 | // Stub function; should not be called. 14 | return "not-implemented"; 15 | } 16 | 17 | @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) 18 | override val descriptor: SerialDescriptor = ContextualSerializer(Any::class, null, emptyArray()).descriptor 19 | 20 | 21 | override fun serialize(encoder: Encoder, value: Any) { 22 | val toJsonElement = value.toJsonElement() 23 | encoder.encodeSerializableValue(Json.serializersModule.serializer(), toJsonElement) 24 | } 25 | } 26 | 27 | /** 28 | * A pre-configured Json Implementation with an Any type serializer. 29 | */ 30 | val JsonAnySerializer = Json { serializersModule = SerializersModule { 31 | contextual(Any::class) { AnySerializer } 32 | } } 33 | -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/utilities/Base64Utils.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("Base64Utils") 2 | package com.segment.analytics.kotlin.core.utilities 3 | 4 | // Encode string to base64 5 | fun encodeToBase64(str: String) = encodeToBase64(str.toByteArray()) 6 | 7 | // Encode byte-array to base64, this implementation is not url-safe 8 | fun encodeToBase64(bytes: ByteArray) = buildString { 9 | val wData = ByteArray(3) // working data 10 | var i = 0 11 | while (i < bytes.size) { 12 | val leftover = bytes.size - i 13 | val available = if (leftover >= 3) { 14 | 3 15 | } else { 16 | leftover 17 | } 18 | for (j in 0 until available) { 19 | wData[j] = bytes[i++] 20 | } 21 | for (j in 2 downTo available) { 22 | wData[j] = 0 // clear out 23 | } 24 | // Given a 3 byte block (24 bits), encode it to 4 base64 characters 25 | val chunk = ((wData[0].toInt() and 0xFF) shl 16) or 26 | ((wData[1].toInt() and 0xFF) shl 8) or 27 | (wData[2].toInt() and 0xFF) 28 | 29 | // if we have too little characters in this block, we add padding 30 | val padCount = (wData.size - available) * 8 / 6 31 | 32 | // encode to base64 33 | for (index in 3 downTo padCount) { // 4 base64 characters 34 | val char = (chunk shr (6 * index)) and 0x3f // 0b00111111 35 | append(char.base64Val()) 36 | } 37 | 38 | // add padding if needed 39 | repeat(padCount) { append("=") } 40 | } 41 | } 42 | 43 | private const val ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" 44 | private fun Int.base64Val(): Char = ALPHABET[this] -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/utilities/DateTimeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.utilities 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.* 5 | 6 | class SegmentInstant { 7 | companion object { 8 | private val formatters = object : ThreadLocal() { 9 | override fun initialValue(): SimpleDateFormat { 10 | return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'.'SSSzzz", Locale.ROOT).apply { 11 | timeZone = TimeZone.getTimeZone("UTC") 12 | } 13 | } 14 | } 15 | 16 | /** 17 | * This function is a replacement for Instant.now().toString(). It produces strings in a 18 | * compatible format. 19 | * 20 | * Ex: 21 | * Instant.now(): 2023-04-19T04:03:46.880969Z 22 | * dateTimeNowString(): 2023-04-19T04:03:46.880Z 23 | */ 24 | fun now(): String { 25 | return from(Date()) 26 | } 27 | 28 | internal fun from(date: Date): String { 29 | return formatters.get().format(date).replace("UTC", "Z") 30 | } 31 | } 32 | } 33 | 34 | @Deprecated("Please use SegmentInstant.now() instead", ReplaceWith("SegmentInstant.now()")) 35 | fun dateTimeNowString(): String { 36 | return SegmentInstant.now() 37 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/utilities/FileUtils.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("FileUtils") 2 | package com.segment.analytics.kotlin.core.utilities 3 | 4 | import java.io.File 5 | import java.io.IOException 6 | 7 | /** 8 | * Ensures that a directory is created in the given location, throws an IOException otherwise. 9 | */ 10 | @Throws(IOException::class) 11 | fun createDirectory(location: File) { 12 | if (!(location.exists() || location.mkdirs() || location.isDirectory)) { 13 | throw IOException("Could not create directory at $location") 14 | } 15 | } 16 | 17 | fun removeFileExtension(fileName: String): String { 18 | val lastDotIndex = fileName.lastIndexOf('.') 19 | return if (lastDotIndex != -1 && lastDotIndex > 0) { 20 | fileName.substring(0, lastDotIndex) 21 | } else { 22 | fileName 23 | } 24 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/utilities/KVS.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.utilities 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | 5 | 6 | /** 7 | * Key-value store interface used by eventsFile 8 | */ 9 | interface KVS { 10 | @Deprecated("Deprecated in favor of `get`", ReplaceWith("get(key, defaultVal)")) 11 | fun getInt(key: String, defaultVal: Int): Int = get(key, defaultVal) 12 | @Deprecated("Deprecated in favor of `put`", ReplaceWith("put(key, value)")) 13 | fun putInt(key: String, value: Int): Boolean = put(key, value) 14 | 15 | /** 16 | * Read the value of a given key as integer 17 | * @param key Key 18 | * @param defaultVal Fallback value to use 19 | * @return Value 20 | */ 21 | fun get(key: String, defaultVal: Int): Int 22 | 23 | /** 24 | * Store the key value pair 25 | * @param key Key 26 | * @param value Fallback value to use 27 | * @return Status of the operation 28 | */ 29 | fun put(key: String, value: Int): Boolean 30 | 31 | /** 32 | * Read the value of a given key as integer 33 | * @param key Key 34 | * @param defaultVal Fallback value to use 35 | * @return Value 36 | */ 37 | fun get(key: String, defaultVal: String?): String? 38 | 39 | /** 40 | * Store the key value pair 41 | * @param key Key 42 | * @param value Fallback value to use 43 | * @return Status of the operation 44 | */ 45 | fun put(key: String, value: String): Boolean 46 | 47 | /** 48 | * Remove a key/value pair by key 49 | * 50 | * @param key Key 51 | * @return Status of the operation 52 | */ 53 | fun remove(key: String): Boolean 54 | 55 | /** 56 | * checks if a given key exists 57 | * 58 | * @param Key 59 | * @return Status of the operation 60 | */ 61 | fun contains(key: String): Boolean 62 | } 63 | 64 | class InMemoryPrefs: KVS { 65 | 66 | private val cache = ConcurrentHashMap() 67 | override fun get(key: String, defaultVal: Int): Int { 68 | return if (cache[key] is Int) cache[key] as Int else defaultVal 69 | } 70 | 71 | override fun get(key: String, defaultVal: String?): String? { 72 | return if (cache[key] is String) cache[key] as String else defaultVal 73 | } 74 | 75 | override fun put(key: String, value: Int): Boolean { 76 | cache[key] = value 77 | return true 78 | } 79 | 80 | override fun put(key: String, value: String): Boolean { 81 | cache[key] = value 82 | return true 83 | } 84 | 85 | override fun remove(key: String): Boolean { 86 | cache.remove(key) 87 | return true 88 | } 89 | 90 | override fun contains(key: String) = cache.containsKey(key) 91 | 92 | } -------------------------------------------------------------------------------- /core/src/main/java/com/segment/analytics/kotlin/core/utilities/PropertiesFile.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.utilities 2 | 3 | import java.io.File 4 | import java.io.FileInputStream 5 | import java.io.FileOutputStream 6 | import java.util.Properties 7 | 8 | /** 9 | * A key-value storage built on top of {@link java.util.Properties} 10 | * conforming to {@link com.segment.analytics.kotlin.core.utilities.KVS} interface. 11 | * Ideal for use on JVM systems to store k-v pairs on a file. 12 | */ 13 | class PropertiesFile(val file: File) : KVS { 14 | private val properties: Properties = Properties() 15 | 16 | init { 17 | load() 18 | } 19 | 20 | /** 21 | * Check if underlying file exists, and load properties if true 22 | */ 23 | fun load() { 24 | if (file.exists()) { 25 | FileInputStream(file).use { 26 | properties.load(it) 27 | } 28 | } 29 | else { 30 | file.parentFile.mkdirs() 31 | file.createNewFile() 32 | } 33 | } 34 | 35 | fun save() { 36 | FileOutputStream(file).use { 37 | properties.store(it, null) 38 | } 39 | } 40 | 41 | override fun get(key: String, defaultVal: Int): Int { 42 | return properties.getProperty(key, "").toIntOrNull() ?: defaultVal 43 | } 44 | 45 | override fun get(key: String, defaultVal: String?): String? { 46 | return properties.getProperty(key, defaultVal) 47 | } 48 | 49 | override fun put(key: String, value: Int): Boolean { 50 | properties.setProperty(key, value.toString()) 51 | save() 52 | return true 53 | } 54 | 55 | override fun put(key: String, value: String): Boolean { 56 | properties.setProperty(key, value) 57 | save() 58 | return true 59 | } 60 | 61 | fun putString(key: String, value: String): Boolean { 62 | properties.setProperty(key, value) 63 | save() 64 | return true 65 | } 66 | 67 | fun getString(key: String, defaultVal: String?): String? = 68 | properties.getProperty(key, defaultVal) 69 | 70 | override fun remove(key: String): Boolean { 71 | properties.remove(key) 72 | save() 73 | return true 74 | } 75 | 76 | override fun contains(key: String) = properties.containsKey(key) 77 | } 78 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/ErrorsTest.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core 2 | 3 | import com.segment.analytics.kotlin.core.utils.clearPersistentStorage 4 | import com.segment.analytics.kotlin.core.utils.mockHTTPClient 5 | import com.segment.analytics.kotlin.core.utils.testAnalytics 6 | import io.mockk.every 7 | import io.mockk.mockk 8 | import io.mockk.spyk 9 | import io.mockk.verify 10 | import kotlinx.coroutines.test.TestScope 11 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 12 | import org.junit.jupiter.api.BeforeEach 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.TestInstance 15 | 16 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 17 | class ErrorsTest { 18 | private lateinit var analytics: Analytics 19 | private val testDispatcher = UnconfinedTestDispatcher() 20 | private val testScope = TestScope(testDispatcher) 21 | private val errorHandler = spyk() 22 | 23 | init { 24 | Telemetry.enable = false 25 | } 26 | 27 | @BeforeEach 28 | fun setup() { 29 | clearPersistentStorage() 30 | mockHTTPClient() 31 | val config = Configuration( 32 | writeKey = "123", 33 | application = "Test", 34 | errorHandler = errorHandler, 35 | ) 36 | 37 | analytics = testAnalytics(config, testScope, testDispatcher) 38 | analytics.configuration.autoAddSegmentDestination = false 39 | } 40 | 41 | @Test 42 | fun `custom errorHandler handles error`() { 43 | val error = Exception() 44 | every { anyConstructed().upload(any()) } throws error 45 | 46 | analytics.track("test") 47 | analytics.flush() 48 | 49 | verify { errorHandler.invoke(error) } 50 | } 51 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/PluginTests.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core 2 | 3 | import com.segment.analytics.kotlin.core.platform.Plugin 4 | import com.segment.analytics.kotlin.core.platform.Timeline 5 | import com.segment.analytics.kotlin.core.utils.mockAnalytics 6 | import io.mockk.spyk 7 | import io.mockk.verify 8 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 9 | import kotlinx.coroutines.test.TestScope 10 | import kotlinx.serialization.json.buildJsonObject 11 | import kotlinx.serialization.json.put 12 | import org.junit.jupiter.api.Assertions 13 | import org.junit.jupiter.api.Test 14 | import org.junit.jupiter.api.TestInstance 15 | import org.junit.jupiter.api.assertDoesNotThrow 16 | import java.util.* 17 | 18 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 19 | class PluginTests { 20 | 21 | private val testDispatcher = UnconfinedTestDispatcher() 22 | private val testScope = TestScope(testDispatcher) 23 | private val mockAnalytics = mockAnalytics(testScope, testDispatcher) 24 | private val timeline: Timeline 25 | 26 | init { 27 | Telemetry.enable = false 28 | timeline = Timeline().also { it.analytics = mockAnalytics } 29 | } 30 | 31 | @Test 32 | fun `setup is called`() { 33 | val plugin = object : Plugin { 34 | override val type: Plugin.Type = Plugin.Type.Before 35 | override lateinit var analytics: Analytics 36 | } 37 | val spy = spyk(plugin) 38 | timeline.add(spy) 39 | verify(exactly = 1) { spy.setup(mockAnalytics) } 40 | Assertions.assertTrue(spy.analytics === mockAnalytics) 41 | } 42 | 43 | @Test 44 | fun `plugin processes the event correctly`() { 45 | val plugin = spyk(object : Plugin { 46 | override val type: Plugin.Type = Plugin.Type.Before 47 | override lateinit var analytics: Analytics 48 | }) 49 | timeline.add(plugin) 50 | val trackEvent = TrackEvent( 51 | event = "clicked", 52 | properties = buildJsonObject { put("behaviour", "good") }) 53 | .apply { 54 | messageId = "qwerty-1234" 55 | anonymousId = "anonId" 56 | integrations = emptyJsonObject 57 | context = emptyJsonObject 58 | timestamp = Date(0).toInstant().toString() 59 | } 60 | val result = timeline.process(trackEvent) 61 | Assertions.assertEquals(trackEvent, result) 62 | verify(exactly = 1) { plugin.execute(trackEvent) } 63 | } 64 | 65 | @Test 66 | fun `throwing exception in setup() is handled in timeline`() { 67 | val plugin = object : Plugin { 68 | override val type: Plugin.Type = Plugin.Type.Before 69 | override lateinit var analytics: Analytics 70 | override fun setup(analytics: Analytics) { 71 | super.setup(analytics) 72 | throw Exception("Boom!") 73 | } 74 | } 75 | val spy = spyk(plugin) 76 | assertDoesNotThrow { 77 | timeline.add(spy) 78 | } 79 | verify(exactly = 1) { spy.setup(mockAnalytics) } 80 | Assertions.assertTrue(spy.analytics === mockAnalytics) 81 | } 82 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/SegmentLogTest.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.plugins 2 | 3 | import com.segment.analytics.kotlin.core.Analytics 4 | import com.segment.analytics.kotlin.core.platform.plugins.logger.* 5 | import org.junit.jupiter.api.Assertions.* 6 | 7 | import org.junit.jupiter.api.Test 8 | import java.util.concurrent.atomic.AtomicBoolean 9 | 10 | internal class SegmentLogTest { 11 | 12 | @Test 13 | fun `can call segmentLog() `() { 14 | val parseLogCalled = AtomicBoolean(false) 15 | val testLogger = object : Logger { 16 | override fun parseLog(log: LogMessage) { 17 | if (log.message.contains("test") && log.kind == LogKind.ERROR) { 18 | parseLogCalled.set(true) 19 | } 20 | } 21 | } 22 | Analytics.logger = testLogger 23 | 24 | Analytics.segmentLog("test") 25 | 26 | assertTrue(parseLogCalled.get()) 27 | } 28 | 29 | @Test 30 | fun `can call segmentLog() with different log filter kind`() { 31 | val parseLogErrorCalled = AtomicBoolean(false) 32 | val parseLogWarnCalled = AtomicBoolean(false) 33 | val parseLogDebugCalled = AtomicBoolean(false) 34 | 35 | val testLogger = object: Logger { 36 | override fun parseLog(log: LogMessage) { 37 | 38 | if (log.message.contains("test")) { 39 | when (log.kind) { 40 | LogKind.ERROR -> { 41 | parseLogErrorCalled.set(true) 42 | } 43 | LogKind.WARNING -> { 44 | parseLogWarnCalled.set(true) 45 | } 46 | LogKind.DEBUG -> { 47 | parseLogDebugCalled.set(true) 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | Analytics.logger = testLogger 55 | Analytics.debugLogsEnabled = true 56 | 57 | Analytics.segmentLog("test") // Default LogFilterKind is ERROR 58 | Analytics.segmentLog("test", kind = LogKind.WARNING) 59 | Analytics.segmentLog("test", kind = LogKind.DEBUG) 60 | 61 | assertTrue(parseLogErrorCalled.get()) 62 | assertTrue(parseLogWarnCalled.get()) 63 | assertTrue(parseLogDebugCalled.get()) 64 | } 65 | 66 | @Test 67 | fun `debug logging respects debugLogsEnabled flag`() { 68 | 69 | var logSent = AtomicBoolean(false) 70 | 71 | val testLogger = object : Logger { 72 | override fun parseLog(log: LogMessage) { 73 | logSent.set(true) 74 | } 75 | } 76 | 77 | Analytics.logger = testLogger 78 | 79 | // Turn ON debug logs 80 | Analytics.debugLogsEnabled = true 81 | Analytics.segmentLog("test", kind = LogKind.DEBUG) 82 | 83 | assertTrue(logSent.get()) 84 | 85 | // Turn OFF debug logs 86 | Analytics.debugLogsEnabled = false 87 | logSent.set(false) 88 | 89 | Analytics.segmentLog("test", kind = LogKind.DEBUG) 90 | assertFalse(logSent.get()) 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/plugins/StartupQueueTest.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.plugins 2 | 3 | import com.segment.analytics.kotlin.core.Analytics 4 | import com.segment.analytics.kotlin.core.Configuration 5 | import com.segment.analytics.kotlin.core.TrackEvent 6 | import com.segment.analytics.kotlin.core.utils.testAnalytics 7 | import io.mockk.every 8 | import io.mockk.spyk 9 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 10 | import kotlinx.coroutines.test.TestScope 11 | import kotlinx.serialization.json.buildJsonObject 12 | import kotlinx.serialization.json.put 13 | import org.junit.jupiter.api.Assertions.* 14 | import org.junit.jupiter.api.BeforeEach 15 | 16 | import org.junit.jupiter.api.Test 17 | 18 | internal class StartupQueueTest { 19 | 20 | private lateinit var analytics: Analytics 21 | 22 | private val testDispatcher = UnconfinedTestDispatcher() 23 | 24 | private val testScope = TestScope(testDispatcher) 25 | 26 | @BeforeEach 27 | internal fun setUp() { 28 | val config = Configuration( 29 | writeKey = "123", 30 | application = "Test", 31 | autoAddSegmentDestination = false 32 | ) 33 | analytics = testAnalytics(config, testScope, testDispatcher) 34 | } 35 | 36 | @Test 37 | fun `execute when startup queue not ready`() { 38 | val startupQueue = spyk(StartupQueue()) 39 | every { startupQueue.setup(any()) } answers { startupQueue.analytics = analytics } 40 | 41 | analytics.add(startupQueue) 42 | val trackEvent = TrackEvent( 43 | event = "clicked", 44 | properties = buildJsonObject { put("behaviour", "good") }) 45 | assertNull(startupQueue.execute(trackEvent)) 46 | } 47 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/policies/CountBasedFlushPolicyTests.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.policies 2 | 3 | import com.segment.analytics.kotlin.core.ScreenEvent 4 | import com.segment.analytics.kotlin.core.emptyJsonObject 5 | import org.junit.jupiter.api.Assertions.* 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.TestInstance 8 | 9 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 10 | class CountBasedFlushPolicyTests { 11 | 12 | @Test 13 | fun `Policy defaults to 20 events`() { 14 | assertEquals(20, CountBasedFlushPolicy().flushAt) 15 | } 16 | 17 | @Test 18 | fun `Policy respects flushAt constructor arg`() { 19 | assertEquals(20, CountBasedFlushPolicy().flushAt) // default 20 | assertEquals(30, CountBasedFlushPolicy(30).flushAt) 21 | assertEquals(1, CountBasedFlushPolicy(1).flushAt) 22 | assertEquals(100, CountBasedFlushPolicy(100).flushAt) 23 | } 24 | 25 | @Test 26 | fun `Policy flushes at appropriate time`() { 27 | val flushAt = 10 28 | val defaultPolicy = CountBasedFlushPolicy(flushAt) // default count based policy with a 20 event flushAt value. 29 | assertFalse(defaultPolicy.shouldFlush()) // Should NOT flush before any events 30 | 31 | // all the first 19 events should not cause the policy to be flushed 32 | for( i in 1 until flushAt) { 33 | defaultPolicy.updateState(ScreenEvent("event 1", "", emptyJsonObject)) 34 | assertFalse(defaultPolicy.shouldFlush()) 35 | } 36 | 37 | // next event should trigger the flush 38 | defaultPolicy.updateState(ScreenEvent("event 1", "", emptyJsonObject)) 39 | assertTrue(defaultPolicy.shouldFlush()) 40 | 41 | // Even if we somehow go over the flushAt event limit, the policy should still want to flush 42 | // events 43 | defaultPolicy.updateState(ScreenEvent("event 1", "", emptyJsonObject)) 44 | assertTrue(defaultPolicy.shouldFlush()) 45 | 46 | // Only when we reset the policy will it not want to flush 47 | defaultPolicy.reset() 48 | assertFalse(defaultPolicy.shouldFlush()) 49 | 50 | // The policy will then be ready to count another N events 51 | for( i in 1 until flushAt) { 52 | defaultPolicy.updateState(ScreenEvent("event 1", "", emptyJsonObject)) 53 | assertFalse(defaultPolicy.shouldFlush()) 54 | } 55 | 56 | // but once again the next event will trigger a flush request 57 | defaultPolicy.updateState(ScreenEvent("event 1", "", emptyJsonObject)) 58 | assertTrue(defaultPolicy.shouldFlush()) 59 | } 60 | 61 | @Test 62 | fun `Policy constructor param flushAt ignored for values less than 1`() { 63 | assertEquals(1, CountBasedFlushPolicy(1).flushAt) // Lowest flushAt that is allowed 64 | assertEquals(20, CountBasedFlushPolicy(0).flushAt) 65 | assertEquals(20, CountBasedFlushPolicy(-1).flushAt) 66 | assertEquals(20, CountBasedFlushPolicy(-20).flushAt) 67 | assertEquals(20, CountBasedFlushPolicy(-1000).flushAt) 68 | assertEquals(20, CountBasedFlushPolicy(-2439872).flushAt) 69 | } 70 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/policies/FrequencyFlushPolicyTests.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.policies 2 | 3 | import com.segment.analytics.kotlin.core.utils.mockAnalytics 4 | import io.mockk.coVerify 5 | import io.mockk.verify 6 | import kotlinx.coroutines.delay 7 | import kotlinx.coroutines.launch 8 | import kotlinx.coroutines.runBlocking 9 | import kotlinx.coroutines.test.TestScope 10 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 11 | import kotlinx.coroutines.test.advanceTimeBy 12 | import kotlinx.coroutines.test.runTest 13 | import org.junit.jupiter.api.Assertions.assertFalse 14 | import org.junit.jupiter.api.Test 15 | import org.junit.jupiter.api.TestInstance 16 | 17 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 18 | class FrequencyFlushPolicyTests { 19 | 20 | private val testDispatcher = UnconfinedTestDispatcher() 21 | private val testScope = TestScope(testDispatcher) 22 | private val mockAnalytics = mockAnalytics(testScope, testDispatcher) 23 | 24 | @Test 25 | fun `Policy should not ever request flush`() { 26 | val frequencyFlushPolicy = FrequencyFlushPolicy() 27 | 28 | // Should not flush at first 29 | assertFalse(frequencyFlushPolicy.shouldFlush()) 30 | 31 | // Start the policy's scheduler 32 | frequencyFlushPolicy.schedule(analytics = mockAnalytics) 33 | 34 | // Should still not flush 35 | assertFalse(frequencyFlushPolicy.shouldFlush()) 36 | 37 | // Stop the scheduler 38 | frequencyFlushPolicy.unschedule() 39 | 40 | // Should still not flush 41 | assertFalse(frequencyFlushPolicy.shouldFlush()) 42 | 43 | } 44 | 45 | 46 | @Test 47 | fun `Policy should flush when first scheduled`() = runTest { 48 | 49 | val frequencyFlushPolicy = FrequencyFlushPolicy() 50 | 51 | // Start the scheduler 52 | frequencyFlushPolicy.schedule(mockAnalytics) 53 | 54 | // Make sure flush is called just once 55 | coVerify(exactly = 1) { 56 | mockAnalytics.flush() 57 | } 58 | } 59 | 60 | 61 | @Test 62 | fun `Policy should flush after each flush interval`() = runTest { 63 | 64 | val frequencyFlushPolicy = FrequencyFlushPolicy(1_000) 65 | 66 | frequencyFlushPolicy.schedule(mockAnalytics) 67 | 68 | delay(2_500) 69 | 70 | // Make sure flush is called just once 71 | coVerify(atLeast = 1, atMost = 2) { 72 | mockAnalytics.flush() 73 | } 74 | 75 | } 76 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/platform/policies/StartupFlushPolicyTests.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.platform.policies 2 | 3 | import com.segment.analytics.kotlin.core.ScreenEvent 4 | import com.segment.analytics.kotlin.core.emptyJsonObject 5 | import org.junit.jupiter.api.Assertions.* 6 | import org.junit.jupiter.api.Test 7 | import org.junit.jupiter.api.TestInstance 8 | 9 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 10 | class StartupFlushPolicyTests { 11 | 12 | @Test 13 | fun `Policy flushes only first call to shouldFlush`() { 14 | 15 | val startupFlushPolicy = StartupFlushPolicy() 16 | 17 | // Should only flush the first time requested! 18 | assertTrue(startupFlushPolicy.shouldFlush()) 19 | 20 | // Should now not flush any more! 21 | assertFalse(startupFlushPolicy.shouldFlush()) 22 | 23 | // No matter how many times you call shouldFlush() 24 | for (i in 1..10) { 25 | assertFalse(startupFlushPolicy.shouldFlush()) 26 | } 27 | 28 | // even you call reset; the policy will not want to flush. 29 | startupFlushPolicy.reset() 30 | assertFalse(startupFlushPolicy.shouldFlush()) 31 | 32 | // Adding events has no effect and does not cause the policy to flush 33 | startupFlushPolicy.updateState(ScreenEvent("event 1", "", emptyJsonObject)) 34 | assertFalse(startupFlushPolicy.shouldFlush()) 35 | } 36 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/Base64UtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.utilities 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.TestInstance 6 | 7 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 8 | class Base64UtilsTest { 9 | 10 | @Test 11 | fun testBase64Encoding() { 12 | assertEquals(encodeToBase64(""), "") 13 | assertEquals(encodeToBase64("f"), "Zg==") 14 | assertEquals(encodeToBase64("fo"), "Zm8=") 15 | assertEquals(encodeToBase64("foo"), "Zm9v") 16 | assertEquals(encodeToBase64("foob"), "Zm9vYg==") 17 | assertEquals(encodeToBase64("fooba"), "Zm9vYmE=") 18 | assertEquals(encodeToBase64("foobar"), "Zm9vYmFy") 19 | } 20 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/DateTimeUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.utilities 2 | 3 | import io.mockk.every 4 | import io.mockk.mockkConstructor 5 | import io.mockk.mockkObject 6 | import org.junit.jupiter.api.Assertions.assertEquals 7 | import org.junit.jupiter.api.Assertions.assertNotNull 8 | import org.junit.jupiter.api.Test 9 | import java.time.format.DateTimeFormatter.ISO_DATE_TIME 10 | import java.util.* 11 | 12 | class DateTimeUtilsTest { 13 | 14 | @Test 15 | fun `dateTimeNowString() produces a string in the correct ISO8601 format`() { 16 | val dateTimeNowString = SegmentInstant.now() 17 | val date = ISO_DATE_TIME.parse(dateTimeNowString) 18 | assertNotNull(date) 19 | } 20 | 21 | @Test 22 | fun `dateTimeNowString() returns three digit seconds`() { 23 | val date = Date(1700617928023L) 24 | val dateTimeNowString = SegmentInstant.from(date) 25 | assertEquals("2023-11-22T01:52:08.023Z", dateTimeNowString) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/KVSTest.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.utilities 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.BeforeEach 5 | import org.junit.jupiter.api.Nested 6 | import org.junit.jupiter.api.Test 7 | 8 | class KVSTest { 9 | @Nested 10 | inner class InMemoryPrefsTest { 11 | private lateinit var prefs: KVS 12 | 13 | @BeforeEach 14 | fun setup(){ 15 | prefs = InMemoryPrefs() 16 | prefs.put("int", 1) 17 | prefs.put("string", "string") 18 | } 19 | 20 | @Test 21 | fun getTest() { 22 | assertEquals(1, prefs.get("int", 0)) 23 | assertEquals("string", prefs.get("string", null)) 24 | assertEquals(0, prefs.get("keyNotExists", 0)) 25 | assertEquals(null, prefs.get("keyNotExists", null)) 26 | } 27 | 28 | @Test 29 | fun putTest() { 30 | prefs.put("int", 2) 31 | prefs.put("string", "stringstring") 32 | 33 | assertEquals(2, prefs.get("int", 0)) 34 | assertEquals("stringstring", prefs.get("string", null)) 35 | } 36 | 37 | @Test 38 | fun containsAndRemoveTest() { 39 | assertTrue(prefs.contains("int")) 40 | prefs.remove("int") 41 | assertFalse(prefs.contains("int")) 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/utilities/PropertiesFileTest.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.utilities 2 | 3 | 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.BeforeEach 6 | import org.junit.jupiter.api.Test 7 | import java.io.File 8 | 9 | internal class PropertiesFileTest { 10 | 11 | private val directory = File("/tmp/analytics-test/123") 12 | private val kvStore = PropertiesFile(File(directory, "123")) 13 | 14 | @BeforeEach 15 | internal fun setUp() { 16 | directory.deleteRecursively() 17 | kvStore.load() 18 | } 19 | 20 | @Test 21 | fun `test properties file operations`() { 22 | kvStore.putString("string", "test") 23 | kvStore.putInt("int", 1) 24 | 25 | assertEquals(kvStore.get("string", ""), "test") 26 | assertEquals(kvStore.get("int", 0), 1) 27 | 28 | kvStore.remove("int") 29 | assertEquals(kvStore.get("int", 0), 0) 30 | } 31 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/utils/Mocks.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.utils 2 | 3 | import com.segment.analytics.kotlin.core.* 4 | import io.mockk.* 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.test.* 8 | import sovran.kotlin.Store 9 | import java.io.ByteArrayInputStream 10 | import java.io.File 11 | import java.net.HttpURLConnection 12 | import kotlin.coroutines.CoroutineContext 13 | 14 | /** 15 | * Retrieve a relaxed mock of analytics, that can be used while testing plugins 16 | * Current capabilities: 17 | * - In-memory sovran.store 18 | */ 19 | fun mockAnalytics(testScope: TestScope, testDispatcher: TestDispatcher): Analytics { 20 | val mock = mockk(relaxed = true) 21 | val mockStore = spyStore(testScope, testDispatcher) 22 | every { mock.store } returns mockStore 23 | every { mock.analyticsScope } returns testScope 24 | every { mock.fileIODispatcher } returns testDispatcher 25 | every { mock.networkIODispatcher } returns testDispatcher 26 | every { mock.analyticsDispatcher } returns testDispatcher 27 | return mock 28 | } 29 | 30 | fun testAnalytics(configuration: Configuration, testScope: TestScope, testDispatcher: TestDispatcher): Analytics { 31 | return object : Analytics(configuration, TestCoroutineConfiguration(testScope, testDispatcher)) {} 32 | } 33 | 34 | fun clearPersistentStorage(writeKey: String = "123") { 35 | File("/tmp/analytics-kotlin/$writeKey").deleteRecursively() 36 | } 37 | 38 | fun spyStore(scope: TestScope, dispatcher: TestDispatcher): Store { 39 | val store = spyk(Store()) 40 | every { store getProperty "sovranScope" } propertyType CoroutineScope::class returns scope 41 | every { store getProperty "syncQueue" } propertyType CoroutineContext::class returns dispatcher 42 | every { store getProperty "updateQueue" } propertyType CoroutineContext::class returns dispatcher 43 | return store 44 | } 45 | val settingsDefault = """ 46 | {"integrations":{"Segment.io":{"apiKey":"1vNgUqwJeCHmqgI9S1sOm9UHCyfYqbaQ"}},"plan":{},"edgeFunction":{}} 47 | """.trimIndent() 48 | 49 | fun mockHTTPClient(settings: String = settingsDefault) { 50 | mockkConstructor(HTTPClient::class) 51 | val settingsStream = ByteArrayInputStream( 52 | settings.toByteArray() 53 | ) 54 | val httpConnection: HttpURLConnection = mockk() 55 | val connection = object : Connection(httpConnection, settingsStream, null) {} 56 | every { anyConstructed().settings("cdn-settings.segment.com/v1") } returns connection 57 | } 58 | 59 | class TestCoroutineConfiguration( 60 | val testScope: TestScope, 61 | val testDispatcher: TestDispatcher 62 | ) : CoroutineConfiguration { 63 | 64 | override val store: Store = spyStore(testScope, testDispatcher) 65 | 66 | override val analyticsScope: CoroutineScope 67 | get() = testScope 68 | 69 | override val analyticsDispatcher: CoroutineDispatcher 70 | get() = testDispatcher 71 | 72 | override val networkIODispatcher: CoroutineDispatcher 73 | get() = testDispatcher 74 | 75 | override val fileIODispatcher: CoroutineDispatcher 76 | get() = testDispatcher 77 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/segment/analytics/kotlin/core/utils/Plugins.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.core.utils 2 | 3 | import com.segment.analytics.kotlin.core.* 4 | import com.segment.analytics.kotlin.core.platform.* 5 | 6 | /** 7 | * An analytics plugin that can be used to test features 8 | * Current capabilities 9 | * - is a `Before` plugin so is guaranteed to be run 10 | * - can add a closure that will be run if any hook is executed 11 | * - has a boolean state `ran` that can be used to verify if a particular hook was executed 12 | * - has a `reset()` function to reset state between tests 13 | */ 14 | class TestRunPlugin(var closure: (BaseEvent?) -> Unit): EventPlugin { 15 | override val type: Plugin.Type = Plugin.Type.Before 16 | override lateinit var analytics: Analytics 17 | var ran = false 18 | 19 | override fun reset() { 20 | ran = false 21 | } 22 | 23 | fun updateState(ran: Boolean) { 24 | this.ran = ran 25 | } 26 | 27 | override fun execute(event: BaseEvent): BaseEvent { 28 | super.execute(event) 29 | updateState(true) 30 | return event 31 | } 32 | 33 | override fun track(payload: TrackEvent): BaseEvent { 34 | closure(payload) 35 | updateState(true) 36 | return payload 37 | } 38 | 39 | override fun identify(payload: IdentifyEvent): BaseEvent { 40 | closure(payload) 41 | updateState(true) 42 | return payload 43 | } 44 | 45 | override fun screen(payload: ScreenEvent): BaseEvent { 46 | closure(payload) 47 | updateState(true) 48 | return payload 49 | } 50 | 51 | override fun group(payload: GroupEvent): BaseEvent { 52 | closure(payload) 53 | updateState(true) 54 | return payload 55 | } 56 | 57 | override fun alias(payload: AliasEvent): BaseEvent { 58 | closure(payload) 59 | updateState(true) 60 | return payload 61 | } 62 | } 63 | 64 | /** 65 | * An analytics plugin that is a simple pass-through plugin. Ideally to be used to verify 66 | * if particular hooks are run via mockk's `verify` 67 | */ 68 | open class StubPlugin : EventPlugin { 69 | override val type: Plugin.Type = Plugin.Type.Before 70 | override lateinit var analytics: Analytics 71 | } 72 | 73 | open class StubAfterPlugin : EventPlugin { 74 | override val type: Plugin.Type = Plugin.Type.After 75 | override lateinit var analytics: Analytics 76 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | 23 | # Deployment variables 24 | GROUP=com.segment.analytics.kotlin 25 | 26 | VERSION_CODE=1192 27 | VERSION_NAME=1.19.2 28 | 29 | POM_NAME=Segment for Kotlin 30 | POM_DESCRIPTION=The hassle-free way to add analytics to your Kotlin app. 31 | 32 | POM_URL=http://github.com/segmentio/analytics-kotlin 33 | POM_SCM_URL=http://github.com/segmentio/analytics-kotlin 34 | POM_SCM_CONNECTION=scm:git:git://github.com/segmentio/analytics-kotlin.git 35 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/segmentio/analytics-kotlin.git 36 | 37 | POM_LICENCE_NAME=The MIT License (MIT) 38 | POM_LICENCE_URL=http://opensource.org/licenses/MIT 39 | POM_LICENCE_DIST=repo 40 | 41 | POM_DEVELOPER_ID=segmentio 42 | POM_DEVELOPER_NAME=Segment, Inc. 43 | -------------------------------------------------------------------------------- /gradle/artifacts-android.gradle: -------------------------------------------------------------------------------- 1 | task sourcesJar(type: Jar) { 2 | archiveClassifier.set('sources') 3 | from android.sourceSets.main.java.srcDirs 4 | } 5 | 6 | task javadoc(type: Javadoc) { 7 | configurations.implementation.setCanBeResolved(true) 8 | 9 | failOnError false 10 | source = android.sourceSets.main.java.sourceFiles 11 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 12 | classpath += configurations.implementation 13 | } 14 | 15 | // build a jar with javadoc 16 | task javadocJar(type: Jar, dependsOn: javadoc) { 17 | archiveClassifier.set('javadoc') 18 | from javadoc.destinationDir 19 | } 20 | 21 | // Attach Javadocs and Sources jar 22 | artifacts { 23 | archives sourcesJar 24 | archives javadocJar 25 | } -------------------------------------------------------------------------------- /gradle/artifacts-core.gradle: -------------------------------------------------------------------------------- 1 | task sourcesJar(type: Jar, dependsOn: classes) { 2 | archiveClassifier.set('sources') 3 | from sourceSets.main.allSource 4 | } 5 | 6 | task javadocJar(type: Jar, dependsOn: javadoc) { 7 | archiveClassifier.set('javadoc') 8 | from javadoc.destinationDir 9 | } 10 | 11 | // Attach Javadocs and Sources jar 12 | artifacts { 13 | archives sourcesJar 14 | archives javadocJar 15 | } -------------------------------------------------------------------------------- /gradle/codecov.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'jacoco' 2 | 3 | jacoco { 4 | toolVersion = "0.8.7" 5 | } 6 | 7 | task codeCoverageReport(type: JacocoReport) { 8 | def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', "**/**/*serializer*.*", "**/**/*Companion*.*" ] 9 | def mainSrc = [] 10 | def debugTrees = [] 11 | def execData = [] 12 | if(project.name == rootProject.name) { 13 | subprojects.each { 14 | mainSrc.add("${it.projectDir}/src/main/java") 15 | debugTrees.add(fileTree(dir: "${it.buildDir}/classes", excludes: fileFilter)) 16 | debugTrees.add(fileTree(dir: "${it.buildDir}/tmp/kotlin-classes/debugUnitTest", excludes: fileFilter)) 17 | execData.add(fileTree(dir: "${it.buildDir}/jacoco", includes: ["*.exec"])) 18 | } 19 | } 20 | else { 21 | mainSrc.add("${project.projectDir}/src/main/java") 22 | debugTrees.add(fileTree(dir: "${project.buildDir}/classes", excludes: fileFilter)) 23 | debugTrees.add(fileTree(dir: "${project.buildDir}/tmp/kotlin-classes/debugUnitTest", excludes: fileFilter)) 24 | execData.add(fileTree(dir: "${project.buildDir}/jacoco", includes: ["*.exec"])) 25 | } 26 | 27 | sourceDirectories.setFrom(files(mainSrc)) 28 | classDirectories.setFrom(files(debugTrees)) 29 | executionData.setFrom(execData) 30 | 31 | reports { 32 | xml.enabled true 33 | xml.destination file("${buildDir}/reports/jacoco/report.xml") 34 | html.enabled true 35 | csv.enabled false 36 | } 37 | } -------------------------------------------------------------------------------- /gradle/promote.gradle: -------------------------------------------------------------------------------- 1 | apply from: rootProject.file('gradle/versioning.gradle') 2 | 3 | group GROUP 4 | version getVersionName() 5 | 6 | if (!hasProperty("signing.keyId")) { 7 | ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') 8 | ext["signing.password"] = System.getenv('SIGNING_KEY_PASSWORD') 9 | 10 | def pgpKeyContent = System.getenv('SIGNING_PRIVATE_KEY_BASE64') 11 | if (pgpKeyContent != null) { 12 | def tmpDir = new File("$rootProject.rootDir/tmp") 13 | mkdir tmpDir 14 | def keyFile = new File("$tmpDir/key.pgp") 15 | keyFile.createNewFile() 16 | def os = keyFile.newDataOutputStream() 17 | os.write(pgpKeyContent.decodeBase64()) 18 | os.close() 19 | 20 | ext['signing.secretKeyRingFile'] = keyFile.absolutePath 21 | } 22 | } 23 | 24 | nexusPublishing { 25 | repositories { 26 | /* 27 | nexus publish plugin by default looking for the following as credential: 28 | * sonatypeUsername and sonatypePassword in global gradle property or 29 | * ORG_GRADLE_PROJECT_sonatypeUsername and ORG_GRADLE_PROJECT_sonatypePassword 30 | in system environments 31 | be sure to set the variable names exactly as above. 32 | */ 33 | sonatype() 34 | } 35 | } -------------------------------------------------------------------------------- /gradle/versioning.gradle: -------------------------------------------------------------------------------- 1 | def isReleaseBuild() { 2 | return hasProperty('release') 3 | } 4 | 5 | def getVersionName() { // If not release build add SNAPSHOT suffix 6 | return isReleaseBuild() ? VERSION_NAME : VERSION_NAME+"-SNAPSHOT" 7 | } 8 | 9 | ext { 10 | isReleaseBuild = this.&isReleaseBuild 11 | getVersionName = this.&getVersionName 12 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-kotlin/c2ac4eab1fffec304deeec8cbfe1693cd1a2ec94/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jan 22 15:05:55 PST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /samples/README.md: -------------------------------------------------------------------------------- 1 | # Sample Projects using analytics-kotlin 2 | 3 | - [Java Android Application](java-android-app) 4 | A sample project showcasing the usage of `analytics-kotlin` in a pure java codebase Android application 5 | 6 | - [Kotlin Android Application](kotlin-android-app) 7 | A sample project showcasing the usage of `analytics-kotlin` in a pure kotlin codebase Android application. Also contains sample plugins that are commonly used. 8 | 9 | - [Kotlin Android Application w/ Sample Destination Plugins](kotlin-android-app-destinations) 10 | A sample project showcasing the usage of `analytics-kotlin` in a pure kotlin codebase Android application. This project also hosts sample Destination plugins, and instructions on how to include them in your own projects 11 | 12 | - [Kotlin JVM Application](kotlin-jvm-app) 13 | A sample project showcasing the usage of `analytics-kotlin` in a pure kotlin codebase JVM application. -------------------------------------------------------------------------------- /samples/java-android-app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /samples/java-android-app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdkVersion 33 7 | 8 | defaultConfig { 9 | applicationId "com.segment.analytics.javaandroidapp" 10 | minSdkVersion 16 11 | targetSdkVersion 33 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | buildConfigField "String", "SEGMENT_WRITE_KEY", "\"${writeKey}\"" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | } 31 | 32 | dependencies { 33 | implementation project(':android') 34 | 35 | implementation 'androidx.appcompat:appcompat:1.4.0' 36 | implementation 'com.google.android.material:material:1.4.0' 37 | implementation 'androidx.constraintlayout:constraintlayout:2.1.2' 38 | testImplementation 'junit:junit:4.13.2' 39 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 41 | } -------------------------------------------------------------------------------- /samples/java-android-app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /samples/java-android-app/src/androidTest/java/com/segment/analytics/javaandroidapp/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.javaandroidapp; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.segment.analytics.javaandroidapp", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /samples/java-android-app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /samples/java-android-app/src/main/java/com/segment/analytics/javaandroidapp/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.javaandroidapp; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import android.os.Bundle; 6 | 7 | public class MainActivity extends AppCompatActivity { 8 | 9 | @Override 10 | protected void onCreate(Bundle savedInstanceState) { 11 | super.onCreate(savedInstanceState); 12 | setContentView(R.layout.activity_main); 13 | } 14 | } -------------------------------------------------------------------------------- /samples/java-android-app/src/main/java/com/segment/analytics/javaandroidapp/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.javaandroidapp; 2 | 3 | import android.app.Application; 4 | 5 | import com.segment.analytics.kotlin.android.AndroidAnalyticsKt; 6 | import com.segment.analytics.kotlin.core.Analytics; 7 | 8 | import kotlin.Unit; 9 | 10 | public class MainApplication extends Application { 11 | private Analytics mainAnalytics; 12 | 13 | @Override 14 | public void onCreate() { 15 | super.onCreate(); 16 | mainAnalytics = AndroidAnalyticsKt.Analytics(BuildConfig.SEGMENT_WRITE_KEY, getApplicationContext(), configuration -> { 17 | configuration.setFlushAt(1); 18 | return Unit.INSTANCE; 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-kotlin/c2ac4eab1fffec304deeec8cbfe1693cd1a2ec94/samples/java-android-app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-kotlin/c2ac4eab1fffec304deeec8cbfe1693cd1a2ec94/samples/java-android-app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-kotlin/c2ac4eab1fffec304deeec8cbfe1693cd1a2ec94/samples/java-android-app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-kotlin/c2ac4eab1fffec304deeec8cbfe1693cd1a2ec94/samples/java-android-app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-kotlin/c2ac4eab1fffec304deeec8cbfe1693cd1a2ec94/samples/java-android-app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-kotlin/c2ac4eab1fffec304deeec8cbfe1693cd1a2ec94/samples/java-android-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-kotlin/c2ac4eab1fffec304deeec8cbfe1693cd1a2ec94/samples/java-android-app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-kotlin/c2ac4eab1fffec304deeec8cbfe1693cd1a2ec94/samples/java-android-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-kotlin/c2ac4eab1fffec304deeec8cbfe1693cd1a2ec94/samples/java-android-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/segmentio/analytics-kotlin/c2ac4eab1fffec304deeec8cbfe1693cd1a2ec94/samples/java-android-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Java Android App 3 | -------------------------------------------------------------------------------- /samples/java-android-app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /samples/java-android-app/src/test/java/com/segment/analytics/javaandroidapp/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.javaandroidapp; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /samples/kotlin-android-app-destinations/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /samples/kotlin-android-app-destinations/README.md: -------------------------------------------------------------------------------- 1 | # Sample App 2 | This is a sample android app that uses the `analytics-kotlin` library and the new `Plugins` concepts. It is meant to be simplistic, and easy to understand all the while showcasing the power of the analytics-kotlin library 3 | 4 | ## Plugins 5 | - [Amplitude Session](https://github.com/segment-integrations/analytics-kotlin-amplitude) 6 | Plugin to enable users to enrich data for the Amplitude Actions destination 7 | 8 | - [Appcues](https://github.com/appcues/segment-appcues-android) 9 | Plugin to enable users to send data to the device-mode Appcues destination 10 | 11 | - [AppsFlyer](https://github.com/segment-integrations/analytics-kotlin-appsflyer) 12 | Plugin to enable users to send data to the device-mode AppsFlyer destination 13 | 14 | - [Comscore](https://github.com/segment-integrations/analytics-kotlin-comscore) 15 | Plugin to enable users to send data to the device-mode Comscore destination 16 | 17 | - [Firebase](https://github.com/segment-integrations/analytics-kotlin-firebase) 18 | Plugin to enable users to send data to the device-mode Firebase destination 19 | 20 | - [Intercom](https://github.com/segment-integrations/analytics-kotlin-intercom) 21 | Plugin to enable users to send data to the device-mode Intercom destination 22 | 23 | - [Mixpanel](https://github.com/segment-integrations/analytics-kotlin-mixpanel) 24 | Plugin to enable users to send data to the device-mode Mixpanel destination 25 | 26 | - [Webhook Plugin](src/main/java/com/segment/analytics/kotlin/destinations/plugins/WebhookPlugin.kt) 27 | An after plugin that allows you to send the event from the analytics timeline to a webhook of your choice. Ideal for debugging payloads in an internal network. 28 | -------------------------------------------------------------------------------- /samples/kotlin-android-app-destinations/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'org.jetbrains.kotlin.plugin.serialization' 5 | } 6 | 7 | android { 8 | compileSdkVersion 33 9 | 10 | defaultConfig { 11 | multiDexEnabled true 12 | applicationId "com.segment.analytics.kotlin.destinations" 13 | minSdkVersion 21 14 | targetSdkVersion 33 15 | versionCode 3 16 | versionName "2.0" 17 | 18 | buildConfigField "String", "SEGMENT_WRITE_KEY", "\"${writeKey}\"" 19 | 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | coreLibraryDesugaringEnabled true 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = '1.8' 36 | } 37 | } 38 | 39 | dependencies { 40 | coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' 41 | 42 | implementation 'com.segment.analytics.kotlin:android:1.6.2' 43 | implementation 'androidx.multidex:multidex:2.0.1' 44 | 45 | implementation 'androidx.core:core-ktx:1.7.0' 46 | implementation 'androidx.appcompat:appcompat:1.4.0' 47 | implementation 'com.google.android.material:material:1.4.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.1.2' 49 | 50 | implementation 'androidx.lifecycle:lifecycle-process:2.4.0' 51 | implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0' 52 | } 53 | 54 | // Supported destinations 55 | dependencies { 56 | implementation 'com.segment.analytics.kotlin.destinations:amplitude:1.5.1' 57 | implementation 'com.segment.analytics.kotlin.destinations:appsflyer:1.5.1' 58 | implementation 'com.segment.analytics.kotlin.destinations:firebase:1.5.2' 59 | implementation 'com.segment.analytics.kotlin.destinations:mixpanel:1.5.2' 60 | implementation 'com.segment.analytics.kotlin.destinations:intercom:1.5.0' 61 | implementation 'com.segment.analytics.kotlin.destinations:comscore:1.5.0' 62 | } 63 | 64 | // Test Dependencies 65 | dependencies { 66 | testImplementation 'junit:junit:4.13.2' 67 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 68 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 69 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0' 70 | 71 | testImplementation 'io.mockk:mockk:1.10.6' 72 | testImplementation(platform("org.junit:junit-bom:5.7.2")) 73 | testImplementation("org.junit.jupiter:junit-jupiter") 74 | 75 | // Add Roboelectric dependencies. 76 | testImplementation 'org.robolectric:robolectric:4.7.3' 77 | testImplementation 'androidx.test:core:1.4.0' 78 | 79 | // Add JUnit4 legacy dependencies. 80 | testImplementation 'junit:junit:4.13.2' 81 | testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.7.2' 82 | 83 | // For JSON Object testing 84 | testImplementation 'org.json:json:20200518' 85 | testImplementation 'org.skyscreamer:jsonassert:1.5.0' 86 | } 87 | 88 | apply from: rootProject.file('gradle/codecov.gradle') -------------------------------------------------------------------------------- /samples/kotlin-android-app-destinations/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "896388530950", 4 | "project_id": "codyfire-aec37", 5 | "storage_bucket": "codyfire-aec37.appspot.com" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:896388530950:android:05388b0468bcc4fb9b1487", 11 | "android_client_info": { 12 | "package_name": "com.segment.analytics.destinations" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "896388530950-athmfbk281ot242c49jg8l50phar0g09.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyDrjBK2AfgX1ZL63ypw5vGUSfp1vBOx970" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "896388530950-athmfbk281ot242c49jg8l50phar0g09.apps.googleusercontent.com", 31 | "client_type": 3 32 | }, 33 | { 34 | "client_id": "896388530950-bevk6oulirofbnc87iahub4vth7salm2.apps.googleusercontent.com", 35 | "client_type": 2, 36 | "ios_info": { 37 | "bundle_id": "com.segment.DestinationsExample" 38 | } 39 | } 40 | ] 41 | } 42 | } 43 | } 44 | ], 45 | "configuration_version": "1" 46 | } -------------------------------------------------------------------------------- /samples/kotlin-android-app-destinations/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /samples/kotlin-android-app-destinations/src/androidTest/java/com/segment/analytics/next/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.next 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.segment.analytics.next", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /samples/kotlin-android-app-destinations/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 26 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /samples/kotlin-android-app-destinations/src/main/java/com/segment/analytics/kotlin/destinations/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.segment.analytics.kotlin.destinations 2 | 3 | import android.os.Bundle 4 | import android.widget.Button 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.fragment.app.Fragment 7 | import androidx.fragment.app.FragmentManager 8 | import androidx.fragment.app.FragmentTransaction 9 | import com.segment.analytics.kotlin.core.EventType 10 | 11 | 12 | class MainActivity : AppCompatActivity() { 13 | 14 | val analytics = MainApplication.analytics 15 | val trackFragment = EventFragment(EventType.Track, analytics) 16 | val identifyFragment = EventFragment(EventType.Identify, analytics) 17 | val screenFragment = EventFragment(EventType.Screen, analytics) 18 | val groupFragment = EventFragment(EventType.Group, analytics) 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_sample) 23 | 24 | findViewById