From bff933b5dc069b89fde02375aafbfa7644365b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=BA=C5=A1=20Tomlein?= Date: Wed, 26 Jan 2022 10:36:23 +0100 Subject: [PATCH 1/3] Initial release (close #1) PR #2 --- .github/workflows/build.yml | 156 +++++ .github/workflows/publish.yml | 26 + .gitignore | 29 + .metadata | 10 + .vscode/launch.json | 14 + CHANGELOG.md | 3 + CONTRIBUTING.md | 80 +++ LICENSE | 202 +++++++ README.md | 245 +++++++- analysis_options.yaml | 4 + android/.gitignore | 8 + android/build.gradle | 53 ++ android/settings.gradle | 1 + android/src/main/AndroidManifest.xml | 3 + .../SnowplowTrackerController.kt | 116 ++++ .../snowplow_tracker/SnowplowTrackerPlugin.kt | 143 +++++ .../snowplow_tracker/TrackerVersion.kt | 16 + .../configurations/GdprConfigurationReader.kt | 34 ++ .../NetworkConfigurationReader.kt | 33 ++ .../SubjectConfigurationReader.kt | 62 ++ .../TrackerConfigurationReader.kt | 68 +++ .../readers/events/ConsentDocumentReader.kt | 30 + .../readers/events/ConsentGrantedReader.kt | 37 ++ .../readers/events/ConsentWithdrawnReader.kt | 37 ++ .../readers/events/ScreenViewReader.kt | 38 ++ .../events/SelfDescribingJsonReader.kt | 28 + .../readers/events/StructuredReader.kt | 32 + .../readers/events/TimingReader.kt | 29 + .../messages/CreateTrackerMessageReader.kt | 36 ++ .../readers/messages/EventMessageReader.kt | 63 ++ .../messages/GetParameterMessageReader.kt | 16 + .../messages/SetUserIdMessageReader.kt | 19 + dartdoc_options.yaml | 23 + doc/01-getting-started.md | 69 +++ doc/02-configuration.md | 87 +++ doc/03-tracking-events.md | 179 ++++++ doc/04-adding-data.md | 54 ++ doc/05-sessions.md | 20 + example/.gitignore | 46 ++ example/.metadata | 10 + example/README.md | 3 + example/analysis_options.yaml | 29 + example/android/.gitignore | 13 + example/android/app/build.gradle | 69 +++ .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 35 ++ .../snowplow_tracker_example/MainActivity.kt | 6 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + example/android/build.gradle | 31 + example/android/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.properties | 6 + example/android/settings.gradle | 11 + .../integration_test/configuration_test.dart | 145 +++++ example/integration_test/events_test.dart | 189 ++++++ example/integration_test/helpers.dart | 52 ++ example/integration_test/session_test.dart | 147 +++++ example/ios/.gitignore | 34 ++ example/ios/Flutter/AppFrameworkInfo.plist | 26 + example/ios/Flutter/Debug.xcconfig | 2 + example/ios/Flutter/Release.xcconfig | 2 + example/ios/Podfile | 41 ++ example/ios/Podfile.lock | 41 ++ example/ios/Runner.xcodeproj/project.pbxproj | 554 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + example/ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 ++++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 ++ example/ios/Runner/Base.lproj/Main.storyboard | 29 + example/ios/Runner/Info.plist | 47 ++ example/ios/Runner/Runner-Bridging-Header.h | 1 + example/lib/main.dart | 224 +++++++ example/pubspec.lock | 279 +++++++++ example/pubspec.yaml | 88 +++ example/test/.gitkeep | 0 example/test_driver/integration_test.dart | 14 + example/tool/e2e_tests.sh | 8 + example/tool/iglu.json | 20 + example/tool/micro.conf | 231 ++++++++ example/web/favicon.png | Bin 0 -> 917 bytes example/web/icons/Icon-192.png | Bin 0 -> 5292 bytes example/web/icons/Icon-512.png | Bin 0 -> 8252 bytes example/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes example/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes example/web/index.html | 107 ++++ example/web/manifest.json | 35 ++ example/web/sp.js | 8 + ios/.gitignore | 38 ++ ios/Assets/.gitkeep | 0 ios/Classes/SnowplowTrackerController.swift | 91 +++ ios/Classes/SnowplowTrackerPlugin.h | 4 + ios/Classes/SnowplowTrackerPlugin.m | 15 + ios/Classes/SwiftSnowplowTrackerPlugin.swift | 151 +++++ ios/Classes/TrackerVersion.swift | 16 + .../GdprConfigurationReader.swift | 48 ++ .../NetworkConfigurationReader.swift | 28 + .../SubjectConfigurationReader.swift | 63 ++ .../TrackerConfigurationReader.swift | 57 ++ .../events/ConsentDocumentReader.swift | 29 + .../readers/events/ConsentGrantedReader.swift | 35 ++ .../events/ConsentWithdrawnReader.swift | 38 ++ .../readers/events/ScreenViewReader.swift | 42 ++ .../events/SelfDescribingJsonReader.swift | 33 ++ .../readers/events/StructuredReader.swift | 31 + ios/Classes/readers/events/TimingReader.swift | 28 + .../messages/CreateTrackerMessageReader.swift | 21 + .../readers/messages/EventMessageReader.swift | 30 + .../messages/GetParameterMessageReader.swift | 16 + .../messages/SetUserIdMessageReader.swift | 17 + .../TrackConsentGrantedMessageReader.swift | 23 + .../TrackConsentWithdrawnMessageReader.swift | 23 + .../TrackScreenViewMessageReader.swift | 23 + .../TrackSelfDescribingMesssageReader.swift | 26 + .../TrackStructuredMessageReader.swift | 23 + .../messages/TrackTimingMessageReader.swift | 23 + ios/snowplow_tracker.podspec | 24 + lib/configurations/configuration.dart | 56 ++ lib/configurations/gdpr_configuration.dart | 47 ++ lib/configurations/network_configuration.dart | 37 ++ lib/configurations/subject_configuration.dart | 132 +++++ lib/configurations/tracker_configuration.dart | 105 ++++ lib/events/consent_granted.dart | 62 ++ lib/events/consent_withdrawn.dart | 63 ++ lib/events/event.dart | 19 + lib/events/screen_view.dart | 71 +++ lib/events/self_describing.dart | 51 ++ lib/events/structured.dart | 68 +++ lib/events/timing.dart | 55 ++ lib/js/sp_session_context_plugin.js | 115 ++++ lib/snowplow.dart | 92 +++ lib/snowplow_tracker.dart | 25 + lib/snowplow_tracker_plugin_web.dart | 142 +++++ .../configurations/configuration_reader.dart | 45 ++ .../gdpr_configuration_reader.dart | 21 + .../network_configuration_reader.dart | 27 + .../subject_configuration_reader.dart | 25 + .../tracker_configuration_reader.dart | 48 ++ .../events/consent_granted_reader.dart | 39 ++ .../events/consent_withdrawn_reader.dart | 39 ++ .../web/readers/events/contexts_reader.dart | 22 + lib/src/web/readers/events/event_reader.dart | 15 + .../readers/events/screen_view_reader.dart | 51 ++ .../events/self_describing_reader.dart | 32 + .../web/readers/events/structured_reader.dart | 41 ++ lib/src/web/readers/events/timing_reader.dart | 37 ++ .../messages/event_message_reader.dart | 127 ++++ .../messages/set_user_id_message_reader.dart | 22 + lib/src/web/snowplow_tracker_controller.dart | 70 +++ lib/src/web/sp.dart | 39 ++ lib/tracker.dart | 57 ++ pubspec.yaml | 38 ++ test/snowplow_test.dart | 226 +++++++ test/tracker_test.dart | 70 +++ 187 files changed, 8325 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 .vscode/launch.json create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 analysis_options.yaml create mode 100644 android/.gitignore create mode 100644 android/build.gradle create mode 100644 android/settings.gradle create mode 100644 android/src/main/AndroidManifest.xml create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/SnowplowTrackerController.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/SnowplowTrackerPlugin.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/TrackerVersion.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/GdprConfigurationReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/NetworkConfigurationReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/SubjectConfigurationReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/TrackerConfigurationReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentDocumentReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentGrantedReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentWithdrawnReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ScreenViewReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/SelfDescribingJsonReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/StructuredReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/TimingReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/CreateTrackerMessageReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/EventMessageReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/GetParameterMessageReader.kt create mode 100644 android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/SetUserIdMessageReader.kt create mode 100644 dartdoc_options.yaml create mode 100644 doc/01-getting-started.md create mode 100644 doc/02-configuration.md create mode 100644 doc/03-tracking-events.md create mode 100644 doc/04-adding-data.md create mode 100644 doc/05-sessions.md create mode 100644 example/.gitignore create mode 100644 example/.metadata create mode 100644 example/README.md create mode 100644 example/analysis_options.yaml create mode 100644 example/android/.gitignore create mode 100644 example/android/app/build.gradle create mode 100644 example/android/app/src/debug/AndroidManifest.xml create mode 100644 example/android/app/src/main/AndroidManifest.xml create mode 100644 example/android/app/src/main/kotlin/com/snowplowanalytics/snowplow_tracker_example/MainActivity.kt create mode 100644 example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 example/android/app/src/main/res/values-night/styles.xml create mode 100644 example/android/app/src/main/res/values/styles.xml create mode 100644 example/android/app/src/profile/AndroidManifest.xml create mode 100644 example/android/build.gradle create mode 100644 example/android/gradle.properties create mode 100644 example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 example/android/settings.gradle create mode 100644 example/integration_test/configuration_test.dart create mode 100644 example/integration_test/events_test.dart create mode 100644 example/integration_test/helpers.dart create mode 100644 example/integration_test/session_test.dart create mode 100644 example/ios/.gitignore create mode 100644 example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 example/ios/Flutter/Debug.xcconfig create mode 100644 example/ios/Flutter/Release.xcconfig create mode 100644 example/ios/Podfile create mode 100644 example/ios/Podfile.lock create mode 100644 example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 example/ios/Runner/AppDelegate.swift create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 example/ios/Runner/Info.plist create mode 100644 example/ios/Runner/Runner-Bridging-Header.h create mode 100644 example/lib/main.dart create mode 100644 example/pubspec.lock create mode 100644 example/pubspec.yaml create mode 100644 example/test/.gitkeep create mode 100644 example/test_driver/integration_test.dart create mode 100755 example/tool/e2e_tests.sh create mode 100644 example/tool/iglu.json create mode 100644 example/tool/micro.conf create mode 100644 example/web/favicon.png create mode 100644 example/web/icons/Icon-192.png create mode 100644 example/web/icons/Icon-512.png create mode 100644 example/web/icons/Icon-maskable-192.png create mode 100644 example/web/icons/Icon-maskable-512.png create mode 100644 example/web/index.html create mode 100644 example/web/manifest.json create mode 100644 example/web/sp.js create mode 100644 ios/.gitignore create mode 100644 ios/Assets/.gitkeep create mode 100644 ios/Classes/SnowplowTrackerController.swift create mode 100644 ios/Classes/SnowplowTrackerPlugin.h create mode 100644 ios/Classes/SnowplowTrackerPlugin.m create mode 100644 ios/Classes/SwiftSnowplowTrackerPlugin.swift create mode 100644 ios/Classes/TrackerVersion.swift create mode 100644 ios/Classes/readers/configurations/GdprConfigurationReader.swift create mode 100644 ios/Classes/readers/configurations/NetworkConfigurationReader.swift create mode 100644 ios/Classes/readers/configurations/SubjectConfigurationReader.swift create mode 100644 ios/Classes/readers/configurations/TrackerConfigurationReader.swift create mode 100644 ios/Classes/readers/events/ConsentDocumentReader.swift create mode 100644 ios/Classes/readers/events/ConsentGrantedReader.swift create mode 100644 ios/Classes/readers/events/ConsentWithdrawnReader.swift create mode 100644 ios/Classes/readers/events/ScreenViewReader.swift create mode 100644 ios/Classes/readers/events/SelfDescribingJsonReader.swift create mode 100644 ios/Classes/readers/events/StructuredReader.swift create mode 100644 ios/Classes/readers/events/TimingReader.swift create mode 100644 ios/Classes/readers/messages/CreateTrackerMessageReader.swift create mode 100644 ios/Classes/readers/messages/EventMessageReader.swift create mode 100644 ios/Classes/readers/messages/GetParameterMessageReader.swift create mode 100644 ios/Classes/readers/messages/SetUserIdMessageReader.swift create mode 100644 ios/Classes/readers/messages/TrackConsentGrantedMessageReader.swift create mode 100644 ios/Classes/readers/messages/TrackConsentWithdrawnMessageReader.swift create mode 100644 ios/Classes/readers/messages/TrackScreenViewMessageReader.swift create mode 100644 ios/Classes/readers/messages/TrackSelfDescribingMesssageReader.swift create mode 100644 ios/Classes/readers/messages/TrackStructuredMessageReader.swift create mode 100644 ios/Classes/readers/messages/TrackTimingMessageReader.swift create mode 100644 ios/snowplow_tracker.podspec create mode 100644 lib/configurations/configuration.dart create mode 100644 lib/configurations/gdpr_configuration.dart create mode 100644 lib/configurations/network_configuration.dart create mode 100644 lib/configurations/subject_configuration.dart create mode 100644 lib/configurations/tracker_configuration.dart create mode 100644 lib/events/consent_granted.dart create mode 100644 lib/events/consent_withdrawn.dart create mode 100644 lib/events/event.dart create mode 100644 lib/events/screen_view.dart create mode 100644 lib/events/self_describing.dart create mode 100644 lib/events/structured.dart create mode 100644 lib/events/timing.dart create mode 100644 lib/js/sp_session_context_plugin.js create mode 100644 lib/snowplow.dart create mode 100644 lib/snowplow_tracker.dart create mode 100644 lib/snowplow_tracker_plugin_web.dart create mode 100644 lib/src/web/readers/configurations/configuration_reader.dart create mode 100644 lib/src/web/readers/configurations/gdpr_configuration_reader.dart create mode 100644 lib/src/web/readers/configurations/network_configuration_reader.dart create mode 100644 lib/src/web/readers/configurations/subject_configuration_reader.dart create mode 100644 lib/src/web/readers/configurations/tracker_configuration_reader.dart create mode 100644 lib/src/web/readers/events/consent_granted_reader.dart create mode 100644 lib/src/web/readers/events/consent_withdrawn_reader.dart create mode 100644 lib/src/web/readers/events/contexts_reader.dart create mode 100644 lib/src/web/readers/events/event_reader.dart create mode 100644 lib/src/web/readers/events/screen_view_reader.dart create mode 100644 lib/src/web/readers/events/self_describing_reader.dart create mode 100644 lib/src/web/readers/events/structured_reader.dart create mode 100644 lib/src/web/readers/events/timing_reader.dart create mode 100644 lib/src/web/readers/messages/event_message_reader.dart create mode 100644 lib/src/web/readers/messages/set_user_id_message_reader.dart create mode 100644 lib/src/web/snowplow_tracker_controller.dart create mode 100644 lib/src/web/sp.dart create mode 100644 lib/tracker.dart create mode 100644 pubspec.yaml create mode 100644 test/snowplow_test.dart create mode 100644 test/tracker_test.dart diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..3203ec5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,156 @@ +name: Build and Test +on: [push, pull_request] +jobs: + build: + name: Run unit tests + runs-on: ubuntu-latest + strategy: + matrix: + flutter: ['2.8.0'] + steps: + - uses: actions/checkout@v2 + - uses: subosito/flutter-action@v1 + with: + flutter-version: ${{ matrix.flutter }} + channel: 'stable' + - run: flutter pub get + - run: flutter test + + analyze: + name: Analyze + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: subosito/flutter-action@v1 + with: + flutter-version: '2.8.0' + channel: 'stable' + - run: flutter pub get + - run: flutter analyze + - run: flutter format -n --set-exit-if-changed . + + build-android: + name: Integration tests on Android + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v1 + with: + java-version: "11.x" + + # -- Micro -- + - name: Cache Micro + id: cache-micro + uses: actions/cache@v2 + with: + path: micro.jar + key: ${{ runner.os }}-micro + + - name: Get micro + if: steps.cache-micro.outputs.cache-hit != 'true' + run: curl -o micro.jar -L https://github.com/snowplow-incubator/snowplow-micro/releases/download/micro-1.1.2/snowplow-micro-1.1.2.jar + + - name: Run Micro in background + run: java -jar micro.jar --collector-config example/tool/micro.conf --iglu example/tool/iglu.json & + + - name: Wait on Micro endpoint + timeout-minutes: 2 + run: while ! nc -z '0.0.0.0' 9090; do sleep 1; done + + # -- Integration tests -- + - uses: subosito/flutter-action@v1 + with: + flutter-version: '2.8.0' + channel: 'stable' + + - name: Run Flutter Driver tests + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 30 + target: default + arch: x86_64 + profile: Nexus 6 + working-directory: example + script: ./tool/e2e_tests.sh http://10.0.2.2:9090 + + build-ios: + name: Integration tests on iOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + + # -- Micro -- + - name: Cache Micro + id: cache-micro + uses: actions/cache@v2 + with: + path: micro.jar + key: ${{ runner.os }}-micro + + - name: Get micro + if: steps.cache-micro.outputs.cache-hit != 'true' + run: curl -o micro.jar -L https://github.com/snowplow-incubator/snowplow-micro/releases/download/micro-1.1.2/snowplow-micro-1.1.2.jar + + - name: Run Micro in background + run: java -jar micro.jar --collector-config example/tool/micro.conf --iglu example/tool/iglu.json & + + - name: Wait on Micro endpoint + timeout-minutes: 2 + run: while ! nc -z '0.0.0.0' 9090; do sleep 1; done + + # -- Integration tests -- + - name: "Start Simulator" + uses: futureware-tech/simulator-action@v1 + with: + model: iPhone 13 + erase_before_boot: true + shutdown_after_job: true + + - uses: subosito/flutter-action@v1 + with: + flutter-version: '2.8.0' + channel: 'stable' + + - run: "flutter clean" + + - name: Run tests + working-directory: example + run: ./tool/e2e_tests.sh http://0.0.0.0:9090 + + build-web: + name: Integration tests on Web + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v2 + + # -- Micro -- + - name: Cache Micro + id: cache-micro + uses: actions/cache@v2 + with: + path: micro.jar + key: ${{ runner.os }}-micro + + - name: Get micro + if: steps.cache-micro.outputs.cache-hit != 'true' + run: curl -o micro.jar -L https://github.com/snowplow-incubator/snowplow-micro/releases/download/micro-1.1.2/snowplow-micro-1.1.2.jar + + - name: Run Micro in background + run: java -jar micro.jar --collector-config example/tool/micro.conf --iglu example/tool/iglu.json & + + - name: Wait on Micro endpoint + timeout-minutes: 2 + run: while ! nc -z '0.0.0.0' 9090; do sleep 1; done + + # -- Integration tests -- + - uses: subosito/flutter-action@v1 + with: + flutter-version: '2.8.0' + channel: 'stable' + + - run: chromedriver --port=4444 & + + - name: Run tests + working-directory: example + run: ./tool/e2e_tests.sh http://0.0.0.0:9090 "-d web-server" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..f747bad --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,26 @@ +name: Publish package to pub.dev +on: + push: + branches: + - master +jobs: + build: + runs-on: ubuntu-latest + container: + image: google/dart:latest + steps: + - uses: actions/checkout@v1 + - name: Setup credentials + run: | + mkdir -p ~/.pub-cache + cat < ~/.pub-cache/credentials.json + { + "accessToken":"${{ secrets.ACCESS_TOKEN }}", + "refreshToken":"${{ secrets.REFRESH_TOKEN }}", + "tokenEndpoint":"https://accounts.google.com/o/oauth2/token", + "scopes": [ "openid", "https://www.googleapis.com/auth/userinfo.email" ], + "expiration": 1643122520446 + } + EOF + - name: Publish package + run: pub publish -f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9be145f --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..8c15ad7 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + channel: stable + +project_type: plugin diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..44713e7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Example App", + "program": "example/lib/main.dart", + "request": "launch", + "type": "dart", + "args":[ + "--dart-define=ENDPOINT=http://192.168.100.127:9090" + ] + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..061bf69 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0-dev.1 + +* Initial pre-release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..df02bc2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,80 @@ +# Contributing + +The Snowplow Flutter Tracker is maintained by the Engineering team at Snowplow Analytics. We welcome suggestions for improvements and bug fixes to all Snowplow Trackers. + +We are extremely grateful for all contributions we receive, whether that is reporting an issue or a change to the code which can be made in the form of a pull request. + +For support requests, please use our community support Discourse forum: https://discourse.snowplowanalytics.com/. + +## Setting up an Environment + +Instructions on how to build and run tests are available in the [README.md](README.md). The README will also list any requirements that you will need to install first before being able to build and run the tests. + +You should ensure you are comfortable building and testing the existing release before adding new functionality or fixing issues. + +## Issues + +### Creating an issue + +The project contains an issue template which should help guiding you through the process. However, please keep in mind that support requests should go to our Discourse forum: https://discourse.snowplowanalytics.com/ and not GitHub issues. + +It's also a good idea to log an issue before starting to work on a pull request to discuss it with the maintainers. A pull request is just one solution to a problem and it is often a good idea to talk about the problem with the maintainers first. + +### Working on an issue + +If you see an issue you would like to work on, please let us know in the issue! That will help us in terms of scheduling and +not doubling the amount of work. + +If you don't know where to start contributing, you can look at +[the issues labeled `good first issue`](https://github.com/snowplow-incubator/snowplow-flutter-tracker/labels/category%3Agood_first_issue). + +## Pull requests + +These are a few guidelines to keep in mind when opening pull requests. + +### Guidelines + +Please supply a good PR description. These are very helpful and help the maintainers to understand _why_ the change has been made, not just _what_ changes have been made. + +Please try and keep your PR to a single feature of fix. This might mean breaking up a feature into multiple PRs but this makes it easier for the maintainers to review and also reduces the risk in each change. + +Please review your own PR as you would do it you were a reviewer first. This is a great way to spot any mistakes you made when writing the change. Additionally, ensure your code compiles and all tests pass. + +### Commit hygiene + +We keep a strict 1-to-1 correspondance between commits and issues, as such our commit messages are formatted in the following +fashion: + +`Issue Description (closes #1234)` + +for example: + +`Fix Issue with Tracker (closes #1234)` + +### Writing tests + +Whenever necessary, it's good practice to add the corresponding tests to whichever feature you are working on. +Any non-trivial PR must have tests and will not be accepted without them. + +### Feedback cycle + +Reviews should happen fairly quickly during weekdays. +If you feel your pull request has been forgotten, please ping one or more maintainers in the pull request. + +### Getting your pull request merged + +If your pull request is fairly chunky, there might be a non-trivial delay between the moment the pull request is approved and the moment it gets merged. This is because your pull request will have been scheduled for a specific milestone which might or might not be actively worked on by a maintainer at the moment. + +### Contributor license agreement + +We require outside contributors to sign a Contributor license agreement (or CLA) before we can merge their pull requests. +You can find more information on the topic in [the dedicated wiki page](https://github.com/snowplow/snowplow/wiki/CLA). +The @snowplowcla bot will guide you through the process. + +## Getting in touch + +### Community support requests + +Please do not log an issue if you are asking for support, all of our community support requests go through our Discourse forum: https://discourse.snowplowanalytics.com/. + +Posting your problem there ensures more people will see it and you should get support faster than creating a new issue on GitHub. Please do create a new issue on GitHub if you think you've found a bug though! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e0977a7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 Snowplow Analytics Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index de9b1b4..901c167 100644 --- a/README.md +++ b/README.md @@ -1 +1,244 @@ -# snowplow_flutter_tracker +# Flutter Analytics for Snowplow + +[![early-release]][tracker-classificiation] +[![Build Status][gh-actions-image]][gh-actions] +[![Release][release-image]][releases] +[![License][license-image]][license] + +Snowplow is a scalable open-source platform for rich, high quality, low-latency data collection. It is designed to collect high quality, complete behavioral data for enterprise business. + +**To find out more, please check out the [Snowplow website][website] and our [documentation][docs].** + +## Snowplow Flutter Tracker Overview + +The Snowplow Flutter Tracker allows you to add analytics to your Flutter apps when using a [Snowplow][snowplow] pipeline. + +With this tracker you can collect granular event-level data as your users interact with your Flutter applications. +It is build on top of Snowplow's native [iOS](https://github.com/snowplow/snowplow-objc-tracker) and [Android](https://github.com/snowplow/snowplow-android-tracker) and [web](https://github.com/snowplow/snowplow-javascript-tracker) trackers, in order to support the full range of out-of-the-box Snowplow events and tracking capabilities. + +**Technical documentation can be found for each tracker in our [Documentation][flutter-docs].** + +## Features + +| Feature | Android | iOS | Web | +|---|---|---|---| +| Manual tracking of events: screen views, self-describing, structured, timing, consent granted and withdrawal | ✔ | ✔ | ✔ | +| Adding custom context entities to events | ✔ | ✔ | ✔ | +| Support for multiple trackers | ✔ | ✔ | ✔ | +| Configurable subject properties | ✔ | ✔ | partly | +| Session context entity added to events | ✔ | ✔ | ✔ | +| Geo-location context entity | ✔ | ✔ | ✔ | +| Mobile platform context entity | ✔ | ✔ | | +| Web page context entity | | | ✔ | +| Configurable GDPR context entity | ✔ | ✔ | ✔ | + +## Quick Start + +### Installation + +Add the Snowplow tracker as a dependency to your Flutter application: + +```bash +flutter pub add snowplow_tracker +``` + +This will add a line with the dependency like this to your `pubspec.yaml`: + +```yml +dependencies: + snowplow_tracker: ^0.1.0 +``` + +Import the package into your Dart code: + +```dart +import 'package:snowplow_tracker/snowplow_tracker.dart' +``` + +#### Installation on Web + +If using the tracker within a Flutter app for Web, you will also need to import the Snowplow JavaScript Tracker in your `index.html` file. Please load the JS tracker with the Snowplow tag as [described in the official documentation](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-trackers/javascript-tracker/javascript-tracker-v3/tracker-setup/loading/). Do not change the global function name `snowplow` that is used to access the tracker – the Flutter APIs assume that it remains the default as shown in documentation. + +Make sure to use JavaScript tracker version `3.2` or newer. You may also refer to the [example project](https://github.com/snowplow-incubator/snowplow-flutter-tracker/tree/main/example) in the Flutter tracker repository to see this in action. + +### Using the Tracker + +Instantiate a tracker using the `Snowplow.createTracker` function. +You may create the tracker in the `initState()` of your main widget. +The function takes two required arguments: `namespace` and `endpoint`. +Tracker namespace identifies the tracker instance; you may create multiple trackers with different namespaces. +The endpoint is the URI of the Snowplow collector to send the events to. +There are additional optional arguments to configure the tracker, please refer to the documentation for a complete specification. + +```dart +Tracker tracker = await Snowplow.createTracker( + namespace: 'ns1', + endpoint: 'http://...' +); +``` + +To track events, simply instantiate their respective types (e.g., `ScreenView`, `SelfDescribing`, `Structured`) and pass them to the `tracker.track` or `Snowplow.track` methods. +Please refer to the documentation for specification of event properties. + +```dart +// Tracking a screen view event +tracker.track(ScreenView( + id: '2c295365-eae9-4243-a3ee-5c4b7baccc8f', + name: 'home', + type: 'full', + transitionType: 'none')); + +// Tracking a self-describing event +tracker.track(SelfDescribing( + schema: 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1', + data: {'targetUrl': 'http://a-target-url.com'} +)); + +// Tracking a structured event +tracker.track(Structured( + category: 'shop', + action: 'add-to-basket', + label: 'Add To Basket', + property: 'pcs', + value: 2.00, +)); + +// Tracking an event using the Snowplow interface and tracker namespace +Snowplow.track( + Structured(category: 'shop', action: 'add-to-basket'), + tracker: 'ns1' // namespace of initialized tracker +); + +// Adding context to an event +tracker.track( + Structured(category: 'shop', action: 'add-to-basket'), + contexts: [ + const SelfDescribing( + schema: 'iglu:org.schema/WebPage/jsonschema/1-0-0', + data: {'keywords': ['tester']} + ) + ]); +``` + +## Find Out More + +| Technical Docs | Setup Guide | +|-----------------------------------|-----------------------------| +| [![i1][techdocs-image]][techdocs] | [![i2][setup-image]][setup] | +| [Technical Docs][techdocs] | [Setup Guide][setup] | + +## Maintainers + +| Contributing | +|----------------------------------------------| +| [![i4][contributing-image]](CONTRIBUTING.md) | +| [Contributing](CONTRIBUTING.md) | + +### Maintainer Quick Start + +Assuming [Flutter SDK](https://docs.flutter.dev/get-started/install) is set up and [Snowplow Micro](https://github.com/snowplow-incubator/snowplow-micro) is running on your computer. + +#### Clone Repository + +```bash +git clone https://github.com/snowplow-incubator/snowplow-flutter-tracker.git +``` + +## Example App + +The tracker comes with a demo app that shows it in use. +It is a simple app with list of buttons for triggering different types of events. +The project is located in the `example` subfolder. + +Running the example app on Android/iOS: + +1. Change into the project folder and `cd example` +2. Run the integration tests (replace the Snowplow Micro URI with your IP address and set your iPhone or Android simulator name or remove to use default): + +```bash +flutter run --dart-define=ENDPOINT=http://192.168.100.127:9090 -d "iPhone 13 Pro" +``` + +To run the example app on Web: + +1. [Download ChromeDriver](https://chromedriver.chromium.org/downloads) and launch it using `chromedriver --port=4444` +2. Change into the project folder and `cd example` +3. Run the integration tests (replace the Snowplow Micro URI with your IP address): + +```bash +flutter run --dart-define=ENDPOINT=http://0.0.0.0:9090 -d Chrome +``` + +Alternatively, you may also run the integration tests from Visual Studio Code using the "Run Example App" target (update your IP address in launch.json). + +## Testing + +The tracker functionality is verified using unit and integration tests. +Unit tests test individual components of the tracker in isolation and do not make any external network requests. +Integration tests use a Snowplow Micro instance to verify end-to-end tracking of events. + +The unit tests are located in the `tests` subfolder in the root of the project. +Having installed the Flutter SDK, run the tests using `flutter test` (or run them directly from Visual Studio Code). + +The integration tests are located in the `example/integration_test` subfolder. +These tests make use of the example app to provide end-to-end testing of the tracker. + +Running the integration tests on Android/iOS: + +1. Change into the project folder and `cd example` +2. Run the integration tests (replace the Snowplow Micro URI with your IP address and set your iPhone or Android simulator name or remove to use default): + +```bash +flutter test integration_test --dart-define=ENDPOINT=http://192.168.100.127:9090 -d "iPhone 13 Pro" +``` + +Alternatively, you may also run the integration tests directly from Visual Studio Code. + +To run the integration tests on Web: + +1. [Download ChromeDriver](https://chromedriver.chromium.org/downloads) and launch it using `chromedriver --port=4444` +2. Change into the project folder and `cd example` +3. Run the integration tests (replace the Snowplow Micro URI with your IP address): + +```bash +./tool/e2e_tests.sh http://0.0.0.0:9090 "-d web-server" +``` + +## Copyright and License + +The Snowplow Flutter Tracker is copyright 2022 Snowplow Analytics Ltd. + +Licensed under the **[Apache License, Version 2.0][license]** (the "License"); +you may not use this software except in compliance with the License. + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +[website]: https://snowplowanalytics.com +[snowplow]: https://github.com/snowplow/snowplow +[docs]: https://docs.snowplowanalytics.com/ +[flutter-docs]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/flutter-tracker/ + +[gh-actions]: https://github.com/snowplow-incubator/snowplow-flutter-tracker/actions/workflows/build.yml +[gh-actions-image]: https://github.com/snowplow-incubator/snowplow-flutter-tracker/actions/workflows/build.yml/badge.svg + +[license]: https://www.apache.org/licenses/LICENSE-2.0 +[license-image]: https://img.shields.io/badge/license-Apache--2-blue.svg?style=flat + +[release-image]: https://img.shields.io/pub/v/snowplow_tracker +[releases]: https://pub.dev/packages/snowplow_tracker + +[techdocs]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/flutter-tracker/ +[techdocs-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/techdocs.png +[setup]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/flutter-tracker/quick-start-guide +[setup-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/setup.png + +[api-docs]: https://snowplow.github.io/snowplow-flutter-tracker/ + +[contributing-image]: https://d3i6fms1cm1j0i.cloudfront.net/github/images/contributing.png + +[tracker-classificiation]: https://github.com/snowplow/snowplow/wiki/Tracker-Maintenance-Classification +[early-release]: https://img.shields.io/static/v1?style=flat&label=Snowplow&message=Early%20Release&color=014477&labelColor=9ba0aa&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEVMaXGXANeYANeXANZbAJmXANeUANSQAM+XANeMAMpaAJhZAJeZANiXANaXANaOAM2WANVnAKWXANZ9ALtmAKVaAJmXANZaAJlXAJZdAJxaAJlZAJdbAJlbAJmQAM+UANKZANhhAJ+EAL+BAL9oAKZnAKVjAKF1ALNBd8J1AAAAKHRSTlMAa1hWXyteBTQJIEwRgUh2JjJon21wcBgNfmc+JlOBQjwezWF2l5dXzkW3/wAAAHpJREFUeNokhQOCA1EAxTL85hi7dXv/E5YPCYBq5DeN4pcqV1XbtW/xTVMIMAZE0cBHEaZhBmIQwCFofeprPUHqjmD/+7peztd62dWQRkvrQayXkn01f/gWp2CrxfjY7rcZ5V7DEMDQgmEozFpZqLUYDsNwOqbnMLwPAJEwCopZxKttAAAAAElFTkSuQmCC diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..c6cbe56 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..f122870 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,53 @@ +group 'com.snowplowanalytics.snowplow_tracker' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.4.32' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 30 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 16 + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "com.snowplowanalytics:snowplow-android-tracker:3.+" +} diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..3388b89 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'snowplow_tracker' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f6414d8 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/SnowplowTrackerController.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/SnowplowTrackerController.kt new file mode 100644 index 0000000..78f61b6 --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/SnowplowTrackerController.kt @@ -0,0 +1,116 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker + +import android.content.Context + +import com.snowplowanalytics.snowplow.Snowplow; +import com.snowplowanalytics.snowplow.configuration.Configuration; +import com.snowplowanalytics.snowplow_tracker.readers.configurations.DefaultTrackerConfiguration +import com.snowplowanalytics.snowplow_tracker.readers.messages.* + +object SnowplowTrackerController { + + fun createTracker(messageReader: CreateTrackerMessageReader, context: Context) { + val controllers: MutableList = mutableListOf() + + val networkConfigReader = messageReader.networkConfig; + val networkConfiguration = networkConfigReader.toConfiguration() + + val trackerConfigReader = messageReader.trackerConfig + if (trackerConfigReader == null) { + controllers.add(DefaultTrackerConfiguration.toConfiguration(null, context)) + } else { + controllers.add(trackerConfigReader.toConfiguration(context)) + } + + val subjectConfigReader = messageReader.subjectConfig + subjectConfigReader?.let { controllers.add(it.toConfiguration()) } + + val gdprConfigReader = messageReader.gdprConfig + gdprConfigReader?.let { controllers.add(it.toConfiguration()) } + + Snowplow.createTracker( + context, + messageReader.namespace, + networkConfiguration, + *controllers.toTypedArray() + ) + } + + fun trackStructured(eventReader: EventMessageReader) { + val trackerController = Snowplow.getTracker(eventReader.tracker) + val structured = eventReader.toStructuredWithContexts() + + trackerController?.track(structured) + } + + fun trackSelfDescribing(eventReader: EventMessageReader) { + val trackerController = Snowplow.getTracker(eventReader.tracker) + val selfDescribing = eventReader.toSelfDescribingWithContexts() + + trackerController?.track(selfDescribing) + } + + fun trackScreenView(eventReader: EventMessageReader) { + val trackerController = Snowplow.getTracker(eventReader.tracker) + val screenView = eventReader.toScreenViewWithContexts() + + trackerController?.track(screenView) + } + + fun trackTiming(eventReader: EventMessageReader) { + val trackerController = Snowplow.getTracker(eventReader.tracker) + val timing = eventReader.toTimingWithContexts() + + trackerController?.track(timing) + } + + fun trackConsentGranted(eventReader: EventMessageReader) { + val trackerController = Snowplow.getTracker(eventReader.tracker) + val consentGranted = eventReader.toConsentGrantedWithContexts() + + trackerController?.track(consentGranted) + } + + fun trackConsentWithdrawn(eventReader: EventMessageReader) { + val trackerController = Snowplow.getTracker(eventReader.tracker) + val consentWithdrawn = eventReader.toConsentWithdrawnWithContexts() + + trackerController?.track(consentWithdrawn) + } + + fun setUserId(messageReader: SetUserIdMessageReader) { + val trackerController = Snowplow.getTracker(messageReader.tracker) + + trackerController?.subject?.userId = messageReader.userId + } + + fun getSessionUserId(messageReader: GetParameterMessageReader): String? { + val trackerController = Snowplow.getTracker(messageReader.tracker) + + return trackerController?.session?.userId + } + + fun getSessionId(messageReader: GetParameterMessageReader): String? { + val trackerController = Snowplow.getTracker(messageReader.tracker) + + return trackerController?.session?.sessionId + } + + fun getSessionIndex(messageReader: GetParameterMessageReader): Int? { + val trackerController = Snowplow.getTracker(messageReader.tracker) + + return trackerController?.session?.sessionIndex + } + +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/SnowplowTrackerPlugin.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/SnowplowTrackerPlugin.kt new file mode 100644 index 0000000..6f860d8 --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/SnowplowTrackerPlugin.kt @@ -0,0 +1,143 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker + +import androidx.annotation.NonNull + +import android.content.Context +import com.snowplowanalytics.snowplow_tracker.readers.messages.CreateTrackerMessageReader +import com.snowplowanalytics.snowplow_tracker.readers.messages.EventMessageReader +import com.snowplowanalytics.snowplow_tracker.readers.messages.GetParameterMessageReader +import com.snowplowanalytics.snowplow_tracker.readers.messages.SetUserIdMessageReader +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** SnowplowTrackerPlugin */ +class SnowplowTrackerPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + private var context: Context? = null + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "snowplow_tracker") + channel.setMethodCallHandler(this) + context = flutterPluginBinding.applicationContext + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + when (call.method) { + "createTracker" -> onCreateTracker(call, result) + "trackStructured" -> onTrackStructured(call, result) + "trackSelfDescribing" -> onTrackSelfDescribing(call, result) + "trackScreenView" -> onTrackScreenView(call, result) + "trackTiming" -> onTrackTiming(call, result) + "trackConsentGranted" -> onTrackConsentGranted(call, result) + "trackConsentWithdrawn" -> onTrackConsentWithdrawn(call, result) + "setUserId" -> onSetUserId(call, result) + "getSessionUserId" -> onGetSessionUserId(call, result) + "getSessionId" -> onGetSessionId(call, result) + "getSessionIndex" -> onGetSessionIndex(call, result) + else -> result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + context = null + } + + private fun onCreateTracker(call: MethodCall, result: MethodChannel.Result) { + context?.let { ctxt -> + (call.arguments as? Map)?.let { values -> + SnowplowTrackerController.createTracker(CreateTrackerMessageReader(values), ctxt) + } + } + result.success(null) + } + + private fun onTrackStructured(call: MethodCall, result: MethodChannel.Result) { + (call.arguments as? Map)?.let { + SnowplowTrackerController.trackStructured(EventMessageReader(it)) + } + result.success(null) + } + + private fun onTrackSelfDescribing(call: MethodCall, result: MethodChannel.Result) { + (call.arguments as? Map)?.let { + SnowplowTrackerController.trackSelfDescribing(EventMessageReader(it)) + } + result.success(null) + } + + private fun onTrackScreenView(call: MethodCall, result: MethodChannel.Result) { + (call.arguments as? Map)?.let { + SnowplowTrackerController.trackScreenView(EventMessageReader(it)) + } + result.success(null) + } + + private fun onTrackTiming(call: MethodCall, result: MethodChannel.Result) { + (call.arguments as? Map)?.let { + SnowplowTrackerController.trackTiming(EventMessageReader(it)) + } + result.success(null) + } + + private fun onTrackConsentGranted(call: MethodCall, result: MethodChannel.Result) { + (call.arguments as? Map)?.let { + SnowplowTrackerController.trackConsentGranted(EventMessageReader(it)) + } + result.success(null) + } + + private fun onTrackConsentWithdrawn(call: MethodCall, result: MethodChannel.Result) { + (call.arguments as? Map)?.let { + SnowplowTrackerController.trackConsentWithdrawn(EventMessageReader(it)) + } + result.success(null) + } + + private fun onSetUserId(call: MethodCall, result: MethodChannel.Result) { + (call.arguments as? Map)?.let { + SnowplowTrackerController.setUserId(SetUserIdMessageReader(it)) + } + result.success(null) + } + + private fun onGetSessionUserId(call: MethodCall, result: MethodChannel.Result) { + val sessionUserId = (call.arguments as? Map)?.let { + SnowplowTrackerController.getSessionUserId(GetParameterMessageReader(it)) + } + result.success(sessionUserId) + } + + private fun onGetSessionId(call: MethodCall, result: MethodChannel.Result) { + val sessionId = (call.arguments as? Map)?.let { + SnowplowTrackerController.getSessionId(GetParameterMessageReader(it)) + } + result.success(sessionId) + } + + private fun onGetSessionIndex(call: MethodCall, result: MethodChannel.Result) { + val sessionIndex = (call.arguments as? Map)?.let { + SnowplowTrackerController.getSessionIndex(GetParameterMessageReader(it)) + } + result.success(sessionIndex) + } + +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/TrackerVersion.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/TrackerVersion.kt new file mode 100644 index 0000000..069b978 --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/TrackerVersion.kt @@ -0,0 +1,16 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker + +object TrackerVersion { + val TRACKER_VERSION = "flutter-0.1.0-dev.1" +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/GdprConfigurationReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/GdprConfigurationReader.kt new file mode 100644 index 0000000..4c2a68a --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/GdprConfigurationReader.kt @@ -0,0 +1,34 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.configurations + +import com.snowplowanalytics.snowplow.configuration.GdprConfiguration +import com.snowplowanalytics.snowplow.util.Basis + +class GdprConfigurationReader(val values: Map) { + val basisForProcessing: String by values + val documentId: String by values + val documentVersion: String by values + val documentDescription: String by values + + fun toConfiguration(): GdprConfiguration { + val basis = when (basisForProcessing) { + "contract" -> Basis.CONTRACT + "legal_obligation" -> Basis.LEGAL_OBLIGATION + "legitimate_interests" -> Basis.LEGITIMATE_INTERESTS + "public_task" -> Basis.PUBLIC_TASK + "vital_interests" -> Basis.VITAL_INTERESTS + else -> Basis.CONSENT + } + return GdprConfiguration(basis, documentId, documentVersion, documentDescription) + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/NetworkConfigurationReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/NetworkConfigurationReader.kt new file mode 100644 index 0000000..3b03aec --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/NetworkConfigurationReader.kt @@ -0,0 +1,33 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.configurations + +import com.snowplowanalytics.snowplow.configuration.NetworkConfiguration +import com.snowplowanalytics.snowplow.network.HttpMethod + +class NetworkConfigurationReader(values: Map) { + private val valuesDefault = values.withDefault { null } + + val endpoint: String by values + val method: String? by valuesDefault + + fun toConfiguration(): NetworkConfiguration { + if (method != null) { + return NetworkConfiguration( + endpoint, + if ("get".equals(method, true)) { HttpMethod.GET } else { HttpMethod.POST } + ) + } else { + return NetworkConfiguration(endpoint) + } + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/SubjectConfigurationReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/SubjectConfigurationReader.kt new file mode 100644 index 0000000..63446d3 --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/SubjectConfigurationReader.kt @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.configurations + +import com.snowplowanalytics.snowplow.configuration.SubjectConfiguration +import com.snowplowanalytics.snowplow.util.Size + +class SubjectConfigurationReader(val values: Map) { + private val valuesDefault = values.withDefault { null } + + val userId: String? by valuesDefault + val networkUserId: String? by valuesDefault + val domainUserId: String? by valuesDefault + val userAgent: String? by valuesDefault + val ipAddress: String? by valuesDefault + val timezone: String? by valuesDefault + val language: String? by valuesDefault + val screenResolution: Pair? by valuesDefault + val screenViewport: Pair? by valuesDefault + val colorDepth: Double? by valuesDefault + + val screenResolutionSize: Size? by lazy { + screenResolution?.let { + val screenWidth = it.first.toInt() + val screenHeight = it.second.toInt() + Size(screenWidth, screenHeight) + } + } + val screenViewportSize: Size? by lazy { + screenViewport?.let { + val screenVPWidth = it.first.toInt() + val screenVPHeight = it.second.toInt() + Size(screenVPWidth, screenVPHeight) + } + } + + fun toConfiguration(): SubjectConfiguration { + val subjectConfig = SubjectConfiguration() + + userId?.let { subjectConfig.userId(it) } + networkUserId?.let { subjectConfig.networkUserId(it) } + domainUserId?.let { subjectConfig.domainUserId(it) } + userAgent?.let { subjectConfig.useragent(it) } + ipAddress?.let { subjectConfig.ipAddress(it) } + timezone?.let { subjectConfig.timezone(it) } + language?.let { subjectConfig.language(it) } + screenResolutionSize?.let { subjectConfig.screenResolution(it) } + screenViewportSize?.let { subjectConfig.screenViewPort(it) } + colorDepth?.let { subjectConfig.colorDepth(it.toInt()) } + + return subjectConfig + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/TrackerConfigurationReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/TrackerConfigurationReader.kt new file mode 100644 index 0000000..a1d579f --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/configurations/TrackerConfigurationReader.kt @@ -0,0 +1,68 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.configurations + +import android.content.Context +import com.snowplowanalytics.snowplow.configuration.TrackerConfiguration +import com.snowplowanalytics.snowplow.tracker.DevicePlatform +import com.snowplowanalytics.snowplow_tracker.TrackerVersion + +class TrackerConfigurationReader(values: Map) { + private val valuesDefault = values.withDefault { null } + + val appId: String? by valuesDefault + val devicePlatform: String? by valuesDefault + val base64Encoding: Boolean? by valuesDefault + val platformContext: Boolean? by valuesDefault + val geoLocationContext: Boolean? by valuesDefault + val sessionContext: Boolean? by valuesDefault + + fun toConfiguration(context: Context): TrackerConfiguration { + val trackerConfig = DefaultTrackerConfiguration.toConfiguration(appId, context) + + devicePlatform?.let { + trackerConfig.devicePlatform(when (it) { + "web" -> DevicePlatform.Web + "srv" -> DevicePlatform.ServerSideApp + "pc" -> DevicePlatform.Desktop + "app" -> DevicePlatform.General + "tv" -> DevicePlatform.ConnectedTV + "cnsl" -> DevicePlatform.GameConsole + "iot" -> DevicePlatform.InternetOfThings + else -> trackerConfig.devicePlatform + }) + } + base64Encoding?.let { trackerConfig.base64encoding(it) } + platformContext?.let { trackerConfig.platformContext(it) } + geoLocationContext?.let { trackerConfig.geoLocationContext(it) } + sessionContext?.let { trackerConfig.sessionContext(it) } + + return trackerConfig + } +} + +object DefaultTrackerConfiguration { + fun toConfiguration(appId: String?, context: Context): TrackerConfiguration { + val trackerConfig = TrackerConfiguration(appId ?: context.getPackageName()) + .trackerVersionSuffix(TrackerVersion.TRACKER_VERSION) + + trackerConfig.applicationContext(false) + trackerConfig.screenContext(false) + trackerConfig.screenViewAutotracking(false) + trackerConfig.lifecycleAutotracking(false) + trackerConfig.installAutotracking(false) + trackerConfig.exceptionAutotracking(false) + trackerConfig.diagnosticAutotracking(false) + + return trackerConfig + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentDocumentReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentDocumentReader.kt new file mode 100644 index 0000000..cce387f --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentDocumentReader.kt @@ -0,0 +1,30 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.events + +import com.snowplowanalytics.snowplow.event.ConsentDocument + +class ConsentDocumentReader(val values: Map) { + private val valuesDefault = values.withDefault { null } + + val documentId: String by values + val documentVersion: String by values + val documentName: String? by valuesDefault + val documentDescription: String? by valuesDefault + + fun toConsentDocument(): ConsentDocument { + val consentDocument = ConsentDocument(documentId, documentVersion) + documentName?.let { consentDocument.documentName(it) } + documentDescription?.let { consentDocument.documentDescription(it) } + return consentDocument + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentGrantedReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentGrantedReader.kt new file mode 100644 index 0000000..891a45b --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentGrantedReader.kt @@ -0,0 +1,37 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.events + +import com.snowplowanalytics.snowplow.event.ConsentDocument +import com.snowplowanalytics.snowplow.event.ConsentGranted + +class ConsentGrantedReader(val values: Map) { + private val valuesDefault = values.withDefault { null } + + val expiry: String by values + val documentId: String by values + val version: String by values + val name: String? by valuesDefault + val documentDescription: String? by valuesDefault + val consentDocuments: List>? by valuesDefault + private val processedConsentDocuments: List? by lazy { + consentDocuments?.let { it.map { ConsentDocumentReader(it).toConsentDocument() } } + } + + fun toConsentGranted(): ConsentGranted { + val consentGranted = ConsentGranted(expiry, documentId, version) + name?.let { consentGranted.documentName(it) } + documentDescription?.let { consentGranted.documentDescription(it) } + processedConsentDocuments?.let { consentGranted.documents(it) } + return consentGranted + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentWithdrawnReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentWithdrawnReader.kt new file mode 100644 index 0000000..8e68706 --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ConsentWithdrawnReader.kt @@ -0,0 +1,37 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.events + +import com.snowplowanalytics.snowplow.event.ConsentDocument +import com.snowplowanalytics.snowplow.event.ConsentWithdrawn + +class ConsentWithdrawnReader(val values: Map) { + private val valuesDefault = values.withDefault { null } + + val all: Boolean by values + val documentId: String by values + val version: String by values + val name: String? by valuesDefault + val documentDescription: String? by valuesDefault + val consentDocuments: List>? by valuesDefault + private val processedConsentDocuments: List? by lazy { + consentDocuments?.let { it.map { ConsentDocumentReader(it).toConsentDocument() } } + } + + fun toConsentWithdrawn(): ConsentWithdrawn { + val consentWithdrawn = ConsentWithdrawn(all, documentId, version) + name?.let { consentWithdrawn.documentName(it) } + documentDescription?.let { consentWithdrawn.documentDescription(it) } + processedConsentDocuments?.let { consentWithdrawn.documents(it) } + return consentWithdrawn + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ScreenViewReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ScreenViewReader.kt new file mode 100644 index 0000000..b134a4e --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/ScreenViewReader.kt @@ -0,0 +1,38 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.events + +import com.snowplowanalytics.snowplow.event.ScreenView +import java.util.* + +class ScreenViewReader(val values: Map) { + private val valuesDefault = values.withDefault { null } + + val name: String by values + private val id: String? by valuesDefault + val idUUID: UUID? by lazy { id?.let { UUID.fromString(it) } } + val type: String? by valuesDefault + val previousName: String? by valuesDefault + val previousType: String? by valuesDefault + val previousId: String? by valuesDefault + val transitionType: String? by valuesDefault + + fun toScreenView(): ScreenView { + val screenView = ScreenView(name, idUUID) + type?.let { screenView.type(it) } + previousName?.let { screenView.previousName(it) } + previousType?.let { screenView.previousType(it) } + previousId?.let { screenView.previousId(it) } + transitionType?.let { screenView.transitionType(it) } + return screenView + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/SelfDescribingJsonReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/SelfDescribingJsonReader.kt new file mode 100644 index 0000000..9c07726 --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/SelfDescribingJsonReader.kt @@ -0,0 +1,28 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.events + +import com.snowplowanalytics.snowplow.event.SelfDescribing +import com.snowplowanalytics.snowplow.payload.SelfDescribingJson + +class SelfDescribingJsonReader(val values: Map) { + val schema: String by values + val data: Map by values + + fun toSelfDescribingJson(): SelfDescribingJson { + return SelfDescribingJson(schema, data) + } + + fun toSelfDescribing(): SelfDescribing { + return SelfDescribing(toSelfDescribingJson()) + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/StructuredReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/StructuredReader.kt new file mode 100644 index 0000000..786afe6 --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/StructuredReader.kt @@ -0,0 +1,32 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.events + +import com.snowplowanalytics.snowplow.event.Structured + +class StructuredReader(values: Map) { + private val valuesDefault = values.withDefault { null } + + val category: String by values + val action: String by values + val label: String? by valuesDefault + val property: String? by valuesDefault + val value: Double? by valuesDefault + + fun toStructured(): Structured { + val structured = Structured(category, action) + label?.let { structured.label(it) } + property?.let { structured.property(it) } + value?.let { structured.value(it) } + return structured + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/TimingReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/TimingReader.kt new file mode 100644 index 0000000..3ebc806 --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/events/TimingReader.kt @@ -0,0 +1,29 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.events + +import com.snowplowanalytics.snowplow.event.Timing + +class TimingReader(val values: Map) { + private val valuesDefault = values.withDefault { null } + + val category: String by values + val variable: String by values + val timing: Int by values + val label: String? by valuesDefault + + fun toTiming(): Timing { + val timing = Timing(category, variable, timing) + label?.let { timing.label(it) } + return timing + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/CreateTrackerMessageReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/CreateTrackerMessageReader.kt new file mode 100644 index 0000000..3240d29 --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/CreateTrackerMessageReader.kt @@ -0,0 +1,36 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.messages + +import com.snowplowanalytics.snowplow_tracker.readers.configurations.* + +class CreateTrackerMessageReader(val values: Map) { + val namespace: String by values + val networkConfig: NetworkConfigurationReader by lazy { + NetworkConfigurationReader(values.get("networkConfig") as Map) + } + val trackerConfig: TrackerConfigurationReader? by lazy { + values.get("trackerConfig")?.let { + TrackerConfigurationReader(it as Map) + } + } + val subjectConfig: SubjectConfigurationReader? by lazy { + values.get("subjectConfig")?.let { + SubjectConfigurationReader(it as Map) + } + } + val gdprConfig: GdprConfigurationReader? by lazy { + values.get("gdprConfig")?.let { + GdprConfigurationReader(it as Map) + } + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/EventMessageReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/EventMessageReader.kt new file mode 100644 index 0000000..7bd8c8e --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/EventMessageReader.kt @@ -0,0 +1,63 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.messages + +import com.snowplowanalytics.snowplow.event.* +import com.snowplowanalytics.snowplow.payload.SelfDescribingJson +import com.snowplowanalytics.snowplow_tracker.readers.events.* + +class EventMessageReader(val values: Map) { + private val valuesDefault = values.withDefault { null } + + val tracker: String by values + private val eventData: Map by values + private val contexts: List>? by valuesDefault + private val contextsJsons: List? by lazy { + contexts?.let { it.map { item -> SelfDescribingJsonReader(item).toSelfDescribingJson() } } + } + + fun toStructuredWithContexts(): Structured { + val structured = StructuredReader(eventData).toStructured() + contextsJsons?.let { structured.customContexts.addAll(it) } + return structured + } + + fun toSelfDescribingWithContexts(): SelfDescribing { + val selfDescribing = SelfDescribingJsonReader(eventData).toSelfDescribing() + contextsJsons?.let { selfDescribing.customContexts.addAll(it) } + return selfDescribing + } + + fun toScreenViewWithContexts(): ScreenView { + val screenView = ScreenViewReader(eventData).toScreenView() + contextsJsons?.let { screenView.customContexts.addAll(it) } + return screenView + } + + fun toTimingWithContexts(): Timing { + val timing = TimingReader(eventData).toTiming() + contextsJsons?.let { timing.customContexts.addAll(it) } + return timing + } + + fun toConsentGrantedWithContexts(): ConsentGranted { + val consentGranted = ConsentGrantedReader(eventData).toConsentGranted() + contextsJsons?.let { consentGranted.customContexts.addAll(it) } + return consentGranted + } + + fun toConsentWithdrawnWithContexts(): ConsentWithdrawn { + val consentWithdrawn = ConsentWithdrawnReader(eventData).toConsentWithdrawn() + contextsJsons?.let { consentWithdrawn.customContexts.addAll(it) } + return consentWithdrawn + } +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/GetParameterMessageReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/GetParameterMessageReader.kt new file mode 100644 index 0000000..76b69ea --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/GetParameterMessageReader.kt @@ -0,0 +1,16 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.messages + +class GetParameterMessageReader(val values: Map) { + val tracker: String by values +} diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/SetUserIdMessageReader.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/SetUserIdMessageReader.kt new file mode 100644 index 0000000..78ad33f --- /dev/null +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/readers/messages/SetUserIdMessageReader.kt @@ -0,0 +1,19 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +package com.snowplowanalytics.snowplow_tracker.readers.messages + +class SetUserIdMessageReader(val values: Map) { + private val valuesDefault = values.withDefault { null } + + val tracker: String by values + val userId: String? by valuesDefault +} diff --git a/dartdoc_options.yaml b/dartdoc_options.yaml new file mode 100644 index 0000000..710c436 --- /dev/null +++ b/dartdoc_options.yaml @@ -0,0 +1,23 @@ +dartdoc: + categories: + "Getting started": + markdown: doc/01-getting-started.md + name: Getting started + "Initialization and configuration": + markdown: doc/02-configuration.md + name: Initialization and configuration + "Tracking events": + markdown: doc/03-tracking-events.md + name: Tracking events + "Adding data to your events": + markdown: doc/04-adding-data.md + name: Adding data to your events + "Sessions and data model": + markdown: doc/05-sessions.md + name: Sessions and data model + categoryOrder: ["Getting started", "Initialization and configuration", "Tracking events", "Adding data to your events", "Sessions and data model"] + showUndocumentedCategories: true + errors: + - unresolved-doc-reference + warnings: + - tool-error diff --git a/doc/01-getting-started.md b/doc/01-getting-started.md new file mode 100644 index 0000000..57cf592 --- /dev/null +++ b/doc/01-getting-started.md @@ -0,0 +1,69 @@ +# Getting started + +Designing how and what to track in your app is an important decision. Check out our docs about tracking design [here](https://docs.snowplowanalytics.com/docs/understanding-tracking-design/introduction-to-tracking-design/). + +The following steps will guide you through setting up the Flutter tracker in your project and tracking a simple event. + +## Installation + +Add the Snowplow tracker as a dependency to your Flutter application: + +```bash +flutter pub add snowplow_tracker +``` + +This will add a line with the dependency like to your pubspec.yaml: + +```yml +dependencies: + snowplow_tracker: ^0.1.0 +``` + +Import the package into your Dart code: + +```dart +import 'package:snowplow_tracker/snowplow_tracker.dart' +``` + +### Installation on Web + +If using the tracker within a Flutter app for Web, you will also need to import the Snowplow JavaScript Tracker in your `index.html` file. Please load the JS tracker with the Snowplow tag as [described in the official documentation](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-trackers/javascript-tracker/javascript-tracker-v3/tracker-setup/loading/). Do not change the global function name `snowplow` that is used to access the tracker – the Flutter APIs assume that it remains the default as shown in documentation. + +Make sure to use JavaScript tracker version `3.2` or newer. You may also refer to the [example project](https://github.com/snowplow-incubator/snowplow-flutter-tracker/tree/main/example) in the Flutter tracker repository to see this in action. + +## Initialization + +Instantiate a tracker using the `Snowplow.createTracker` function. +You may create the tracker in the `initState()` of your main widget. +At its most basic, the function takes two required arguments: `namespace` and `endpoint`. +Tracker namespace identifies the tracker instance, you may create multiple trackers with different namespaces. +The endpoint is the URI of the Snowplow collector to send the events to. + +```dart +Tracker tracker = await Snowplow.createTracker( + namespace: 'ns1', + endpoint: 'http://...' +); +``` + +There are additional optional arguments to configure the tracker. To learn more about configuring how events are sent, check out [this page](02-configuration.md). + +## Tracking events + +To track events, simply instantiate their respective types (e.g., `ScreenView`, `SelfDescribing`, `Structured`) and pass them to the `tracker.track` or `Snowplow.track` methods. + +```dart +tracker.track(ScreenView( + id: '2c295365-eae9-4243-a3ee-5c4b7baccc8f', + name: 'home', + type: 'full', + transitionType: 'none')); +``` + +Visit documentation about [tracking events](03-tracking-events.md) to learn about other supported event types. You may also want to read about [adding more data to tracked events](04-adding-data.md). + +## Testing + +Testing that your event tracking is properly configured can be as important as testing the other aspects of your app. It confirms that you are generating the events you expect. + +We provide two types of pipeline for testing and debugging. [Snowplow Mini](https://docs.snowplowanalytics.com/docs/understanding-your-pipeline/what-is-snowplow-mini/) is especially useful in manual schema and pipeline testing. [Snowplow Micro](https://docs.snowplowanalytics.com/docs/understanding-your-pipeline/what-is-snowplow-micro/) is a minimal pipeline designed to be used as part of your app's automated test suite. diff --git a/doc/02-configuration.md b/doc/02-configuration.md new file mode 100644 index 0000000..97631fd --- /dev/null +++ b/doc/02-configuration.md @@ -0,0 +1,87 @@ +# Initialization and configuration + +The package provides a single method to initialize and configure a new tracker, the `Snowplow.createTracker` method. It accepts configuration parameters for the tracker and returns a `Tracker` instance. + +```dart +Tracker tracker = await Snowplow.createTracker( + namespace: 'ns1', + endpoint: 'http://...', + method: Method.post, + trackerConfig: const TrackerConfiguration(...), + gdprConfig: const GdprConfiguration(...), + subjectConfig: const SubjectConfiguration(...)); +); +``` + +The method returns a `Tracker` instance. This can be later used for tracking events, or accessing tracker properties. However, all methods provided by the `Tracker` instance are also available as static functions in the `Snowplow` class but they require passing the tracker namespace as string. + +The only required attributes of the `Snowplow.createTracker` method are `namespace` used to identify the tracker, and the Snowplow collector `endpoint`. Additionally, one can configure the HTTP method to be used when sending events to the collector and provide configuration by instantiating classes for `TrackerConfiguration`, `SubjectConfiguration`, or `GdprConfiguration`. The following arguments are accepted by the `Snowplow.createTracker` method: + +| Attribute | Type | Description | +|---|---|---| +| `namespace` | `String` | Tracker namespace to identify the tracker. | +| `endpoint` | `String` | URI for the Snowplow collector endpoint. | +| `method` | `Method?` | HTTP method to use. `Method.get` and `Method.post` options are available. | +| `trackerConfig` | `TrackerConfiguration?` | Configuration of the tracker and the core tracker properties. | +| `gdprConfig` | `GdprConfiguration?` | Determines the GDPR context that will be attached to all events sent by the tracker. | +| `subjectConfig` | `SubjectConfiguration?` | Subject information about tracked user and device that is added to events. | + +## Configuration of tracker properties: `TrackerConfiguration` + +`TrackerConfiguration` provides options to configure properties and features of the tracker. In addition to setting the app identifier and device platform, the configuration enables turning several automatic context entities on and off. + +| Attribute | Type | Description | Android | iOS | Web | Default | +|---|---|---|---|---|---|---| +| `appId` | `String?` | Identifier of the app. | ✔ | ✔ | ✔ | null on Web, bundle identifier on iOS/Android | +| `devicePlatform` | `DevicePlatform?` | The device platform the tracker runs on. Available options are provided by the `DevicePlatform` enum. | ✔ | ✔ | ✔ | "web" on Web, "mob" on iOS/Android | +| `base64Encoding` | `bool?` | Indicates whether payload JSON data should be base64 encoded. | ✔ | ✔ | ✔ | true | +| `platformContext` | `bool?` | Indicates whether platform context should be attached to tracked events. | ✔ | ✔ | | true | +| `geoLocationContext` | `bool?` | Indicates whether geo-location context should be attached to tracked events. | ✔ | ✔ | ✔ | false | +| `sessionContext` | `bool?` | Indicates whether session context should be attached to tracked events. | ✔ | ✔ | ✔ | true | +| `webPageContext` | `bool?` | Indicates whether context about current web page should be attached to tracked events. | | | ✔ | true | + +## Configuration of subject information: `SubjectConfiguration` + +Subject information are persistent and global information about the tracked device or user. They apply to all events and are assigned as event properties. + +Some of the properties are only configurable on iOS and Android and are automatically assigned on the Web. + +| Attribute | Type | Description | Android | iOS | Web | Default | +|---|---|---|---|---|---|---| +| `userId` | `String?` | Business ID of the user. | ✔ | ✔ | ✔ | | +| `networkUserId` | `String?` | Network user ID (UUIDv4). | ✔ | ✔ | Non-configurable, auto-assigned. | | +| `domainUserId` | `String?` | Domain user ID (UUIDv4). | ✔ | ✔ | Non-configurable, auto-assigned. | | +| `userAgent` | `String?` | Custom user-agent. It overrides the user-agent used by default. | ✔ | ✔ | Non-configurable, auto-assigned. | | +| `timezone` | `String?` | The timezone label. | ✔ | ✔ | Non-configurable, auto-assigned. | | +| `language` | `String?` | The language set on the device. | ✔ | ✔ | Non-configurable, auto-assigned. | | +| `screenResolution` | `Size?` | The screen resolution on the device. | ✔ | ✔ | Non-configurable, auto-assigned. | | +| `screenViewport` | `Size?` | The screen viewport. | ✔ | ✔ | Non-configurable, auto-assigned. | | +| `colorDepth` | `double?` | The color depth. | ✔ | ✔ | Non-configurable, auto-assigned. | | + +The configured attributes are mapped to Snowplow event properties described in the [Snowplow Tracker Protocol](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol/). They are mapped as follows: + +| Attribute | Event Property | +|---|---| +| `userId` | `uid` | +| `networkUserId` | `network_userid` | +| `domainUserId` | `domain_userid` | +| `userAgent` | `useragent` | +| `ipAddress` | `user_ipaddress` | +| `timezone` | `os_timezone` | +| `language` | `lang` | +| `screenResolution.width` | `dvce_screenwidth` | +| `screenResolution.height` | `dvce_screenheight` | +| `screenViewport.width` | `br_viewwidth` | +| `screenViewport.height` | `br_viewheight` | +| `colorDepth` | `br_colordepth` | + +## GDPR context entity configuration: `GdprConfiguration` + +Determines the GDPR context that will be attached to all events sent by the tracker. + +| Attribute | Type | Description | Android | iOS | Web | Default | +|---|---|---|---|---|---|---| +| `basisForProcessing` | `String` | Basis for processing. | ✔ | ✔ | ✔ | | +| `documentId` | `String` | ID of a GDPR basis document. | ✔ | ✔ | ✔ | | +| `documentVersion` | `String` | Version of the document. | ✔ | ✔ | ✔ | | +| `documentDescription` | `String` | Description of the document. | ✔ | ✔ | ✔ | | diff --git a/doc/03-tracking-events.md b/doc/03-tracking-events.md new file mode 100644 index 0000000..3e17f32 --- /dev/null +++ b/doc/03-tracking-events.md @@ -0,0 +1,179 @@ +# Tracking events + +Snowplow has been built to enable you to track a wide range of events that occur when users interact with your apps. + +We provide several built-in event classes to help you track different kinds of events. When instantiated, their objects can be passed to the `Snowplow.track()` method to send events to the Snowplow collector. The event classes range from single purpose ones, such as `ScreenView`, to the more complex but flexible `SelfDescribing`, which can be used to track any kind of user behaviour. We strongly recommend using `SelfDescribing` for your tracking, as it allows you to design custom event types to match your business requirements. [This post](https://snowplowanalytics.com/blog/2020/01/24/re-thinking-the-structure-of-event-data/) on our blog, "Re-thinking the structure of event data" might be informative here. + +Event classes supported by the Flutter Tracker: + +| Method | Event type tracked | +|---|---| +| `SelfDescribing` | Custom event based on "self-describing" JSON schema | +| `Structured` | Semi-custom structured event | +| `ScreenView` | View of a screen in the app | +| `Timing` | User timing events such as how long resources take to load. | +| `ConsentGranted` | User opting into data collection. | +| `ConsentWithdrawn` | User withdrawing consent for data collection. | + +All the methods share common features and parameters. Every type of event can have an optional context added. See the [next page](04-adding-data.md) to learn about adding extra data to events. It's important to understand how event context works, as it is one of the most powerful Snowplow features. Adding event context is a way to add depth, richness and value to all of your events. + +Snowplow events are all processed into the same format, regardless of the event type (and regardless of the tracker language used). Read about the different properties and fields of events in the [Snowplow Tracker Protocol](https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/snowplow-tracker-protocol/). + +We will first discuss the custom event types, followed by the out-of-the-box event types. Note that you can also design and create your own page view, or screen view, using `selfDescribing`, to fit your business needs better. The out-of-the-box event types are provided so you can get started with generating event data quickly. + +## Track self-describing events with `SelfDescribing` + +Use the `SelfDescribing` type to track a custom event. This is the most advanced and powerful tracking method, which requires a certain amount of planning and infrastructure. + +Self-describing events are based around "self-describing" (self-referential) JSONs, which are a specific kind of [JSON schema](http://json-schema.org/). A unique schema can be designed for each type of event that you want to track. This allows you to track the specific things that are important to you, in a way that is defined by you. + +This is particularly useful when: + +- You want to track event types which are proprietary/specific to your business +- You want to track events which have unpredictable or frequently changing properties + +A self-describing JSON has two keys, `schema` and `data`. The `schema` value should point to a valid self-describing JSON schema. They are called self-describing because the schema will specify the fields allowed in the data value. Read more about how schemas are used with Snowplow [here](https://docs.snowplowanalytics.com/docs/understanding-tracking-design/understanding-schemas-and-validation/). + +After events have been collected by the event collector, they are validated to ensure that the properties match the self-describing JSONs. Mistakes (e.g. extra fields, or incorrect types) will result in events being processed as Bad Events. This means that only high-quality, valid events arrive in your data storage or real-time stream. + +Your schemas must be accessible to your pipeline to allow this validation. We provide [Iglu](https://docs.snowplowanalytics.com/docs/pipeline-components-and-applications/iglu/) for schema management. If you are a paid Snowplow customer, you can manage your schemas through your console. Check out our [Ruby tracker Rails](https://github.com/snowplow-incubator/snowplow-ruby-tracker-examples) example to see how we included schemas in the Snowplow Micro testing pipeline in that app. + +Creating an instance of `SelfDescribing` takes a schema name and a dictionary of event data. + +Example (assumes that `tracker` is a tracker instance created using `Snowplow.createTracker`): + +```dart +tracker.track(SelfDescribing( + schema: 'iglu:com.example_company/save_game/jsonschema/1-0-2', + data: { + 'saveId': '4321', + 'level': 23, + 'difficultyLevel': 'HARD', + 'dlContent': true + } +)); +``` + +## Track structured events with `Structured` + +This method provides a halfway-house between tracking fully user-defined self-describing events and out-of-the box predefined events. This event type can be used to track many types of user activity, as it is somewhat customizable. "Struct" events closely mirror the structure of Google Analytics events, with "category", "action", "label", and "value" properties. + +As these fields are fairly arbitrary, we recommend following the advice in this table how to define structured events. It's important to be consistent throughout the business about how each field is used. + +| Argument | Description | Required in event? | +| -----------| -------------------------------------------------------------- | ------------------ | +| `category` | The grouping of structured events which this action belongs to | Yes | +| `action` | Defines the type of user interaction which this event involves | Yes | +| `label` | Often used to refer to the 'object' the action is performed on | No | +| `property` | Describing the 'object', or the action performed on it | No | +| `value` | Provides numerical data about the event | No | + +Example: + +```dart +tracker.track(Structured( + category: 'shop', + action: 'add-to-basket', + label: 'Add To Basket', + property: 'pcs', + value: 2.00, +)); +``` + +## Track screen views with `ScreenView` + +Use `ScreenView` to track a user viewing a screen (or similar) within your app. This is the page view equivalent for apps that are not webpages (the Flutter tracker follows the [Snowplow mobile data model](https://docs.snowplowanalytics.com/docs/modeling-your-data/the-snowplow-mobile-model/) and uses them also on the Web). The arguments are `name`, `id`, `type`, and `transitionType`. The `name` and `id` properties are required. "Name" is the human-readable screen name, and "ID" should be the unique screen ID (UUID v4). + +This method creates an unstruct event, by creating and tracking a self-describing event. The schema ID for this is "iglu:com.snowplowanalytics.snowplow/screen_view/jsonschema/1-0-0", and the data field will contain the parameters which you provide. That schema is hosted on the schema repository Iglu Central, and so will always be available to your pipeline. + +| Argument | Description | Required in event? | +|---|---|---| +| `name` | The name of the screen viewed. | Yes | +| `id` | The id (UUID v4) of screen that was viewed. | Yes | +| `type` | The type of screen that was viewed. | No | +| `previousName` | The name of the previous screen that was viewed. | No | +| `previousType` | The type of screen that was viewed. | No | +| `previousId` | The id (UUID v4) of the previous screen that was viewed. | No | +| `transitionType` | The type of transition that led to the screen being viewed. | No | + +Example: + +```dart +tracker.track(ScreenView( + id: '2c295365-eae9-4243-a3ee-5c4b7baccc8f', + name: 'home', + type: 'full', + transitionType: 'none')); +``` + +## Track timing events with `Timing` + +Use the `Timing` type to track user timing events such as how long resources take to load. These events take a timing `category`, the `variable` being measured, and the `timing` time measurement. An optional `label` can be added to further identify the timing event + +| Argument | Description | Required in event? | +|---|---|---| +| `category` | Defines the timing category. | Yes | +| `variable` | Defines the timing variable measured. | Yes | +| `timing` | Represents the time. | Yes | +| `label` | An optional string to further identify the timing event. | No | + +Example: + +```dart +tracker.track(Timing( + category: 'category', + variable: 'variable', + timing: 1, + label: 'label', +)); +``` + +## Track user consent with `ConsentGranted` and `ConsentWithdrawn` + +Use the `ConsentGranted` to track a user opting into data collection and `ConsentWithdrawn` to track a user withdrawing their consent for data collection. + +For both events, a consent document context will be attached to the event using the `documentId` and `version` arguments supplied. To specify that a user opts out of all data collection using the `ConsentWithdrawn` event, the `all` property should be set to true. + +Properties of `ConsentGranted`: + +| Argument | Description | Required in event? | +|---|---|---| +| `expiry` | The expiry date-time of the consent. | Yes | +| `documentId` | The consent document ID. | Yes | +| `version` | The consent document version. | Yes | +| `name` | Optional consent document name. | No | +| `documentDescription` | Optional consent document description. | No | + +Example: + +```dart +tracker.track(ConsentGranted( + expiry: DateTime.now(), + documentId: '1234', + version: '5', + name: 'name1', + documentDescription: 'description1', +)); +``` + +Properties of `ConsentWithdrawn`: + +| Argument | Description | Required in event? | +|---|---|---| +| `all` | Whether user opts out of all data collection. | Yes | +| `documentId` | The consent document ID. | Yes | +| `version` | The consent document version. | Yes | +| `name` | Optional consent document name. | No | +| `documentDescription` | Optional consent document description. | No | + +Example: + +```dart +tracker.track(ConsentWithdrawn( + all: false, + documentId: '1234', + version: '5', + name: 'name1', + documentDescription: 'description1', +)); +``` diff --git a/doc/04-adding-data.md b/doc/04-adding-data.md new file mode 100644 index 0000000..a8a476d --- /dev/null +++ b/doc/04-adding-data.md @@ -0,0 +1,54 @@ +# Adding data to your events: context and more + +There are multiple ways to add extra data to your tracked events, adding richness and value to your dataset: + +1. Event context using self-describing data: Attach event context, describing anything you like, in the form of self-describing JSONs. +2. Subject: Include information about the user, or the platform on which the event occurred. + +## Event context + +Event context is an incredibly powerful aspect of Snowplow tracking, which allows you to create very rich data. It is based on the same self-describing JSON schemas as the self-describing events. Using event context, you can add any details you like to your events, as long as you can describe them in a self-describing JSON schema. + +Each schema will describe a single "entity". All of an event’s entities together form the event context. The event context will be sent as one field of the event, finally ending up in one column (`context`) in your data storage. There is no limit to how many entities can be attached to one event. + +Note that context can be added to any event type, not just self-describing events. This means that even a simple event type like a page view can hold complex and extensive information – reducing the chances of data loss and the amount of modelling (JOINs etc.) needed in modelling, while increasing the value of each event, and the sophistication of the possible use cases. + +The entities you provide are validated against their schemas as the event is processed (during the enrich phase). If there is a mistake or mismatch, the event is processed as a Bad Event. + +Once defined, an entity can be attached to any kind of event. This is also an important point; it means your tracking is as DRY as possible. Using the same "user" or "image" or "search result" (etc.) entities throughout your tracking reduces error, and again makes the data easier to model. + +Example: + +```dart +tracker.track( + Structured(category: 'shop', action: 'add-to-basket'), + contexts: [ + const SelfDescribing( + schema: 'iglu:com.my_company/movie_poster/jsonschema/1-0-0', + data: { + 'movie_name': 'Solaris', + 'poster_country': 'JP', + 'poster_date': '1978-01-01' + } + ), + const SelfDescribing( + schema: 'iglu:com.my_company/customer/jsonschema/1-0-0', + data: { + 'p_buy': 0.23, + 'segment': 'young_adult' + } + ) + ] +); +``` + +## Adding user and platform data with Subject + +Subject information describes the user and device associated with the event, such as their user ID, what type of device they used, or what size screen that device had. + +This information can be entered during tracker initialization by passing a `SubjectConfiguration` instance to the `Snowplow.createTracker` method. All of the information is optional. + +Some subject information is filled automatically by the tracker. This includes the platform of the user, timezone, language, resolution, +and viewport. + +Please refer to the section on subject configuration on the [Configuration page](02-configuration.md) to learn more. diff --git a/doc/05-sessions.md b/doc/05-sessions.md new file mode 100644 index 0000000..9638c3c --- /dev/null +++ b/doc/05-sessions.md @@ -0,0 +1,20 @@ +# Sessions and data model + +The Flutter tracker uses the [Snowplow mobile data model](https://docs.snowplowanalytics.com/docs/modeling-your-data/the-snowplow-mobile-model/) across all supported platforms – Android, iOS, and Web. In contrast with the [web data model](https://docs.snowplowanalytics.com/docs/modeling-your-data/the-snowplow-web-data-model/) which builds on page view and page ping events, the mobile data model uses screen view events. The mobile data model was chosen in order to make event tracking consistent across all supported Flutter platforms. + +In addition to adopting screen view events, the mobile data model defines that sessions are represented using a [context entity](https://github.com/snowplow/iglu-central/blob/master/schemas/com.snowplowanalytics.snowplow/client_session/jsonschema/1-0-1). Concretely, the `client_session` context entity is added to all tracked events if session tracking is enabled in the tracker configuration (through the `sessionContext` property). This entity consists of the following properties: + +| Attribute | Description | Required? | +|---|---|---| +| `userId` | An identifier for the user of the session. | Yes | +| `sessionId` | An identifier (UUID) for the session. | Yes | +| `sessionIndex` | The index of the current session for this user. | Yes | +| `previousSessionId` | The previous session identifier (UUID) for this user. | No | +| `storageMechanism` | The mechanism that the session information has been stored on the device. | Yes | +| `firstEventId` | The optional identifier (UUID) of the first event id for this session. | No | + +Behind the scenes, the Flutter tracker uses the default configuration for session management on the Android, iOS, and Web trackers. + +On Android and iOS, session data is maintained for the life of the application being installed on a device. Essentially it will update if it is not accessed within a configurable timeout. There are two inactivity timeouts that result in updates to the `sessionId`: foreground inactivity, and background inactivity timeout. The default 30 minutes setting is used for both. + +On the Web, the tracker uses domain (`duid`) and session cookies (`sid`) as implemented by the JavaScript tracker. The session cookie expires after 30 minutes of inactivity. This means that a user leaving the site and returning in under 30 minutes does not change the session. In contrast with the JavaScript tracker, the Flutter tracker also adds the `client_session` context entity that wraps the domain and session IDs. diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..0fa6b67 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..fd70cab --- /dev/null +++ b/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b + channel: stable + +project_type: app diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..2f8acb6 --- /dev/null +++ b/example/README.md @@ -0,0 +1,3 @@ +# snowplow_tracker_example + +Demonstrates how to use the snowplow_tracker plugin. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..61b6c4d --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..87853a4 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,69 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.snowplowanalytics.snowplow_tracker_example" + minSdkVersion 21 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "com.squareup.okhttp3:okhttp:4.9.3" +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..685719a --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..191593c --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/snowplowanalytics/snowplow_tracker_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/snowplowanalytics/snowplow_tracker_example/MainActivity.kt new file mode 100644 index 0000000..7d72d95 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/snowplowanalytics/snowplow_tracker_example/MainActivity.kt @@ -0,0 +1,6 @@ +package com.snowplowanalytics.snowplow_tracker_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..3db14bb --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..d460d1e --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..685719a --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..6a9509c --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.4.32' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..94adc3a --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b8793d3 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..44e62bc --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/example/integration_test/configuration_test.dart b/example/integration_test/configuration_test.dart new file mode 100644 index 0000000..8d9bbb7 --- /dev/null +++ b/example/integration_test/configuration_test.dart @@ -0,0 +1,145 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:snowplow_tracker/snowplow_tracker.dart'; + +import 'helpers.dart'; +import 'package:snowplow_tracker_example/main.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + await SnowplowTests.resetMicro(); + }); + + testWidgets("sets and changes user id", (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + Tracker tracker = await Snowplow.createTracker( + namespace: 'test', + endpoint: SnowplowTests.microEndpoint, + subjectConfig: const SubjectConfiguration(userId: 'XYZ')); + + await tracker + .track(const Structured(category: 'category', action: 'action')); + + expect( + await SnowplowTests.checkMicroGood((events) => + (events.length == 1) && (events[0]['event']['user_id'] == 'XYZ')), + isTrue); + + await tracker.setUserId('ABC'); + + await SnowplowTests.resetMicro(); + await tracker + .track(const Structured(category: 'category', action: 'action')); + + expect( + await SnowplowTests.checkMicroGood((events) => + (events.length == 1) && (events[0]['event']['user_id'] == 'ABC')), + isTrue); + }); + + testWidgets("adds web page context depending on configuration", + (WidgetTester tester) async { + if (!kIsWeb) { + return; + } + await tester.pumpWidget(const MyApp(testing: true)); + + Tracker withoutContext = await Snowplow.createTracker( + namespace: 'withoutContext', + endpoint: SnowplowTests.microEndpoint, + trackerConfig: const TrackerConfiguration(webPageContext: false)); + + Tracker withContext = await Snowplow.createTracker( + namespace: 'withContext', + endpoint: SnowplowTests.microEndpoint, + trackerConfig: const TrackerConfiguration(webPageContext: true)); + + await withoutContext + .track(const Structured(category: 'category', action: 'action')); + await withContext + .track(const Structured(category: 'category', action: 'action')); + + expect( + await SnowplowTests.checkMicroGood((events) => + (events.length == 2) && + (events + .where((x) => (x['contexts'] as List).contains( + 'iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0')) + .length == + 1) && + (events + .where((x) => !(x['contexts'] as List).contains( + 'iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0')) + .length == + 1)), + isTrue); + }); + + testWidgets("attaches gdpr context to events", (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + Tracker tracker = await Snowplow.createTracker( + namespace: 'gdpr', + endpoint: SnowplowTests.microEndpoint, + trackerConfig: const TrackerConfiguration(), + gdprConfig: const GdprConfiguration( + basisForProcessing: 'consent', + documentId: 'consentDoc-abc123', + documentVersion: '0.1.0', + documentDescription: + 'this document describes consent basis for processing')); + + tracker.track(const Structured(category: 'category', action: 'action')); + + expect( + await SnowplowTests.checkMicroGood((dynamic events) { + if (events.length != 1) { + return false; + } + dynamic context = events[0]['event']['contexts']['data'] + .firstWhere((x) => x['schema'].toString().contains('gdpr')); + return (context['data']['documentId'] == 'consentDoc-abc123') && + (context['data']['documentVersion'] == '0.1.0') && + (context['data']['basisForProcessing'] == 'consent') && + (context['data']['documentDescription'] == + 'this document describes consent basis for processing'); + }), + isTrue); + }); + + testWidgets("sets app ID and platform based on configuration", + (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + Tracker tracker = await Snowplow.createTracker( + namespace: 'app-platform', + endpoint: SnowplowTests.microEndpoint, + trackerConfig: const TrackerConfiguration( + appId: 'App Z', devicePlatform: DevicePlatform.iot)); + + await tracker + .track(const Structured(category: 'category', action: 'action')); + + expect( + await SnowplowTests.checkMicroGood((events) => + (events.length == 1) && + (events[0]['event']['app_id'] == 'App Z') && + (events[0]['event']['platform'] == 'iot')), + isTrue); + }); +} diff --git a/example/integration_test/events_test.dart b/example/integration_test/events_test.dart new file mode 100644 index 0000000..3984a3e --- /dev/null +++ b/example/integration_test/events_test.dart @@ -0,0 +1,189 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:uuid/uuid.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:snowplow_tracker/snowplow.dart'; +import 'package:snowplow_tracker/events/structured.dart'; +import 'package:snowplow_tracker/events/self_describing.dart'; +import 'package:snowplow_tracker/events/screen_view.dart'; +import 'package:snowplow_tracker/events/timing.dart'; +import 'package:snowplow_tracker/events/consent_granted.dart'; +import 'package:snowplow_tracker/events/consent_withdrawn.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'helpers.dart'; +import 'package:snowplow_tracker_example/main.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + await SnowplowTests.createTracker(); + }); + + setUp(() async { + await SnowplowTests.resetMicro(); + }); + + testWidgets("tracks a structured event", (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + await Snowplow.track( + const Structured(category: 'category', action: 'action'), + tracker: "test"); + + expect( + await SnowplowTests.checkMicroCounts( + (body) => (body['good'] == 1) && (body['bad'] == 0)), + isTrue); + expect( + await SnowplowTests.checkMicroGood((events) => + (events.length == 1) && + (events[0]['event']['se_category'] == 'category') && + (events[0]['event']['se_action'] == 'action')), + isTrue); + }); + + testWidgets("tracks a self-describing event", (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + const selfDescribing = SelfDescribing( + schema: 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1', + data: {'targetUrl': 'http://a-target-url.com'}, + ); + await Snowplow.track(selfDescribing, tracker: "test"); + + expect( + await SnowplowTests.checkMicroCounts( + (body) => (body['good'] == 1) && (body['bad'] == 0)), + isTrue); + expect( + await SnowplowTests.checkMicroGood((events) => + (events.length == 1) && + (events[0]['event']['unstruct_event']['data']['schema'] == + 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1') && + (events[0]['event']['unstruct_event']['data']['data'] + ['targetUrl'] == + 'http://a-target-url.com')), + isTrue); + }); + + testWidgets("tracks a screen view event", (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + String id = const Uuid().v4(); + + var screenView = ScreenView(id: id, name: 'name'); + await Snowplow.track(screenView, tracker: 'test'); + + expect( + await SnowplowTests.checkMicroGood((events) => + (events.length == 1) && + (events[0]['event']['unstruct_event']['data']['data']['id'] + .toLowerCase() == + id.toLowerCase()) && + (events[0]['event']['unstruct_event']['data']['data']['name'] == + 'name')), + isTrue); + }); + + testWidgets("tracks a timing event", (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + var timing = + const Timing(category: 'cat', variable: 'var', timing: 10, label: 'l'); + await Snowplow.track(timing, tracker: 'test'); + + expect( + await SnowplowTests.checkMicroGood((events) => + (events.length == 1) && + (events[0]['event']['unstruct_event']['data']['data']['category'] == + 'cat') && + (events[0]['event']['unstruct_event']['data']['data']['timing'] == + 10)), + isTrue); + }); + + testWidgets("tracks a consent granted event", (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + final consentGranted = ConsentGranted( + expiry: DateTime.parse('2021-12-30T09:03:51.196Z'), + documentId: '1234', + version: '5', + name: 'name1', + documentDescription: 'description1', + ); + await Snowplow.track(consentGranted, tracker: 'test'); + + expect( + await SnowplowTests.checkMicroGood((events) => + (events.length == 1) && + (events[0]['event']['unstruct_event']['data']['data']['expiry'] == + '2021-12-30T09:03:51.196Z')), + isTrue); + }); + + testWidgets("tracks a consent withdrawn event", (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + const consentWithdrawn = ConsentWithdrawn( + all: false, + documentId: '1234', + version: '5', + name: 'name1', + documentDescription: 'description1', + ); + await Snowplow.track(consentWithdrawn, tracker: 'test'); + + expect( + await SnowplowTests.checkMicroGood((events) => + (events.length == 1) && + (events[0]['event']['unstruct_event']['data']['data']['all'] == + false)), + isTrue); + }); + + testWidgets("tracks an event with custom context", + (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + await Snowplow.track( + const Structured(category: 'category', action: 'action'), + tracker: "test", + contexts: [ + const SelfDescribing( + schema: + 'iglu:com.snowplowanalytics.snowplow/media_player/jsonschema/1-0-0', + data: { + 'currentTime': 0, + 'duration': 10, + 'ended': false, + 'loop': false, + 'muted': false, + 'paused': false, + 'playbackRate': 1, + 'volume': 100 + }) + ]); + + expect( + await SnowplowTests.checkMicroGood((dynamic events) { + if (events.length != 1) { + return false; + } + dynamic context = events[0]['event']['contexts']['data'].firstWhere( + (x) => x['schema'].toString().contains('media_player')); + return context['data']['volume'] == 100; + }), + isTrue); + }); +} diff --git a/example/integration_test/helpers.dart b/example/integration_test/helpers.dart new file mode 100644 index 0000000..41a0acf --- /dev/null +++ b/example/integration_test/helpers.dart @@ -0,0 +1,52 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:http/http.dart' as http; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:snowplow_tracker/snowplow.dart'; +import 'dart:convert'; + +class SnowplowTests { + static const microEndpoint = + String.fromEnvironment('ENDPOINT', defaultValue: 'http://0.0.0.0:9090'); + + static Future createTracker() async { + await Snowplow.createTracker(namespace: 'test', endpoint: microEndpoint); + } + + static Future resetMicro() async { + await http.get(Uri.parse(microEndpoint + '/micro/reset')); + await Future.delayed(const Duration(seconds: 1), () {}); + } + + static Future checkMicroCounts( + bool Function(dynamic body) validation) async { + return checkMicroResponse('/micro/all', validation); + } + + static Future checkMicroGood( + bool Function(dynamic body) validation) async { + return checkMicroResponse('/micro/good', validation); + } + + static Future checkMicroResponse( + String api, bool Function(dynamic body) validation) async { + for (int i = 0; i < 10; i++) { + final response = await http.get(Uri.parse(microEndpoint + api)); + if (validation(jsonDecode(response.body))) { + return true; + } + await Future.delayed(const Duration(seconds: 1), () {}); + } + return false; + } +} diff --git a/example/integration_test/session_test.dart b/example/integration_test/session_test.dart new file mode 100644 index 0000000..2393ef9 --- /dev/null +++ b/example/integration_test/session_test.dart @@ -0,0 +1,147 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:snowplow_tracker/snowplow.dart'; +import 'package:snowplow_tracker/events/structured.dart'; +import 'package:snowplow_tracker/configurations/tracker_configuration.dart'; +import 'package:snowplow_tracker/tracker.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'helpers.dart'; +import 'package:snowplow_tracker_example/main.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + await SnowplowTests.createTracker(); + }); + + setUp(() async { + await SnowplowTests.resetMicro(); + }); + + testWidgets("maintains the same session context", + (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + await Snowplow.track( + const Structured(category: 'category', action: 'action'), + tracker: "test"); + + dynamic clientSession1; + expect( + await SnowplowTests.checkMicroGood((dynamic events) { + if (events.length == 0) { + return false; + } + Iterable sessions = events[0]['event']['contexts']['data'] + .where((x) => x['schema'].toString().contains('client_session')); + if (sessions.length != 1) { + return false; + } + clientSession1 = sessions.first; + return true; + }), + isTrue); + + expect(clientSession1['data']['firstEventId'], isNotNull); + expect(clientSession1['data']['sessionId'], isNotNull); + expect(clientSession1['data']['userId'], isNotNull); + + await SnowplowTests.resetMicro(); + + await Snowplow.track( + const Structured(category: 'category', action: 'action'), + tracker: "test"); + + dynamic clientSession2; + expect( + await SnowplowTests.checkMicroGood((dynamic events) { + if (events.length == 0) { + return false; + } + Iterable sessions = events[0]['event']['contexts']['data'] + .where((x) => x['schema'].toString().contains('client_session')); + if (sessions.length != 1) { + return false; + } + clientSession2 = sessions.first; + return true; + }), + isTrue); + + expect(clientSession1['data']['firstEventId'], + equals(clientSession2['data']['firstEventId'])); + expect(clientSession1['data']['sessionId'], + equals(clientSession2['data']['sessionId'])); + expect(clientSession1['data']['userId'], + equals(clientSession2['data']['userId'])); + }); + + testWidgets("tracks the same session information as returned from API", + (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + await Snowplow.track( + const Structured(category: 'category', action: 'action'), + tracker: "test"); + + dynamic clientSession; + expect( + await SnowplowTests.checkMicroGood((dynamic events) { + if (events.length != 1) { + return false; + } + Iterable sessions = events[0]['event']['contexts']['data'] + .where((x) => x['schema'].toString().contains('client_session')); + if (sessions.length != 1) { + return false; + } + clientSession = sessions.first; + return true; + }), + isTrue); + + String? sessionId = await Snowplow.getSessionId(tracker: 'test'); + String? sessionUserId = await Snowplow.getSessionUserId(tracker: 'test'); + int? sessionIndex = await Snowplow.getSessionIndex(tracker: 'test'); + + expect(clientSession['data']['sessionId'], equals(sessionId)); + expect(clientSession['data']['userId'], equals(sessionUserId)); + expect(clientSession['data']['sessionIndex'], equals(sessionIndex)); + }); + + testWidgets("doesn't add session context when disabled", + (WidgetTester tester) async { + await tester.pumpWidget(const MyApp(testing: true)); + + Tracker tracker = await Snowplow.createTracker( + namespace: 'test-without-session', + endpoint: SnowplowTests.microEndpoint, + trackerConfig: const TrackerConfiguration(sessionContext: false)); + + await tracker + .track(const Structured(category: 'category', action: 'action')); + + expect( + await SnowplowTests.checkMicroGood((dynamic events) { + if (events.length != 1) { + return false; + } + Iterable contexts = events[0]['event']['contexts']['data'] + .where((x) => x['schema'].toString().contains('client_session')); + return contexts.isEmpty; + }), + isTrue); + }); +} diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..8d4492f --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..1e8c3c9 --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..7872c5c --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,41 @@ +PODS: + - Flutter (1.0.0) + - FMDB (2.7.5): + - FMDB/standard (= 2.7.5) + - FMDB/standard (2.7.5) + - integration_test (0.0.1): + - Flutter + - snowplow_tracker (0.1.0-dev.1): + - Flutter + - SnowplowTracker (~> 3.0.2) + - SnowplowTracker (3.0.2): + - FMDB (~> 2.7) + +DEPENDENCIES: + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - snowplow_tracker (from `.symlinks/plugins/snowplow_tracker/ios`) + +SPEC REPOS: + trunk: + - FMDB + - SnowplowTracker + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + snowplow_tracker: + :path: ".symlinks/plugins/snowplow_tracker/ios" + +SPEC CHECKSUMS: + Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a + FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 + snowplow_tracker: d177e74163dea0ef1545e0033e752b09decef2c0 + SnowplowTracker: a96fd8819c86c56844f930af372d0f4f92c35472 + +PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c + +COCOAPODS: 1.11.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..3fb93fc --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,554 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 22453CEABDA43B9EFA837383 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C71A1876E9FDA558A0C1C3E2 /* Pods_Runner.framework */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 893CDD6FC73BDB021927FCB9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 8C4CAEE2FD470F85C2A25BBD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A41F2E2A082493BADA509FA5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + C71A1876E9FDA558A0C1C3E2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 22453CEABDA43B9EFA837383 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 34BA420BA5830C53E9965B6E /* Pods */ = { + isa = PBXGroup; + children = ( + 893CDD6FC73BDB021927FCB9 /* Pods-Runner.debug.xcconfig */, + 8C4CAEE2FD470F85C2A25BBD /* Pods-Runner.release.xcconfig */, + A41F2E2A082493BADA509FA5 /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 4BB2C4240AA52FA81EEFEE21 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C71A1876E9FDA558A0C1C3E2 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 34BA420BA5830C53E9965B6E /* Pods */, + 4BB2C4240AA52FA81EEFEE21 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + DD3E2067043D51B3CB645EC8 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 4887ED9A4353D76AB18B67A1 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 4887ED9A4353D76AB18B67A1 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + DD3E2067043D51B3CB645EC8 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = V487J8L84E; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.snowplowanalytics.snowplowTrackerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = V487J8L84E; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.snowplowanalytics.snowplowTrackerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = V487J8L84E; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.snowplowanalytics.snowplowTrackerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c87d15a --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 zcmeHN2~<R zh`|8`A_PQ1nSu(UMFx?8j8PC!!VDphaL#`F42fd#7Vlc`zIE4n%Y~eiz4y1j|NDpi z?<@|pSJ-HM`qifhf@m%MamgwK83`XpBA<+azdF#2QsT{X@z0A9Bq>~TVErigKH1~P zRX-!h-f0NJ4Mh++{D}J+K>~~rq}d%o%+4dogzXp7RxX4C>Km5XEI|PAFDmo;DFm6G zzjVoB`@qW98Yl0Kvc-9w09^PrsobmG*Eju^=3f?0o-t$U)TL1B3;sZ^!++3&bGZ!o-*6w?;oOhf z=A+Qb$scV5!RbG+&2S}BQ6YH!FKb0``VVX~T$dzzeSZ$&9=X$3)_7Z{SspSYJ!lGE z7yig_41zpQ)%5dr4ff0rh$@ky3-JLRk&DK)NEIHecf9c*?Z1bUB4%pZjQ7hD!A0r-@NF(^WKdr(LXj|=UE7?gBYGgGQV zidf2`ZT@pzXf7}!NH4q(0IMcxsUGDih(0{kRSez&z?CFA0RVXsVFw3^u=^KMtt95q z43q$b*6#uQDLoiCAF_{RFc{!H^moH_cmll#Fc^KXi{9GDl{>%+3qyfOE5;Zq|6#Hb zp^#1G+z^AXfRKaa9HK;%b3Ux~U@q?xg<2DXP%6k!3E)PA<#4$ui8eDy5|9hA5&{?v z(-;*1%(1~-NTQ`Is1_MGdQ{+i*ccd96ab$R$T3=% zw_KuNF@vI!A>>Y_2pl9L{9h1-C6H8<)J4gKI6{WzGBi<@u3P6hNsXG=bRq5c+z;Gc3VUCe;LIIFDmQAGy+=mRyF++u=drBWV8-^>0yE9N&*05XHZpPlE zxu@?8(ZNy7rm?|<+UNe0Vs6&o?l`Pt>P&WaL~M&#Eh%`rg@Mbb)J&@DA-wheQ>hRV z<(XhigZAT z>=M;URcdCaiO3d^?H<^EiEMDV+7HsTiOhoaMX%P65E<(5xMPJKxf!0u>U~uVqnPN7T!X!o@_gs3Ct1 zlZ_$5QXP4{Aj645wG_SNT&6m|O6~Tsl$q?nK*)(`{J4b=(yb^nOATtF1_aS978$x3 zx>Q@s4i3~IT*+l{@dx~Hst21fR*+5}S1@cf>&8*uLw-0^zK(+OpW?cS-YG1QBZ5q! zgTAgivzoF#`cSz&HL>Ti!!v#?36I1*l^mkrx7Y|K6L#n!-~5=d3;K<;Zqi|gpNUn_ z_^GaQDEQ*jfzh;`j&KXb66fWEk1K7vxQIMQ_#Wu_%3 z4Oeb7FJ`8I>Px;^S?)}2+4D_83gHEq>8qSQY0PVP?o)zAv3K~;R$fnwTmI-=ZLK`= zTm+0h*e+Yfr(IlH3i7gUclNH^!MU>id$Jw>O?2i0Cila#v|twub21@e{S2v}8Z13( zNDrTXZVgris|qYm<0NU(tAPouG!QF4ZNpZPkX~{tVf8xY690JqY1NVdiTtW+NqyRP zZ&;T0ikb8V{wxmFhlLTQ&?OP7 z;(z*<+?J2~z*6asSe7h`$8~Se(@t(#%?BGLVs$p``;CyvcT?7Y!{tIPva$LxCQ&4W z6v#F*);|RXvI%qnoOY&i4S*EL&h%hP3O zLsrFZhv&Hu5tF$Lx!8(hs&?!Kx5&L(fdu}UI5d*wn~A`nPUhG&Rv z2#ixiJdhSF-K2tpVL=)5UkXRuPAFrEW}7mW=uAmtVQ&pGE-&az6@#-(Te^n*lrH^m@X-ftVcwO_#7{WI)5v(?>uC9GG{lcGXYJ~Q8q zbMFl7;t+kV;|;KkBW2!P_o%Czhw&Q(nXlxK9ak&6r5t_KH8#1Mr-*0}2h8R9XNkr zto5-b7P_auqTJb(TJlmJ9xreA=6d=d)CVbYP-r4$hDn5|TIhB>SReMfh&OVLkMk-T zYf%$taLF0OqYF?V{+6Xkn>iX@TuqQ?&cN6UjC9YF&%q{Ut3zv{U2)~$>-3;Dp)*(? zg*$mu8^i=-e#acaj*T$pNowo{xiGEk$%DusaQiS!KjJH96XZ-hXv+jk%ard#fu=@Q z$AM)YWvE^{%tDfK%nD49=PI|wYu}lYVbB#a7wtN^Nml@CE@{Gv7+jo{_V?I*jkdLD zJE|jfdrmVbkfS>rN*+`#l%ZUi5_bMS<>=MBDNlpiSb_tAF|Zy`K7kcp@|d?yaTmB^ zo?(vg;B$vxS|SszusORgDg-*Uitzdi{dUV+glA~R8V(?`3GZIl^egW{a919!j#>f` znL1o_^-b`}xnU0+~KIFLQ)$Q6#ym%)(GYC`^XM*{g zv3AM5$+TtDRs%`2TyR^$(hqE7Y1b&`Jd6dS6B#hDVbJlUXcG3y*439D8MrK!2D~6gn>UD4Imctb z+IvAt0iaW73Iq$K?4}H`7wq6YkTMm`tcktXgK0lKPmh=>h+l}Y+pDtvHnG>uqBA)l zAH6BV4F}v$(o$8Gfo*PB>IuaY1*^*`OTx4|hM8jZ?B6HY;F6p4{`OcZZ(us-RVwDx zUzJrCQlp@mz1ZFiSZ*$yX3c_#h9J;yBE$2g%xjmGF4ca z&yL`nGVs!Zxsh^j6i%$a*I3ZD2SoNT`{D%mU=LKaEwbN(_J5%i-6Va?@*>=3(dQy` zOv%$_9lcy9+(t>qohkuU4r_P=R^6ME+wFu&LA9tw9RA?azGhjrVJKy&8=*qZT5Dr8g--d+S8zAyJ$1HlW3Olryt`yE zFIph~Z6oF&o64rw{>lgZISC6p^CBer9C5G6yq%?8tC+)7*d+ib^?fU!JRFxynRLEZ zj;?PwtS}Ao#9whV@KEmwQgM0TVP{hs>dg(1*DiMUOKHdQGIqa0`yZnHk9mtbPfoLx zo;^V6pKUJ!5#n`w2D&381#5#_t}AlTGEgDz$^;u;-vxDN?^#5!zN9ngytY@oTv!nc zp1Xn8uR$1Z;7vY`-<*?DfPHB;x|GUi_fI9@I9SVRv1)qETbNU_8{5U|(>Du84qP#7 z*l9Y$SgA&wGbj>R1YeT9vYjZuC@|{rajTL0f%N@>3$DFU=`lSPl=Iv;EjuGjBa$Gw zHD-;%YOE@<-!7-Mn`0WuO3oWuL6tB2cpPw~Nvuj|KM@))ixuDK`9;jGMe2d)7gHin zS<>k@!x;!TJEc#HdL#RF(`|4W+H88d4V%zlh(7#{q2d0OQX9*FW^`^_<3r$kabWAB z$9BONo5}*(%kx zOXi-yM_cmB3>inPpI~)duvZykJ@^^aWzQ=eQ&STUa}2uT@lV&WoRzkUoE`rR0)`=l zFT%f|LA9fCw>`enm$p7W^E@U7RNBtsh{_-7vVz3DtB*y#*~(L9+x9*wn8VjWw|Q~q zKFsj1Yl>;}%MG3=PY`$g$_mnyhuV&~O~u~)968$0b2!Jkd;2MtAP#ZDYw9hmK_+M$ zb3pxyYC&|CuAbtiG8HZjj?MZJBFbt`ryf+c1dXFuC z0*ZQhBzNBd*}s6K_G}(|Z_9NDV162#y%WSNe|FTDDhx)K!c(mMJh@h87@8(^YdK$&d*^WQe8Z53 z(|@MRJ$Lk-&ii74MPIs80WsOFZ(NX23oR-?As+*aq6b?~62@fSVmM-_*cb1RzZ)`5$agEiL`-E9s7{GM2?(KNPgK1(+c*|-FKoy}X(D_b#etO|YR z(BGZ)0Ntfv-7R4GHoXp?l5g#*={S1{u-QzxCGng*oWr~@X-5f~RA14b8~B+pLKvr4 zfgL|7I>jlak9>D4=(i(cqYf7#318!OSR=^`xxvI!bBlS??`xxWeg?+|>MxaIdH1U~#1tHu zB{QMR?EGRmQ_l4p6YXJ{o(hh-7Tdm>TAX380TZZZyVkqHNzjUn*_|cb?T? zt;d2s-?B#Mc>T-gvBmQZx(y_cfkXZO~{N zT6rP7SD6g~n9QJ)8F*8uHxTLCAZ{l1Y&?6v)BOJZ)=R-pY=Y=&1}jE7fQ>USS}xP#exo57uND0i*rEk@$;nLvRB@u~s^dwRf?G?_enN@$t* zbL%JO=rV(3Ju8#GqUpeE3l_Wu1lN9Y{D4uaUe`g>zlj$1ER$6S6@{m1!~V|bYkhZA z%CvrDRTkHuajMU8;&RZ&itnC~iYLW4DVkP<$}>#&(`UO>!n)Po;Mt(SY8Yb`AS9lt znbX^i?Oe9r_o=?})IHKHoQGKXsps_SE{hwrg?6dMI|^+$CeC&z@*LuF+P`7LfZ*yr+KN8B4{Nzv<`A(wyR@!|gw{zB6Ha ziwPAYh)oJ(nlqSknu(8g9N&1hu0$vFK$W#mp%>X~AU1ay+EKWcFdif{% z#4!4aoVVJ;ULmkQf!ke2}3hqxLK>eq|-d7Ly7-J9zMpT`?dxo6HdfJA|t)?qPEVBDv z{y_b?4^|YA4%WW0VZd8C(ZgQzRI5(I^)=Ub`Y#MHc@nv0w-DaJAqsbEHDWG8Ia6ju zo-iyr*sq((gEwCC&^TYBWt4_@|81?=B-?#P6NMff(*^re zYqvDuO`K@`mjm_Jd;mW_tP`3$cS?R$jR1ZN09$YO%_iBqh5ftzSpMQQtxKFU=FYmP zeY^jph+g<4>YO;U^O>-NFLn~-RqlHvnZl2yd2A{Yc1G@Ga$d+Q&(f^tnPf+Z7serIU};17+2DU_f4Z z@GaPFut27d?!YiD+QP@)T=77cR9~MK@bd~pY%X(h%L={{OIb8IQmf-!xmZkm8A0Ga zQSWONI17_ru5wpHg3jI@i9D+_Y|pCqVuHJNdHUauTD=R$JcD2K_liQisqG$(sm=k9;L* z!L?*4B~ql7uioSX$zWJ?;q-SWXRFhz2Jt4%fOHA=Bwf|RzhwqdXGr78y$J)LR7&3T zE1WWz*>GPWKZ0%|@%6=fyx)5rzUpI;bCj>3RKzNG_1w$fIFCZ&UR0(7S?g}`&Pg$M zf`SLsz8wK82Vyj7;RyKmY{a8G{2BHG%w!^T|Njr!h9TO2LaP^_f22Q1=l$QiU84ao zHe_#{S6;qrC6w~7{y(hs-?-j?lbOfgH^E=XcSgnwW*eEz{_Z<_Px$?ny*JR5%f>l)FnDQ543{x%ZCiu33$Wg!pQFfT_}?5Q|_VSlIbLC`dpoMXL}9 zHfd9&47Mo(7D231gb+kjFxZHS4-m~7WurTH&doVX2KI5sU4v(sJ1@T9eCIKPjsqSr z)C01LsCxk=72-vXmX}CQD#BD;Cthymh&~=f$Q8nn0J<}ZrusBy4PvRNE}+1ceuj8u z0mW5k8fmgeLnTbWHGwfKA3@PdZxhn|PypR&^p?weGftrtCbjF#+zk_5BJh7;0`#Wr zgDpM_;Ax{jO##IrT`Oz;MvfwGfV$zD#c2xckpcXC6oou4ML~ezCc2EtnsQTB4tWNg z?4bkf;hG7IMfhgNI(FV5Gs4|*GyMTIY0$B=_*mso9Ityq$m^S>15>-?0(zQ<8Qy<_TjHE33(?_M8oaM zyc;NxzRVK@DL6RJnX%U^xW0Gpg(lXp(!uK1v0YgHjs^ZXSQ|m#lV7ip7{`C_J2TxPmfw%h$|%acrYHt)Re^PB%O&&=~a zhS(%I#+V>J-vjIib^<+s%ludY7y^C(P8nmqn9fp!i+?vr`bziDE=bx`%2W#Xyrj|i z!XQ4v1%L`m{7KT7q+LZNB^h8Ha2e=`Wp65^0;J00)_^G=au=8Yo;1b`CV&@#=jIBo zjN^JNVfYSs)+kDdGe7`1&8!?MQYKS?DuHZf3iogk_%#9E|5S zWeHrmAo>P;ejX7mwq#*}W25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+ zX$F_KMdb6sRz!~7KkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&Q->Na@Xb&u5Q3`3DGf+a8O5x7c#7+R+EAYl@R5us)CIw z7sT@_y~Ao@uL#&^LIh&QceqiT^+lb0YbFZt_SHOtWA%mgPEKVNvVgCsXy{5+zl*X8 zCJe)Q@y>wH^>l4;h1l^Y*9%-23TSmE>q5nI@?mt%n;Sj4Qq`Z+ib)a*a^cJc%E9^J zB;4s+K@rARbcBLT5P=@r;IVnBMKvT*)ew*R;&8vu%?Z&S>s?8?)3*YawM0P4!q$Kv zMmKh3lgE~&w&v%wVzH3Oe=jeNT=n@Y6J6TdHWTjXfX~-=1A1Bw`EW8rn}MqeI34nh zexFeA?&C3B2(E?0{drE@DA2pu(A#ElY&6el60Rn|Qpn-FkfQ8M93AfWIr)drgDFEU zghdWK)^71EWCP(@(=c4kfH1Y(4iugD4fve6;nSUpLT%!)MUHs1!zJYy4y||C+SwQ! z)KM&$7_tyM`sljP2fz6&Z;jxRn{Wup8IOUx8D4uh&(=O zx-7$a;U><*5L^!%xRlw)vAbh;sdlR||& ze}8_8%)c2Fwy=F&H|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}JeGuMZk~LPi7{cidvUGB zAJ4LVeNV%XO>LTrklB#^-;8nb;}6l;1oW&WS=Mz*Az!4cqqQzbOSFq`$Q%PfD7srM zpKgP-D_0XPTRX*hAqeq0TDkJ;5HB1%$3Np)99#16c{ zJImlNL(npL!W|Gr_kxl1GVmF5&^$^YherS7+~q$p zt}{a=*RiD2Ikv6o=IM1kgc7zqpaZ;OB)P!1zz*i3{U()Dq#jG)egvK}@uFLa`oyWZ zf~=MV)|yJn`M^$N%ul5);JuQvaU1r2wt(}J_Qgyy`qWQI`hEeRX0uC@c1(dQ2}=U$ tNIIaX+dr)NRWXcxoR{>fqI{SF_dm1Ylv~=3YHI)h002ovPDHLkV1g(pWS;;4 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 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 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 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 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..09ac997 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..82d988f --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Snowplow Flutter Tracker + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + snowplow_tracker_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..48428ac --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,224 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'dart:async'; + +import 'package:snowplow_tracker/snowplow_tracker.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + final bool? testing; + const MyApp({Key? key, this.testing}) : super(key: key); + + @override + // ignore: no_logic_in_create_state + State createState() => _MyAppState(testing: testing); +} + +class _MyAppState extends State with WidgetsBindingObserver { + int _numberOfEventsSent = 0; + String _sessionId = 'Unknown'; + String _sessionUserId = 'Unknown'; + int? _sessionIndex; + final bool? testing; + + _MyAppState({this.testing}) : super(); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + Future initPlatformState() async { + if (testing == null || testing == false) { + await Snowplow.createTracker( + namespace: "ns1", + endpoint: const String.fromEnvironment('ENDPOINT', + defaultValue: 'http://0.0.0.0:9090'), + trackerConfig: const TrackerConfiguration(webPageContext: false), + gdprConfig: const GdprConfiguration( + basisForProcessing: 'consent', + documentId: 'consentDoc-abc123', + documentVersion: '0.1.0', + documentDescription: + 'this document describes consent basis for processing'), + subjectConfig: const SubjectConfiguration(userId: 'XYZ')); + // await Snowplow.setUserId('XYZ', tracker: 'ns1'); + updateState(); + } + + WidgetsBinding.instance?.addObserver(this); + } + + Future updateState() async { + String? sessionId; + String? sessionUserId; + int? sessionIndex; + + try { + sessionId = await Snowplow.getSessionId(tracker: 'ns1') ?? 'Unknown'; + sessionUserId = + await Snowplow.getSessionUserId(tracker: 'ns1') ?? 'Unknown'; + sessionIndex = await Snowplow.getSessionIndex(tracker: 'ns1'); + } on PlatformException catch (err) { + if (kDebugMode) { + print(err); + } + } + + if (!mounted) return; + + setState(() { + _sessionId = sessionId ?? 'Unknown'; + _sessionUserId = sessionUserId ?? 'Unknown'; + _sessionIndex = sessionIndex; + }); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + updateState(); + } + + Future trackEvent(event, {List? contexts}) async { + Snowplow.track(event, tracker: "ns1", contexts: contexts); + + setState(() { + _numberOfEventsSent += 1; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 24.0), + child: Center( + child: Column(children: [ + Text('Number of events sent: $_numberOfEventsSent'), + const SizedBox(height: 24.0), + ElevatedButton( + onPressed: () { + const structured = Structured( + category: 'shop', + action: 'add-to-basket', + label: 'Add To Basket', + property: 'pcs', + value: 2.00, + ); + trackEvent(structured); + }, + child: const Text('Send Structured Event'), + ), + ElevatedButton( + onPressed: () { + const event = SelfDescribing( + schema: + 'iglu:com.snowplowanalytics.snowplow/link_click/jsonschema/1-0-1', + data: {'targetUrl': 'http://a-target-url.com'}, + ); + trackEvent(event); + }, + child: const Text('Send Self-Describing Event'), + ), + ElevatedButton( + onPressed: () { + const event = ScreenView( + id: '2c295365-eae9-4243-a3ee-5c4b7baccc8f', + name: 'home', + type: 'full', + transitionType: 'none'); + trackEvent(event); + }, + child: const Text('Send Screen View Event'), + ), + ElevatedButton( + onPressed: () { + const event = Timing( + category: 'category', + variable: 'variable', + timing: 1, + label: 'label', + ); + trackEvent(event); + }, + child: const Text('Send Timing Event'), + ), + ElevatedButton( + onPressed: () { + final event = ConsentGranted( + expiry: DateTime.parse('2021-12-30T09:03:51.196111Z'), + documentId: '1234', + version: '5', + name: 'name1', + documentDescription: 'description1', + ); + trackEvent(event); + }, + child: const Text('Send Consent Granted Event'), + ), + ElevatedButton( + onPressed: () { + const event = ConsentWithdrawn( + all: false, + documentId: '1234', + version: '5', + name: 'name1', + documentDescription: 'description1', + ); + trackEvent(event); + }, + child: const Text('Send Consent Withdrawn Event'), + ), + ElevatedButton( + onPressed: () { + const structured = Structured( + category: 'shop', + action: 'add-to-basket', + label: 'Add To Basket', + property: 'pcs', + value: 2.00, + ); + trackEvent(structured, contexts: [ + const SelfDescribing( + schema: 'iglu:org.schema/WebPage/jsonschema/1-0-0', + data: { + 'keywords': ['tester'] + }) + ]); + }, + child: const Text('Send Structured Event With Context'), + ), + const SizedBox(height: 24.0), + Text('Session ID: $_sessionId'), + const SizedBox(height: 5.0), + Text('Session user ID: $_sessionUserId'), + const SizedBox(height: 5.0), + Text('Session index: $_sessionIndex'), + const SizedBox(height: 5.0) + ]), + ), + ), + ), + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..9015876 --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,279 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.6" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: transitive + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: "direct dev" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.3" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + snowplow_tracker: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.1.0-dev.1" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + sync_http: + dependency: transitive + description: + name: sync_http + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + uuid: + dependency: "direct main" + description: + name: uuid + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.5" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + vm_service: + dependency: transitive + description: + name: vm_service + url: "https://pub.dartlang.org" + source: hosted + version: "7.3.0" + webdriver: + dependency: transitive + description: + name: webdriver + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" +sdks: + dart: ">=2.15.0 <3.0.0" + flutter: ">=2.8.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..1844990 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,88 @@ +name: snowplow_tracker_example +description: Demonstrates how to use the snowplow_tracker plugin. + +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.15.0 <3.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + uuid: ^3.0.5 + + snowplow_tracker: + # When depending on this package from a real application you should use: + # snowplow_tracker: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + http: ^0.13.3 + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/example/test/.gitkeep b/example/test/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/example/test_driver/integration_test.dart b/example/test_driver/integration_test.dart new file mode 100644 index 0000000..8e45d6e --- /dev/null +++ b/example/test_driver/integration_test.dart @@ -0,0 +1,14 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/example/tool/e2e_tests.sh b/example/tool/e2e_tests.sh new file mode 100755 index 0000000..2aacbae --- /dev/null +++ b/example/tool/e2e_tests.sh @@ -0,0 +1,8 @@ + +#!/bin/sh + +set -e + +flutter drive --driver test_driver/integration_test.dart --target integration_test/configuration_test.dart --dart-define=ENDPOINT=$1 $2 +flutter drive --driver test_driver/integration_test.dart --target integration_test/events_test.dart --dart-define=ENDPOINT=$1 $2 +flutter drive --driver test_driver/integration_test.dart --target integration_test/session_test.dart --dart-define=ENDPOINT=$1 $2 diff --git a/example/tool/iglu.json b/example/tool/iglu.json new file mode 100644 index 0000000..7601844 --- /dev/null +++ b/example/tool/iglu.json @@ -0,0 +1,20 @@ +{ + "schema": "iglu:com.snowplowanalytics.iglu/resolver-config/jsonschema/1-0-0", + "data": { + "cacheSize": 500, + "repositories": [ + { + "name": "Iglu Central", + "priority": 0, + "vendorPrefixes": [ + "com.snowplowanalytics" + ], + "connection": { + "http": { + "uri": "http://iglucentral.com" + } + } + } + ] + } +} diff --git a/example/tool/micro.conf b/example/tool/micro.conf new file mode 100644 index 0000000..51fa0a1 --- /dev/null +++ b/example/tool/micro.conf @@ -0,0 +1,231 @@ +# 'collector' contains configuration options for the main Scala collector. +collector { + # The collector runs as a web service specified on the following interface and port. + interface = "0.0.0.0" + port = "9090" + + # optional SSL/TLS configuration + ssl { + enable = false + # whether to redirect HTTP to HTTPS + redirect = false + port = 9543 + } + + # The collector responds with a cookie to requests with a path that matches the 'vendor/version' protocol. + # The expected values are: + # - com.snowplowanalytics.snowplow/tp2 for Tracker Protocol 2 + # - r/tp2 for redirects + # - com.snowplowanalytics.iglu/v1 for the Iglu Webhook + # Any path that matches the 'vendor/version' protocol will result in a cookie response, for use by custom webhooks + # downstream of the collector. + # But you can also map any valid (i.e. two-segment) path to one of the three defaults. + # Your custom path must be the key and the value must be one of the corresponding default paths. Both must be full + # valid paths starting with a leading slash. + # Pass in an empty map to avoid mapping. + paths { + # "/com.acme/track" = "/com.snowplowanalytics.snowplow/tp2" + # "/com.acme/redirect" = "/r/tp2" + # "/com.acme/iglu" = "/com.snowplowanalytics.iglu/v1" + } + + # Configure the P3P policy header. + p3p { + policyRef = "/w3c/p3p.xml" + CP = "NOI DSP COR NID PSA OUR IND COM NAV STA" + } + + # Cross domain policy configuration. + # If "enabled" is set to "false", the collector will respond with a 404 to the /crossdomain.xml + # route. + crossDomain { + enabled = false + # Domains that are granted access, *.acme.com will match http://acme.com and http://sub.acme.com + domains = [ "*" ] + # Whether to only grant access to HTTPS or both HTTPS and HTTP sources + secure = true + } + + # The collector returns a cookie to clients for user identification + # with the following domain and expiration. + cookie { + enabled = true + expiration = "365 days" + # Network cookie name + name = "micro" + # The domain is optional and will make the cookie accessible to other + # applications on the domain. Comment out these lines to tie cookies to + # the collector's full domain. + # The domain is determined by matching the domains from the Origin header of the request + # to the list below. The first match is used. If no matches are found, the fallback domain will be used, + # if configured. + # If you specify a main domain, all subdomains on it will be matched. + # If you specify a subdomain, only that subdomain will be matched. + # Examples: + # domain.com will match domain.com, www.domain.com and secure.client.domain.com + # client.domain.com will match secure.client.domain.com but not domain.com or www.domain.com + domains = [ + # "{{cookieDomain1}}" # e.g. "domain.com" -> any origin domain ending with this will be matched and domain.com will be returned + # "{{cookieDomain2}}" # e.g. "secure.anotherdomain.com" -> any origin domain ending with this will be matched and secure.anotherdomain.com will be returned + # ... more domains + ] + # ... more domains + # If specified, the fallback domain will be used if none of the Origin header hosts matches the list of + # cookie domains configured above. (For example, if there is no Origin header.) + # fallback-domain = "{{fallbackDomain}}" + secure = false + httpOnly = false + # The sameSite is optional. You can choose to not specify the attribute, or you can use `Strict`, + # `Lax` or `None` to limit the cookie sent context. + # Strict: the cookie will only be sent along with "same-site" requests. + # Lax: the cookie will be sent with same-site requests, and with cross-site top-level navigation. + # None: the cookie will be sent with same-site and cross-site requests. + # sameSite = "{{cookieSameSite}}" + } + + # If you have a do not track cookie in place, the Scala Stream Collector can respect it by + # completely bypassing the processing of an incoming request carrying this cookie, the collector + # will simply reply by a 200 saying "do not track". + # The cookie name and value must match the configuration below, where the names of the cookies must + # match entirely and the value could be a regular expression. + doNotTrackCookie { + enabled = false + name = "foo" + value = "bar" + } + + # When enabled and the cookie specified above is missing, performs a redirect to itself to check + # if third-party cookies are blocked using the specified name. If they are indeed blocked, + # fallbackNetworkId is used instead of generating a new random one. + cookieBounce { + enabled = false + # The name of the request parameter which will be used on redirects checking that third-party + # cookies work. + name = "n3pc" + # Network user id to fallback to when third-party cookies are blocked. + fallbackNetworkUserId = "00000000-0000-4000-A000-000000000000" + # Optionally, specify the name of the header containing the originating protocol for use in the + # bounce redirect location. Use this if behind a load balancer that performs SSL termination. + # The value of this header must be http or https. Example, if behind an AWS Classic ELB. + # forwardedProtocolHeader = "X-Forwarded-Proto" + } + + # When enabled, redirect prefix `r/` will be enabled and its query parameters resolved. + # Otherwise the request prefixed with `r/` will be dropped with `404 Not Found` + # Custom redirects configured in `paths` can still be used. + # enableDefaultRedirect = true + + # When enabled, the redirect url passed via the `u` query parameter is scanned for a placeholder + # token. All instances of that token are replaced withe the network ID. If the placeholder isn't + # specified, the default value is `${SP_NUID}`. + redirectMacro { + enabled = false + # Optional custom placeholder token (defaults to the literal `${SP_NUID}`) + placeholder = "[TOKEN]" + } + + # Customize response handling for requests for the root path ("/"). + # Useful if you need to redirect to web content or privacy policies regarding the use of this collector. + rootResponse { + enabled = false + statusCode = 302 + # Optional, defaults to empty map + headers = { + Location = "https://127.0.0.1/", + X-Custom = "something" + } + # Optional, defaults to empty string + body = "302, redirecting" + } + + # Configuration related to CORS preflight requests + cors { + # The Access-Control-Max-Age response header indicates how long the results of a preflight + # request can be cached. -1 seconds disables the cache. Chromium max is 10m, Firefox is 24h. + accessControlMaxAge = 5 seconds + } + + # Configuration of prometheus http metrics + prometheusMetrics { + # If metrics are enabled then all requests will be logged as prometheus metrics + # and '/metrics' endpoint will return the report about the requests + enabled = false + # Custom buckets for http_request_duration_seconds_bucket duration metric + #durationBucketsInSeconds = [0.1, 3, 10] + } + + streams { + # Events which have successfully been collected will be stored in the good stream/topic + good = "" + + # Events that are too big (w.r.t Kinesis 1MB limit) will be stored in the bad stream/topic + bad = "" + + # Whether to use the incoming event's ip as the partition key for the good stream/topic + # Note: Nsq does not make use of partition key. + useIpAddressAsPartitionKey = false + + # Enable the chosen sink by uncommenting the appropriate configuration + sink { + # Choose between kinesis, googlepubsub, kafka, nsq, or stdout. + # To use stdout, comment or remove everything in the "collector.streams.sink" section except + # "enabled" which should be set to "stdout". + enabled = stdout + + } + + # Incoming events are stored in a buffer before being sent to Kinesis/Kafka. + # Note: Buffering is not supported by NSQ. + # The buffer is emptied whenever: + # - the number of stored records reaches record-limit or + # - the combined size of the stored records reaches byte-limit or + # - the time in milliseconds since the buffer was last emptied reaches time-limit + buffer { + byteLimit = 100000 + recordLimit = 40 + timeLimit = 1000 + } + } +} + +# Akka has a variety of possible configuration options defined at +# http://doc.akka.io/docs/akka/current/scala/general/configuration.html +akka { + loglevel = DEBUG # 'OFF' for no logging, 'DEBUG' for all logging. + loggers = ["akka.event.slf4j.Slf4jLogger"] + + # akka-http is the server the Stream collector uses and has configurable options defined at + # http://doc.akka.io/docs/akka-http/current/scala/http/configuration.html + http.server { + # To obtain the hostname in the collector, the 'remote-address' header + # should be set. By default, this is disabled, and enabling it + # adds the 'Remote-Address' header to every request automatically. + remote-address-header = on + + raw-request-uri-header = on + + # Define the maximum request length (the default is 2048) + parsing { + max-uri-length = 32768 + uri-parsing-mode = relaxed + } + } + + # By default setting `collector.ssl` relies on JSSE (Java Secure Socket + # Extension) to enable secure communication. + # To override the default settings set the following section as per + # https://lightbend.github.io/ssl-config/ExampleSSLConfig.html + # ssl-config { + # debug = { + # ssl = true + # } + # keyManager = { + # stores = [ + # {type = "PKCS12", classpath = false, path = "/etc/ssl/mycert.p12", password = "mypassword" } + # ] + # } + # loose { + # disableHostnameVerification = false + # } + # } +} diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0X7 zltGxWVyS%@P(fs7NJL45ua8x7ey(0(N`6wRUPW#JP&EUCO@$SZnVVXYs8ErclUHn2 zVXFjIVFhG^g!Ppaz)DK8ZIvQ?0~DO|i&7O#^-S~(l1AfjnEK zjFOT9D}DX)@^Za$W4-*MbbUihOG|wNBYh(yU7!lx;>x^|#0uTKVr7USFmqf|i<65o z3raHc^AtelCMM;Vme?vOfh>Xph&xL%(-1c06+^uR^q@XSM&D4+Kp$>4P^%3{)XKjo zGZknv$b36P8?Z_gF{nK@`XI}Z90TzwSQO}0J1!f2c(B=V`5aP@1P1a|PZ!4!3&Gl8 zTYqUsf!gYFyJnXpu0!n&N*SYAX-%d(5gVjrHJWqXQshj@!Zm{!01WsQrH~9=kTxW#6SvuapgMqt>$=j#%eyGrQzr zP{L-3gsMA^$I1&gsBAEL+vxi1*Igl=8#8`5?A-T5=z-sk46WA1IUT)AIZHx1rdUrf zVJrJn<74DDw`j)Ki#gt}mIT-Q`XRa2-jQXQoI%w`nb|XblvzK${ZzlV)m-XcwC(od z71_OEC5Bt9GEXosOXaPTYOia#R4ID2TiU~`zVMl08TV_C%DnU4^+HE>9(CE4D6?Fz oujB08i7adh9xk7*FX66dWH6F5TM;?E2b5PlUHx3vIVCg!0Dx9vYXATM literal 0 HcmV?d00001 diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 zcmd5=2T+s!lYZ%-(h(2@5fr2dC?F^$C=i-}R6$UX8af(!je;W5yC_|HmujSgN*6?W z3knF*TL1$|?oD*=zPbBVex*RUIKsL<(&Rj9%^UD2IK3W?2j>D?eWQgvS-HLymHo9%~|N2Q{~j za?*X-{b9JRowv_*Mh|;*-kPFn>PI;r<#kFaxFqbn?aq|PduQg=2Q;~Qc}#z)_T%x9 zE|0!a70`58wjREmAH38H1)#gof)U3g9FZ^ zF7&-0^Hy{4XHWLoC*hOG(dg~2g6&?-wqcpf{ z&3=o8vw7lMi22jCG9RQbv8H}`+}9^zSk`nlR8?Z&G2dlDy$4#+WOlg;VHqzuE=fM@ z?OI6HEJH4&tA?FVG}9>jAnq_^tlw8NbjNhfqk2rQr?h(F&WiKy03Sn=-;ZJRh~JrD zbt)zLbnabttEZ>zUiu`N*u4sfQaLE8-WDn@tHp50uD(^r-}UsUUu)`!Rl1PozAc!a z?uj|2QDQ%oV-jxUJmJycySBINSKdX{kDYRS=+`HgR2GO19fg&lZKyBFbbXhQV~v~L za^U944F1_GtuFXtvDdDNDvp<`fqy);>Vw=ncy!NB85Tw{&sT5&Ox%-p%8fTS;OzlRBwErvO+ROe?{%q-Zge=%Up|D4L#>4K@Ke=x%?*^_^P*KD zgXueMiS63!sEw@fNLB-i^F|@Oib+S4bcy{eu&e}Xvb^(mA!=U=Xr3||IpV~3K zQWzEsUeX_qBe6fky#M zzOJm5b+l;~>=sdp%i}}0h zO?B?i*W;Ndn02Y0GUUPxERG`3Bjtj!NroLoYtyVdLtl?SE*CYpf4|_${ku2s`*_)k zN=a}V8_2R5QANlxsq!1BkT6$4>9=-Ix4As@FSS;1q^#TXPrBsw>hJ}$jZ{kUHoP+H zvoYiR39gX}2OHIBYCa~6ERRPJ#V}RIIZakUmuIoLF*{sO8rAUEB9|+A#C|@kw5>u0 zBd=F!4I)Be8ycH*)X1-VPiZ+Ts8_GB;YW&ZFFUo|Sw|x~ZajLsp+_3gv((Q#N>?Jz zFBf`~p_#^${zhPIIJY~yo!7$-xi2LK%3&RkFg}Ax)3+dFCjGgKv^1;lUzQlPo^E{K zmCnrwJ)NuSaJEmueEPO@(_6h3f5mFffhkU9r8A8(JC5eOkux{gPmx_$Uv&|hyj)gN zd>JP8l2U&81@1Hc>#*su2xd{)T`Yw< zN$dSLUN}dfx)Fu`NcY}TuZ)SdviT{JHaiYgP4~@`x{&h*Hd>c3K_To9BnQi@;tuoL z%PYQo&{|IsM)_>BrF1oB~+`2_uZQ48z9!)mtUR zdfKE+b*w8cPu;F6RYJiYyV;PRBbThqHBEu_(U{(gGtjM}Zi$pL8Whx}<JwE3RM0F8x7%!!s)UJVq|TVd#hf1zVLya$;mYp(^oZQ2>=ZXU1c$}f zm|7kfk>=4KoQoQ!2&SOW5|JP1)%#55C$M(u4%SP~tHa&M+=;YsW=v(Old9L3(j)`u z2?#fK&1vtS?G6aOt@E`gZ9*qCmyvc>Ma@Q8^I4y~f3gs7*d=ATlP>1S zyF=k&6p2;7dn^8?+!wZO5r~B+;@KXFEn^&C=6ma1J7Au6y29iMIxd7#iW%=iUzq&C=$aPLa^Q zncia$@TIy6UT@69=nbty5epP>*fVW@5qbUcb2~Gg75dNd{COFLdiz3}kODn^U*=@E z0*$7u7Rl2u)=%fk4m8EK1ctR!6%Ve`e!O20L$0LkM#f+)n9h^dn{n`T*^~d+l*Qlx z$;JC0P9+en2Wlxjwq#z^a6pdnD6fJM!GV7_%8%c)kc5LZs_G^qvw)&J#6WSp< zmsd~1-(GrgjC56Pdf6#!dt^y8Rg}!#UXf)W%~PeU+kU`FeSZHk)%sFv++#Dujk-~m zFHvVJC}UBn2jN& zs!@nZ?e(iyZPNo`p1i#~wsv9l@#Z|ag3JR>0#u1iW9M1RK1iF6-RbJ4KYg?B`dET9 zyR~DjZ>%_vWYm*Z9_+^~hJ_|SNTzBKx=U0l9 z9x(J96b{`R)UVQ$I`wTJ@$_}`)_DyUNOso6=WOmQKI1e`oyYy1C&%AQU<0-`(ow)1 zT}gYdwWdm4wW6|K)LcfMe&psE0XGhMy&xS`@vLi|1#Za{D6l@#D!?nW87wcscUZgELT{Cz**^;Zb~7 z(~WFRO`~!WvyZAW-8v!6n&j*PLm9NlN}BuUN}@E^TX*4Or#dMMF?V9KBeLSiLO4?B zcE3WNIa-H{ThrlCoN=XjOGk1dT=xwwrmt<1a)mrRzg{35`@C!T?&_;Q4Ce=5=>z^*zE_c(0*vWo2_#TD<2)pLXV$FlwP}Ik74IdDQU@yhkCr5h zn5aa>B7PWy5NQ!vf7@p_qtC*{dZ8zLS;JetPkHi>IvPjtJ#ThGQD|Lq#@vE2xdl%`x4A8xOln}BiQ92Po zW;0%A?I5CQ_O`@Ad=`2BLPPbBuPUp@Hb%a_OOI}y{Rwa<#h z5^6M}s7VzE)2&I*33pA>e71d78QpF>sNK;?lj^Kl#wU7G++`N_oL4QPd-iPqBhhs| z(uVM}$ItF-onXuuXO}o$t)emBO3Hjfyil@*+GF;9j?`&67GBM;TGkLHi>@)rkS4Nj zAEk;u)`jc4C$qN6WV2dVd#q}2X6nKt&X*}I@jP%Srs%%DS92lpDY^K*Sx4`l;aql$ zt*-V{U&$DM>pdO?%jt$t=vg5|p+Rw?SPaLW zB6nvZ69$ne4Z(s$3=Rf&RX8L9PWMV*S0@R zuIk&ba#s6sxVZ51^4Kon46X^9`?DC9mEhWB3f+o4#2EXFqy0(UTc>GU| zGCJmI|Dn-dX#7|_6(fT)>&YQ0H&&JX3cTvAq(a@ydM4>5Njnuere{J8p;3?1az60* z$1E7Yyxt^ytULeokgDnRVKQw9vzHg1>X@@jM$n$HBlveIrKP5-GJq%iWH#odVwV6cF^kKX(@#%%uQVb>#T6L^mC@)%SMd4DF? zVky!~ge27>cpUP1Vi}Z32lbLV+CQy+T5Wdmva6Fg^lKb!zrg|HPU=5Qu}k;4GVH+x z%;&pN1LOce0w@9i1Mo-Y|7|z}fbch@BPp2{&R-5{GLoeu8@limQmFF zaJRR|^;kW_nw~0V^ zfTnR!Ni*;-%oSHG1yItARs~uxra|O?YJxBzLjpeE-=~TO3Dn`JL5Gz;F~O1u3|FE- zvK2Vve`ylc`a}G`gpHg58Cqc9fMoy1L}7x7T>%~b&irrNMo?np3`q;d3d;zTK>nrK zOjPS{@&74-fA7j)8uT9~*g23uGnxwIVj9HorzUX#s0pcp2?GH6i}~+kv9fWChtPa_ z@T3m+$0pbjdQw7jcnHn;Pi85hk_u2-1^}c)LNvjdam8K-XJ+KgKQ%!?2n_!#{$H|| zLO=%;hRo6EDmnOBKCL9Cg~ETU##@u^W_5joZ%Et%X_n##%JDOcsO=0VL|Lkk!VdRJ z^|~2pB@PUspT?NOeO?=0Vb+fAGc!j%Ufn-cB`s2A~W{Zj{`wqWq_-w0wr@6VrM zbzni@8c>WS!7c&|ZR$cQ;`niRw{4kG#e z70e!uX8VmP23SuJ*)#(&R=;SxGAvq|&>geL&!5Z7@0Z(No*W561n#u$Uc`f9pD70# z=sKOSK|bF~#khTTn)B28h^a1{;>EaRnHj~>i=Fnr3+Fa4 z`^+O5_itS#7kPd20rq66_wH`%?HNzWk@XFK0n;Z@Cx{kx==2L22zWH$Yg?7 zvDj|u{{+NR3JvUH({;b*$b(U5U z7(lF!1bz2%06+|-v(D?2KgwNw7( zJB#Tz+ZRi&U$i?f34m7>uTzO#+E5cbaiQ&L}UxyOQq~afbNB4EI{E04ZWg53w0A{O%qo=lF8d zf~ktGvIgf-a~zQoWf>loF7pOodrd0a2|BzwwPDV}ShauTK8*fmF6NRbO>Iw9zZU}u zw8Ya}?seBnEGQDmH#XpUUkj}N49tP<2jYwTFp!P+&Fd(%Z#yo80|5@zN(D{_pNow*&4%ql zW~&yp@scb-+Qj-EmErY+Tu=dUmf@*BoXY2&oKT8U?8?s1d}4a`Aq>7SV800m$FE~? zjmz(LY+Xx9sDX$;vU`xgw*jLw7dWOnWWCO8o|;}f>cu0Q&`0I{YudMn;P;L3R-uz# zfns_mZED_IakFBPP2r_S8XM$X)@O-xVKi4`7373Jkd5{2$M#%cRhWer3M(vr{S6>h zj{givZJ3(`yFL@``(afn&~iNx@B1|-qfYiZu?-_&Z8+R~v`d6R-}EX9IVXWO-!hL5 z*k6T#^2zAXdardU3Ao~I)4DGdAv2bx{4nOK`20rJo>rmk3S2ZDu}))8Z1m}CKigf0 z3L`3Y`{huj`xj9@`$xTZzZc3je?n^yG<8sw$`Y%}9mUsjUR%T!?k^(q)6FH6Af^b6 zlPg~IEwg0y;`t9y;#D+uz!oE4VP&Je!<#q*F?m5L5?J3i@!0J6q#eu z!RRU`-)HeqGi_UJZ(n~|PSNsv+Wgl{P-TvaUQ9j?ZCtvb^37U$sFpBrkT{7Jpd?HpIvj2!}RIq zH{9~+gErN2+}J`>Jvng2hwM`=PLNkc7pkjblKW|+Fk9rc)G1R>Ww>RC=r-|!m-u7( zc(a$9NG}w#PjWNMS~)o=i~WA&4L(YIW25@AL9+H9!?3Y}sv#MOdY{bb9j>p`{?O(P zIvb`n?_(gP2w3P#&91JX*md+bBEr%xUHMVqfB;(f?OPtMnAZ#rm5q5mh;a2f_si2_ z3oXWB?{NF(JtkAn6F(O{z@b76OIqMC$&oJ_&S|YbFJ*)3qVX_uNf5b8(!vGX19hsG z(OP>RmZp29KH9Ge2kKjKigUmOe^K_!UXP`von)PR8Qz$%=EmOB9xS(ZxE_tnyzo}7 z=6~$~9k0M~v}`w={AeqF?_)9q{m8K#6M{a&(;u;O41j)I$^T?lx5(zlebpY@NT&#N zR+1bB)-1-xj}R8uwqwf=iP1GbxBjneCC%UrSdSxK1vM^i9;bUkS#iRZw2H>rS<2<$ zNT3|sDH>{tXb=zq7XZi*K?#Zsa1h1{h5!Tq_YbKFm_*=A5-<~j63he;4`77!|LBlo zR^~tR3yxcU=gDFbshyF6>o0bdp$qmHS7D}m3;^QZq9kBBU|9$N-~oU?G5;jyFR7>z hN`IR97YZXIo@y!QgFWddJ3|0`sjFx!m))><{BI=FK%f8s literal 0 HcmV?d00001 diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 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 MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 0000000..f1a0a5c --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + snowplow_tracker_example + + + + + + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 0000000..3b4bad5 --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "snowplow_tracker_example", + "short_name": "snowplow_tracker_example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "Demonstrates how to use the snowplow_tracker plugin.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/example/web/sp.js b/example/web/sp.js new file mode 100644 index 0000000..70638bd --- /dev/null +++ b/example/web/sp.js @@ -0,0 +1,8 @@ +/*! + * Web analytics for Snowplow v3.2.3 (http://bit.ly/sp-js) + * Copyright 2022 Snowplow Analytics Ltd, 2010 Anthon Pang + * Licensed under BSD-3-Clause + */ + +"use strict";!function(){function e(e,n){var t,o={};for(t in e)Object.prototype.hasOwnProperty.call(e,t)&&0>n.indexOf(t)&&(o[t]=e[t]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var r=0;for(t=Object.getOwnPropertySymbols(e);rn.indexOf(t[r])&&Object.prototype.propertyIsEnumerable.call(e,t[r])&&(o[t[r]]=e[t[r]])}return o}function n(e,n,t){if(t||2===arguments.length)for(var o,r=0,a=n.length;r=e.length}function g(e){return p(e)||m(e)}function v(e){return!(!Array.isArray(e)||2!==e.length)&&(Array.isArray(e[1])?p(e[0])&&e[1].every(g):p(e[0])&&g(e[1]))}function h(e){return!(!Array.isArray(e)||2!==e.length)&&(!!function(e){var n=0;if(null!=e&&"object"==typeof e&&!Array.isArray(e)){if(Object.prototype.hasOwnProperty.call(e,"accept")){if(!d(e.accept))return!1;n+=1}if(Object.prototype.hasOwnProperty.call(e,"reject")){if(!d(e.reject))return!1;n+=1}return 0=n}return!1}(e[0])&&(Array.isArray(e[1])?e[1].every(g):g(e[1])))}function y(e){return v(e)||h(e)}function w(e,n){if(!l(e))return!1;if(e=u(e),n=null!==(n=/^iglu:([a-zA-Z0-9-_.]+)\/([a-zA-Z0-9-_]+)\/jsonschema\/([1-9][0-9]*)-(0|[1-9][0-9]*)-(0|[1-9][0-9]*)$/.exec(n))?n.slice(1,6):void 0,e&&n){if(!function(e,n){if(n=n.split("."),e=e.split("."),n&&e){if(n.length!==e.length)return!1;for(var t=0;tt;t++)if(!k(e[t],n[t]))return!1;return!0}return!1}function k(e,n){return e&&n&&"*"===e||e===n}function b(e){return Array.isArray(e)?e:[e]}function A(e,n,t,o){var r;return e=b(e).map((function(e){e:if(m(e))e=[e];else{if(p(e)){n:{var r=void 0;try{if(r=e({event:n.getPayload(),eventType:t,eventSchema:o}),Array.isArray(r)&&r.every(m)||m(r)){var a=r;break n}a=void 0;break n}catch(e){}a=void 0}if(m(a)){e=[a];break e}if(Array.isArray(a)){e=a;break e}}e=void 0}if(e&&0!==e.length)return e})),(r=[]).concat.apply(r,e.filter((function(e){return null!=e&&e.filter(Boolean)})))}function P(e){void 0===e&&(e={});var n,t,o,r,c,s,u,l=e.base64,f=e.corePlugins,d=null!=f?f:[];n=null==l||l,t=d,o=e.callback,r=function(e){return{addPluginContexts:function(n){var t=null!=n?n:[];return e.forEach((function(e){try{e.contexts&&t.push.apply(t,e.contexts())}catch(e){Ue.error("Error adding plugin contexts",e)}})),t}}}(t),c=i(),s=n,u={};var m=Pe(Pe({},e={track:function(e,n,a){e.withJsonProcessor(function(e){return function(n,t){for(var o=0;o>18&63,d=p>>12&63,m=p>>6&63,p&=63,l[u++]=Be.charAt(f)+Be.charAt(d)+Be.charAt(m)+Be.charAt(p)}while(s=o?n+=1:2047>=o?n+=2:55296<=o&&57343>=o?(n+=4,t++):n=65535>o?n+3:n+4}return n}function p(e){for(void 0===e&&(e=!1);T.length&&"string"!=typeof T[0]&&"object"!=typeof T[0];)T.shift();if(1>T.length)P=!1;else{if(!w||"string"!=typeof w.valueOf())throw"No collector configured";if(P=!0,C){var n=function(e){for(var n=0,t=0;n=i);)n+=1;return n},o=void 0;if(I(T))var r=g(o=w,!0,e),a=n(T);else o=y(T[0]),r=g(o,!1,e),a=1;var c=setTimeout((function(){r.abort(),P=!1}),u),f=function(e){for(var n=0;nr.status?(clearTimeout(c),f(a)):4===r.readyState&&400<=r.status&&(clearTimeout(c),P=!1)},I(T)){if(0<(n=T.slice(0,a)).length){if(e=!1,n=n.map((function(e){return e.evt})),S){var d=new Blob([v(h(n))],{type:"application/json"});try{e=navigator.sendBeacon(o,d)}catch(n){e=!1}}!0===e?f(a):r.send(v(h(n)))}}else r.send()}else if(l||I(T))P=!1;else{o=new Image(1,1);var m=!0;o.onload=function(){m&&(m=!1,T.shift(),t&&L(E,JSON.stringify(T.slice(0,s))),p())},o.onerror=function(){m&&(P=m=!1)},o.src=y(T[0]),setTimeout((function(){m&&P&&(m=!1,p())}),u)}}}function g(e,n,t){var o=new XMLHttpRequest;for(var r in n?(o.open("POST",e,!t),o.setRequestHeader("Content-Type","application/json; charset=UTF-8")):o.open("GET",e,!t),o.withCredentials=d,l&&o.setRequestHeader("SP-Anonymous","*"),f)Object.prototype.hasOwnProperty.call(f,r)&&o.setRequestHeader(r,f[r]);return o}function v(e){return JSON.stringify({schema:"iglu:com.snowplowanalytics.snowplow/payload_data/jsonschema/1-0-4",data:e})}function h(e){for(var n=(new Date).getTime().toString(),t=0;t=i)return Ue.warn("Event ("+e.bytes+"B) too big, max is "+i),void g(w,!0,!1).send(v(h([e.evt])));T.push(e)}else{var o,r=(n=T).push,c="?",u={co:!0,cx:!0},l=!0;for(o in e)e.hasOwnProperty(o)&&!u.hasOwnProperty(o)&&(l?l=!1:c+="&",c+=encodeURIComponent(o)+"="+encodeURIComponent(e[o]));for(var f in u)e.hasOwnProperty(f)&&u.hasOwnProperty(f)&&(c+="&"+f+"="+encodeURIComponent(e[f]));r.call(n,c)}e=!1,t&&(e=L(E,JSON.stringify(T.slice(0,s)))),P||e&&!(T.length>=a)||p()},executeQueue:function(){P||p()},setUseLocalStorage:function(e){t=e},setAnonymousTracking:function(e){l=e},setCollectorUrl:function(e){w=e+x},setBufferSize:function(e){a=e}}}function J(e,n,t){return"translate.googleusercontent.com"===e?(""===t&&(t=n),e=E(n=null!=(e=(e=/^(?:https?|ftp)(?::\/*(?:[^?]+))([?][^#]+)/.exec(n))&&1<(null==e?void 0:e.length)?N("u",e[1]):null)?e:"")):"cc.bingj.com"!==e&&"webcache.googleusercontent.com"!==e||(e=E(n=document.links[0].href)),[e,n,t]}function Y(e,n,t,r,a,i){void 0===i&&(i={});var c=[];e=function(e,n,t,r,a,i){function s(){(Be=J(window.location.hostname,window.location.href,I()))[1]!==Me&&(Fe=I(Me)),ze=j(Be[0]),Me=Be[1]}function u(e){var n=(new Date).getTime();if(null!=(e=e.currentTarget)&&e.href){n="_sp="+xe+"."+n;var t=e.href.split("#"),o=t[0].split("?"),r=o.shift();if(o=o.join("?")){for(var a=!0,i=o.split("&"),c=0;cDate.now())var o=n.getItem(e);else n.removeItem(e),n.removeItem(e+".expires"),o=void 0}catch(e){o=void 0}return o}if("cookie"==cn||"cookieAndLocalStorage"==cn)return z(e)}function p(){s(),Oe=Re((We||ze)+(Xe||"/")).slice(0,4)}function g(){Ae=(new Date).getTime()}function v(){var e=h(),n=e[0];n_e&&(_e=n),(e=e[1])Ce&&(Ce=e),g()}function h(){var e=document.documentElement;return e?[e.scrollLeft||window.pageXOffset,e.scrollTop||window.pageYOffset]:[0,0]}function y(){var e=h(),n=e[0];_e=Te=n,Ce=Se=e=e[1]}function w(){b(Ke+"ses."+Oe,"*",tn)}function k(e,n,t,o,r,a){b(Ke+"id."+Oe,e+"."+n+"."+t+"."+o+"."+r+"."+a,nn)}function b(e,n,t){an&&!on||("localStorage"==cn?L(e,n,t):("cookie"==cn||"cookieAndLocalStorage"==cn)&&z(e,n,t,Xe,We,Qe,Ze))}function A(e){var n=Ke+"id."+Oe,t=Ke+"ses."+Oe;B(n),B(t),z(n,"",-1,"/",We,Qe,Ze),z(t,"",-1,"/",We,Qe,Ze),null!=e&&e.preserveSession||(Ee=Le.v4(),un=0),null!=e&&e.preserveUser||(xe=Le.v4(),je=null)}function T(e){e&&e.stateStorageStrategy&&(i.stateStorageStrategy=e.stateStorageStrategy,cn=me(i)),an=!!i.anonymousTracking,on=pe(i),rn=ge(i),ln.setUseLocalStorage("localStorage"==cn||"cookieAndLocalStorage"==cn),ln.setAnonymousTracking(rn)}function _(){if(!an||on){var e="none"!=cn&&!!m("ses"),n=S();n[1]?xe=n[1]:(xe=an?"":Le.v4(),n[1]=xe),Ee=n[6],e||(n[3]++,Ee=Le.v4(),n[6]=Ee,n[5]=n[4]),"none"!=cn&&(w(),n[4]=Math.round((new Date).getTime()/1e3),n.shift(),k.apply(null,n))}}function S(){if("none"==cn)return[];var e=Math.round((new Date).getTime()/1e3),n=m("id");return n?(e=n.split(".")).unshift("0"):e=["1",xe,e,0,e,""],e[6]&&"undefined"!==e[6]||(e[6]=Le.v4()),e}function O(e){return 0===e.indexOf("http")?e:("https:"===document.location.protocol?"https":"http")+"://"+e}function M(){fn&&null!=a.pageViewId||(a.pageViewId=Le.v4())}function U(){return null==a.pageViewId&&(a.pageViewId=Le.v4()),a.pageViewId}function F(e){var n=e.title,t=e.context,r=e.timestamp;if(e=e.contextCallback,s(),dn&&M(),dn=!0,Je=document.title,n=x((ye=n)||Je),Ie.track(function(e){var n=e.pageUrl,t=e.pageTitle;e=e.referrer;var r=o();return r.add("e","pv"),r.add("url",n),r.add("page",t),r.add("refr",e),r}({pageUrl:f(he||Me),pageTitle:n,referrer:f(ve||Fe)}),(t||[]).concat(e?e():[]),r),r=new Date,n=!1,mn.enabled&&!mn.installed){n=mn.installed=!0;var a={update:function(){if("undefined"!=typeof window&&"function"==typeof window.addEventListener){var e=!1,n=Object.defineProperty({},"passive",{get:function(){e=!0},set:function(){}}),t=function(){};window.addEventListener("testPassiveEventSupport",t,n),window.removeEventListener("testPassiveEventSupport",t,n),a.hasSupport=e}}};a.update();var i="onwheel"in document.createElement("div")?"wheel":void 0!==document.onmousewheel?"mousewheel":"DOMMouseScroll";Object.prototype.hasOwnProperty.call(a,"hasSupport")?D(document,i,g,{passive:!0}):D(document,i,g),y(),i=function(e,n){return void 0===n&&(n=g),function(e){return D(document,e,n)}},"click mouseup mousedown mousemove keypress keydown keyup".split(" ").forEach(i(document)),["resize","focus","blur"].forEach(i(window)),i(window,v)("scroll")}if(mn.enabled&&(Ye||n))for(r in Ae=r.getTime(),r=void 0,mn.configurations)(n=mn.configurations[r])&&(window.clearInterval(n.activityInterval),V(n,t,e))}function V(e,n,t){var o=function(e,n){s(),e({context:n,pageViewId:U(),minXOffset:Te,minYOffset:Se,maxXOffset:_e,maxYOffset:Ce}),y()},r=function(){Ae+e.configHeartBeatTimer>(new Date).getTime()&&o(e.callback,(n||[]).concat(t?t():[]))};e.activityInterval=0!=e.configMinimumVisitLength?window.setTimeout((function(){Ae+e.configMinimumVisitLength>(new Date).getTime()&&o(e.callback,(n||[]).concat(t?t():[])),e.activityInterval=window.setInterval(r,e.configHeartBeatTimer)}),e.configMinimumVisitLength):window.setInterval(r,e.configHeartBeatTimer)}function H(e){var n=e.minimumVisitLength,t=e.heartbeatDelay;if(e=e.callback,C(n)&&C(t))return{configMinimumVisitLength:1e3*n,configHeartBeatTimer:1e3*t,callback:e};Ue.error("Activity tracking minimumVisitLength & heartbeatDelay must be integers")}function R(e){var n=e.context,t=e.minXOffset,r=e.minYOffset,a=e.maxXOffset,i=e.maxYOffset;(e=document.title)!==Je&&(Je=e,ye=void 0);var c=(e=Ie).track,s=f(he||Me),u=x(ye||Je),l=f(ve||Fe);t=Math.round(t),a=Math.round(a),r=Math.round(r),i=Math.round(i);var d=o();d.add("e","pp"),d.add("url",s),d.add("page",u),d.add("refr",l),t&&!isNaN(Number(t))&&d.add("pp_mix",t.toString()),a&&!isNaN(Number(a))&&d.add("pp_max",a.toString()),r&&!isNaN(Number(r))&&d.add("pp_miy",r.toString()),i&&!isNaN(Number(i))&&d.add("pp_may",i.toString()),c.call(e,d,n)}var G,Y,K,W,X,Q,Z,$,ee,ne,te,oe,re,ae,ie,ce,se,ue,le,fe,de;i.eventMethod=null!==(G=i.eventMethod)&&void 0!==G?G:"post";var me=function(e){var n;return null!==(n=e.stateStorageStrategy)&&void 0!==n?n:"cookieAndLocalStorage"},pe=function(e){var n,t;return"boolean"!=typeof e.anonymousTracking&&(null!==(t=!0===(null===(n=e.anonymousTracking)||void 0===n?void 0:n.withSessionTracking))&&void 0!==t&&t)},ge=function(e){var n,t;return"boolean"!=typeof e.anonymousTracking&&(null!==(t=!0===(null===(n=e.anonymousTracking)||void 0===n?void 0:n.withServerAnonymisation))&&void 0!==t&&t)};c.push({beforeTrack:function(e){var n=Math.round((new Date).getTime()/1e3),t=m("ses"),o=S(),r=o[0],a=o[1],i=o[2],c=o[3],u=o[4],l=o[5];o=o[6];var d=!!be&&!!z(be);en||d?A():("0"===r?(Ee=o,t||"none"==cn||(c++,l=u,Ee=Le.v4()),un=c):(new Date).getTime()-sn>1e3*tn&&(Ee=Le.v4(),un++),t=e.add,"innerWidth"in window?(r=window.innerWidth,c=window.innerHeight):(r=(c=document.documentElement||document.body).clientWidth,c=c.clientHeight),t.call(e,"vp",0<=r&&0<=c?r+"x"+c:null),t=e.add,c=document.documentElement,u=document.body,r=Math.max(c.clientWidth,c.offsetWidth,c.scrollWidth),c=Math.max(c.clientHeight,c.offsetHeight,c.scrollHeight,u?Math.max(u.offsetHeight,u.scrollHeight):0),r=isNaN(r)||isNaN(c)?"":r+"x"+c,t.call(e,"ds",r),e.add("vid",on?un:an?null:un),e.add("sid",on?Ee:an?null:Ee),e.add("duid",an?null:a),e.add("uid",an?null:je),s(),e.add("refr",f(ve||Fe)),e.add("url",f(he||Me)),"none"!=cn&&(k(a,i,un,n,l,Ee),w()),sn=(new Date).getTime())}}),(null===(K=null===(Y=null==i?void 0:i.contexts)||void 0===Y?void 0:Y.webPage)||void 0===K||K)&&c.push({contexts:function(){return[{schema:"iglu:com.snowplowanalytics.snowplow/web_page/jsonschema/1-0-0",data:{id:U()}}]}}),c.push.apply(c,null!==(W=i.plugins)&&void 0!==W?W:[]);var ve,he,ye,we,ke,be,Ae,Te,_e,Se,Ce,Oe,xe,Ee,je,Ie=P({base64:i.encodeBase64,corePlugins:c,callback:function(e){var n=!!be&&!!z(be);en||n||ln.enqueueRequest(e.build(),He)}}),De=navigator.userLanguage||navigator.language,Ne=document.characterSet||document.charset,Be=J(window.location.hostname,window.location.href,I()),ze=j(Be[0]),Me=Be[1],Fe=Be[2],Ve=null!==(X=i.platform)&&void 0!==X?X:"web",He=O(r),Ge=null!==(Q=i.postPath)&&void 0!==Q?Q:"/com.snowplowanalytics.snowplow/tp2",qe=null!==(Z=i.appId)&&void 0!==Z?Z:"",Je=document.title,Ye=null===($=i.resetActivityTrackingOnPageView)||void 0===$||$,Ke=null!==(ee=i.cookieName)&&void 0!==ee?ee:"_sp_",We=null!==(ne=i.cookieDomain)&&void 0!==ne?ne:void 0,Xe="/",Qe=null!==(te=i.cookieSameSite)&&void 0!==te?te:"None",Ze=null===(oe=i.cookieSecure)||void 0===oe||oe,$e=navigator.doNotTrack||navigator.msDoNotTrack||window.doNotTrack,en=void 0!==i.respectDoNotTrack&&(i.respectDoNotTrack&&("yes"===$e||"1"===$e)),nn=null!==(re=i.cookieLifetime)&&void 0!==re?re:63072e3,tn=null!==(ae=i.sessionCookieTimeout)&&void 0!==ae?ae:1800,on=pe(i),rn=ge(i),an=!!i.anonymousTracking,cn=me(i),sn=(new Date).getTime(),un=1,ln=q(e,a,"localStorage"==cn||"cookieAndLocalStorage"==cn,i.eventMethod,Ge,null!==(ie=i.bufferSize)&&void 0!==ie?ie:1,null!==(ce=i.maxPostBytes)&&void 0!==ce?ce:4e4,null===(se=i.useStm)||void 0===se||se,null!==(ue=i.maxLocalStorageQueueSize)&&void 0!==ue?ue:1e3,null!==(le=i.connectionTimeout)&&void 0!==le?le:5e3,rn,null!==(fe=i.customHeaders)&&void 0!==fe?fe:{},null===(de=i.withCredentials)||void 0===de||de),fn=!1,dn=!1,mn={enabled:!1,installed:!1,configurations:{}};return i.hasOwnProperty("discoverRootDomain")&&i.discoverRootDomain&&(We=function(e,n){for(var t=window.location.hostname,o="_sp_root_domain_test_"+(new Date).getTime(),r="_test_value_"+(new Date).getTime(),a=t.split("."),i=a.length-1;0<=i;){var c=a.slice(i,a.length).join(".");if(z(o,r,0,"/",c,e,n),z(o)===r){for(z(o,"",-1,"/",c,e,n),t=document.cookie.split("; "),o=[],r=0;rn;n++)0==(3&n)&&(e=4294967296*Math.random()),o[n]=e>>>((3&n)<<3)&255;return o}}})),_e=[],Se=0;256>Se;++Se)_e[Se]=(Se+256).toString(16).substr(1);var Ce,Oe,xe=function(e,n){return n=n||0,[_e[e[n++]],_e[e[n++]],_e[e[n++]],_e[e[n++]],"-",_e[e[n++]],_e[e[n++]],"-",_e[e[n++]],_e[e[n++]],"-",_e[e[n++]],_e[e[n++]],"-",_e[e[n++]],_e[e[n++]],_e[e[n++]],_e[e[n++]],_e[e[n++]],_e[e[n++]]].join("")},Ee=0,je=0,Ie=function(e,n,t){if(t=n&&t||0,"string"==typeof e&&(n="binary"===e?Array(16):null,e=null),(e=(e=e||{}).random||(e.rng||Te)())[6]=15&e[6]|64,e[8]=63&e[8]|128,n)for(var o=0;16>o;++o)n[t+o]=e[o];return n||xe(e)};Ie.v1=function(e,n,t){t=n&&t||0;var o=n||[],r=(e=e||{}).node||Ce,a=void 0!==e.clockseq?e.clockseq:Oe;if(null==r||null==a){var i=Te();null==r&&(r=Ce=[1|i[0],i[1],i[2],i[3],i[4],i[5]]),null==a&&(a=Oe=16383&(i[6]<<8|i[7]))}i=void 0!==e.msecs?e.msecs:(new Date).getTime();var c=void 0!==e.nsecs?e.nsecs:je+1,s=i-Ee+(c-je)/1e4;if(0>s&&void 0===e.clockseq&&(a=a+1&16383),(0>s||i>Ee)&&void 0===e.nsecs&&(c=0),1e4<=c)throw Error("uuid.v1(): Can't create more than 10M uuids/sec");for(Ee=i,je=c,Oe=a,e=(1e4*(268435455&(i+=122192928e5))+c)%4294967296,o[t++]=e>>>24&255,o[t++]=e>>>16&255,o[t++]=e>>>8&255,o[t++]=255&e,e=i/4294967296*1e4&268435455,o[t++]=e>>>8&255,o[t++]=255&e,o[t++]=e>>>24&15|16,o[t++]=e>>>16&255,o[t++]=a>>>8|128,o[t++]=255&a,a=0;6>a;++a)o[t+a]=r[a];return n||xe(o)};var De,Ne,Le=Ie.v4=Ie,Be="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";(Ne=De||(De={}))[Ne.none=0]="none",Ne[Ne.error=1]="error",Ne[Ne.warn=2]="warn",Ne[Ne.debug=3]="debug",Ne[Ne.info=4]="info";var ze,Me,Ue=function(e){return void 0===e&&(e=De.warn),{setLogLevel:function(n){e=De[n]?n:De.warn},warn:function(t,o){for(var r=[],a=2;a=De.warn&&"undefined"!=typeof console&&(a="Snowplow: "+t,o?console.warn.apply(console,n([a+"\n",o],r)):console.warn.apply(console,n([a],r)))},error:function(t,o){for(var r=[],a=2;a=De.error&&"undefined"!=typeof console&&(a="Snowplow: "+t+"\n",o?console.error.apply(console,n([a+"\n",o],r)):console.error.apply(console,n([a],r)))},debug:function(t){for(var o=[],r=1;r=De.debug&&"undefined"!=typeof console&&console.debug.apply(console,n(["Snowplow: "+t],o))},info:function(t){for(var o=[],r=1;r=De.info&&"undefined"!=typeof console&&console.info.apply(console,n(["Snowplow: "+t],o))}}}(),Fe=t((function(e){var n;n={rotl:function(e,n){return e<>>32-n},rotr:function(e,n){return e<<32-n|e>>>n},endian:function(e){if(e.constructor==Number)return 16711935&n.rotl(e,8)|4278255360&n.rotl(e,24);for(var t=0;t>>5]|=e[t]<<24-o%32;return n},wordsToBytes:function(e){for(var n=[],t=0;t<32*e.length;t+=8)n.push(e[t>>>5]>>>24-t%32&255);return n},bytesToHex:function(e){for(var n=[],t=0;t>>4).toString(16)),n.push((15&e[t]).toString(16));return n.join("")},hexToBytes:function(e){for(var n=[],t=0;tr;r++)8*t+6*r<=8*e.length?n.push("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(o>>>6*(3-r)&63)):n.push("=");return n.join("")},base64ToBytes:function(e){e=e.replace(/[^A-Z0-9+\/]/gi,"");for(var n=[],t=0,o=0;t>>6-2*o);return n}},e.exports=n})),Ve={utf8:{stringToBytes:function(e){return Ve.bin.stringToBytes(unescape(encodeURIComponent(e)))},bytesToString:function(e){return decodeURIComponent(escape(Ve.bin.bytesToString(e)))}},bin:{stringToBytes:function(e){for(var n=[],t=0;t>5]|=128<<24-i%32,e[15+(i+64>>>9<<4)]=i,i=0;ih;h++){if(16>h)a[h]=e[i+h];else{var y=a[h-3]^a[h-8]^a[h-14]^a[h-16];a[h]=y<<1|y>>>31}y=(c<<5|c>>>27)+f+(a[h]>>>0)+(20>h?1518500249+(s&u|~s&l):40>h?1859775393+(s^u^l):60>h?(s&u|s&l|u&l)-1894007588:(s^u^l)-899497514),f=l,l=u,u=s<<30|s>>>2,s=c,c=y}c+=d,s+=m,u+=p,l+=g,f+=v}return r=r.call(Fe,[c,s,u,l,f]),o&&o.asBytes?r:o&&o.asString?t.bytesToString(r):Fe.bytesToHex(r)})._blocksize=16,o._digestsize=20,e.exports=o})),Ge={},qe=function(){this.outQueues=[],this.bufferFlushers=[],this.hasLoaded=!1,this.registeredOnLoadHandlers=[]},Je="undefined"!=typeof window?Z():void 0,Ye=Object.freeze({__proto__:null,addGlobalContexts:function(e,n){K(n,(function(n){n.core.addGlobalContexts(e)}))},addPlugin:function(e,n){K(n,(function(n){n.addPlugin(e)}))},clearGlobalContexts:function(e){K(e,(function(e){e.core.clearGlobalContexts()}))},clearUserData:function(e,n){K(n,(function(n){n.clearUserData(e)}))},crossDomainLinker:function(e,n){K(n,(function(n){n.crossDomainLinker(e)}))},disableAnonymousTracking:function(e,n){K(n,(function(n){n.disableAnonymousTracking(e)}))},discardBrace:function(e,n){K(n,(function(n){n.discardBrace(e)}))},discardHashTag:function(e,n){K(n,(function(n){n.discardHashTag(e)}))},enableActivityTracking:function(e,n){K(n,(function(n){n.enableActivityTracking(e)}))},enableActivityTrackingCallback:function(e,n){K(n,(function(n){n.enableActivityTrackingCallback(e)}))},enableAnonymousTracking:function(e,n){K(n,(function(n){n.enableAnonymousTracking(e)}))},flushBuffer:function(e,n){K(n,(function(n){n.flushBuffer(e)}))},newSession:function(e){K(e,(function(e){e.newSession()}))},newTracker:function(e,n,t){if(void 0===t&&(t={}),Je)return X(e,e,"js-3.2.3",n,Je,t)},preservePageViewId:function(e){K(e,(function(e){e.preservePageViewId()}))},removeGlobalContexts:function(e,n){K(n,(function(n){n.core.removeGlobalContexts(e)}))},setBufferSize:function(e,n){K(n,(function(n){n.setBufferSize(e)}))},setCollectorUrl:function(e,n){K(n,(function(n){n.setCollectorUrl(e)}))},setCookiePath:function(e,n){K(n,(function(n){n.setCookiePath(e)}))},setCustomUrl:function(e,n){K(n,(function(n){n.setCustomUrl(e)}))},setDocumentTitle:function(e,n){K(n,(function(n){n.setDocumentTitle(e)}))},setOptOutCookie:function(e,n){K(n,(function(n){n.setOptOutCookie(e)}))},setReferrerUrl:function(e,n){K(n,(function(n){n.setReferrerUrl(e)}))},setUserId:function(e,n){K(n,(function(n){n.setUserId(e)}))},setUserIdFromCookie:function(e,n){K(n,(function(n){n.setUserIdFromCookie(e)}))},setUserIdFromLocation:function(e,n){K(n,(function(n){n.setUserIdFromLocation(e)}))},setUserIdFromReferrer:function(e,n){K(n,(function(n){n.setUserIdFromReferrer(e)}))},setVisitorCookieTimeout:function(e,n){K(n,(function(n){n.setVisitorCookieTimeout(e)}))},trackPageView:function(e,n){K(n,(function(n){n.trackPageView(e)}))},trackSelfDescribingEvent:function(e,n){K(n,(function(n){n.core.track(T({event:e.event}),e.context,e.timestamp)}))},trackStructEvent:function(e,n){K(n,(function(n){var t=(n=n.core).track,r=e.category,a=e.action,i=e.label,c=e.property,s=e.value,u=o();u.add("e","se"),u.add("se_ca",r),u.add("se_ac",a),u.add("se_la",i),u.add("se_pr",c),u.add("se_va",null==s?void 0:s.toString()),t.call(n,u,e.context,e.timestamp)}))},updatePageActivity:function(e){K(e,(function(e){e.updatePageActivity()}))},version:"3.2.3"}),Ke=Object.freeze({__proto__:null,ClientHintsPlugin:$}),We=Object.freeze({__proto__:null,OptimizelyXPlugin:ee}),Xe=Object.freeze({__proto__:null,PerformanceTimingPlugin:ne});!function(e){e.consent="consent",e.contract="contract",e.legalObligation="legal_obligation",e.vitalInterests="vital_interests",e.publicTask="public_task",e.legitimateInterests="legitimate_interests"}(Me||(Me={}));var Qe,Ze,$e={},en={},nn=Object.freeze({__proto__:null,ConsentPlugin:te,enableGdprContext:function(e,n){void 0===n&&(n=Object.keys($e));var t=e.documentId,o=e.documentVersion,r=e.documentDescription,a=Me[e.basisForProcessing];a?n.forEach((function(e){$e[e]&&(en[e]={basisForProcessing:a,documentId:null!=t?t:null,documentVersion:null!=o?o:null,documentDescription:null!=r?r:null})})):Qe.warn("enableGdprContext: basisForProcessing must be one of: consent, contract, legalObligation, vitalInterests, publicTask, legitimateInterests")},get gdprBasis(){return Me},trackConsentGranted:function(e,n){void 0===n&&(n=Object.keys($e)),W(n,$e,(function(n){var t=e.expiry,o={schema:"iglu:com.snowplowanalytics.snowplow/consent_document/jsonschema/1-0-0",data:S({id:e.id,version:e.version,name:e.name,description:e.description})};t=T({event:{schema:"iglu:com.snowplowanalytics.snowplow/consent_granted/jsonschema/1-0-0",data:S({expiry:t})}}),o=[o],n.core.track(t,e.context?e.context.concat(o):o,e.timestamp)}))},trackConsentWithdrawn:function(e,n){void 0===n&&(n=Object.keys($e)),W(n,$e,(function(n){var t=e.all,o={schema:"iglu:com.snowplowanalytics.snowplow/consent_document/jsonschema/1-0-0",data:S({id:e.id,version:e.version,name:e.name,description:e.description})};t=T({event:{schema:"iglu:com.snowplowanalytics.snowplow/consent_withdrawn/jsonschema/1-0-0",data:S({all:t})}}),o=[o],n.core.track(t,e.context?e.context.concat(o):o,e.timestamp)}))}}),tn={},on=!1,rn=Object.freeze({__proto__:null,GeolocationPlugin:oe,enableGeolocationContext:re}),an=Object.freeze({__proto__:null,GaCookiesPlugin:ae}),cn={},sn={},un=Object.freeze({__proto__:null,LinkClickTrackingPlugin:ie,enableLinkClickTracking:function(e,n){void 0===e&&(e={}),void 0===n&&(n=Object.keys(cn)),n.forEach((function(n){cn[n]&&(cn[n].sharedState.hasLoaded?(ue(e,n),le(n)):cn[n].sharedState.registeredOnLoadHandlers.push((function(){ue(e,n),le(n)})))}))},refreshLinkClickTracking:function(e){void 0===e&&(e=Object.keys(cn)),e.forEach((function(e){cn[e]&&(cn[e].sharedState.hasLoaded?le(e):cn[e].sharedState.registeredOnLoadHandlers.push((function(){le(e)})))}))},trackLinkClick:function(e,n){void 0===n&&(n=Object.keys(cn)),W(n,cn,(function(n){n.core.track(_(e),e.context,e.timestamp)}))}}),ln=["textarea","input","select"],fn=function(e){return e},dn={},mn=Object.freeze({__proto__:null,FormTrackingPlugin:ge,enableFormTracking:function(e,n){void 0===e&&(e={}),void 0===n&&(n=Object.keys(dn)),n.forEach((function(n){dn[n]&&(dn[n].sharedState.hasLoaded?fe(dn[n],e):dn[n].sharedState.registeredOnLoadHandlers.push((function(){fe(dn[n],e)})))}))}}),pn={},gn=Object.freeze({__proto__:null,ErrorTrackingPlugin:ve,enableErrorTracking:function(e,n){void 0===e&&(e={}),void 0===n&&(n=Object.keys(pn));var t=e.filter,o=e.contextAdder,r=e.context;D(window,"error",(function(e){if(t&&O(t)&&t(e)||null==t){var a=n,i=r||[];o&&O(o)&&(i=i.concat(o(e))),he({message:e.message,filename:e.filename,lineno:e.lineno,colno:e.colno,error:e.error,context:i},a)}}),!0)},trackError:he}),vn=t((function(e){var n,t,o,r,a,i;n={"America/Denver":["America/Mazatlan"],"America/Chicago":["America/Mexico_City"],"America/Asuncion":["America/Campo_Grande","America/Santiago"],"America/Montevideo":["America/Sao_Paulo","America/Santiago"],"Asia/Beirut":"Asia/Amman Asia/Jerusalem Europe/Helsinki Asia/Damascus Africa/Cairo Asia/Gaza Europe/Minsk Africa/Windhoek".split(" "),"Pacific/Auckland":["Pacific/Fiji"],"America/Los_Angeles":["America/Santa_Isabel"],"America/New_York":["America/Havana"],"America/Halifax":["America/Goose_Bay"],"America/Godthab":["America/Miquelon"],"Asia/Dubai":["Asia/Yerevan"],"Asia/Jakarta":["Asia/Krasnoyarsk"],"Asia/Shanghai":["Asia/Irkutsk","Australia/Perth"],"Australia/Sydney":["Australia/Lord_Howe"],"Asia/Tokyo":["Asia/Yakutsk"],"Asia/Dhaka":["Asia/Omsk"],"Asia/Baku":["Asia/Yerevan"],"Australia/Brisbane":["Asia/Vladivostok"],"Pacific/Noumea":["Asia/Vladivostok"],"Pacific/Majuro":["Asia/Kamchatka","Pacific/Fiji"],"Pacific/Tongatapu":["Pacific/Apia"],"Asia/Baghdad":["Europe/Minsk","Europe/Moscow"],"Asia/Karachi":["Asia/Yekaterinburg"],"Africa/Johannesburg":["Asia/Gaza","Africa/Cairo"]},t=function(){for(var e=[],n=0;11>=n;n++)for(var t=1;28>=t;t++){var o=-new Date(2014,n,t).getTimezoneOffset();o=null!==o?o:0,e?e&&e[e.length-1]!==o&&e.push(o):e.push()}return e},o=function e(n,t,o){void 0===t&&(t=864e5,o=36e5);var r=new Date(n.getTime()-t).getTime();n=n.getTime()+t;for(var a=new Date(r).getTimezoneOffset(),i=null;ra&&(s=u),a=l),r+=864e5}t=!(!c||!s)&&{s:o(c).getTime(),e:o(s).getTime()},e.push(t)}return e}();return function(e){for(var n=0;n=f.rules[m].s&&e[m].e<=f.rules[m].e)){d="N/A";break}if(d=0,d+=Math.abs(e[m].s-f.rules[m].s),864e6<(d+=Math.abs(f.rules[m].e-e[m].e))){d="N/A";break}}"N/A"!==(f=r(e,t,d,f))&&(o[l.name]=f)}for(var p in o)if(o.hasOwnProperty(p))for(e=0;ee?n[0]+",1":0n.length&&Array.isArray(n[0])&&(n=[{},n[0]]),r(e[0],n)})))}var i;if("string"==typeof n[0]&&f(n[1])&&(void 0===n[2]||Array.isArray(n[2]))){var c=n[0],s=n[1],d=n[2];(null===(i=n[3])||void 0===i||i)&&(i=u.setTimeout((function(){o(c)}),5e3),p[c]={timeout:i}),(i=l.createElement("script")).setAttribute("src",c),i.setAttribute("async","1"),D(i,"error",(function(){o(c),Ue.warn("Failed to load plugin "+s[0]+" from "+c)}),!0),D(i,"load",(function(){var n=s[1],r=u[s[0]];if(r&&"object"==typeof r){var i=r[n];n=e(r,["symbol"==typeof n?n:n+""]),h.addPlugin.apply(null,[{plugin:i.apply(null,d)},t]),a(n)}o(c)}),!0),l.head.appendChild(i)}else{if("object"==typeof n[0]&&"string"==typeof n[1]&&(void 0===n[2]||Array.isArray(n[2]))){var m=n[0],v=n[1];if(i=n[2],m)return n=m[v],m=e(m,["symbol"==typeof v?v:v+""]),h.addPlugin.apply(null,[{plugin:n.apply(null,i)},t]),void a(m)}Ue.warn("Failed to add Plugin: "+n[1])}}function s(){for(var e=[],t=0;t String? { + return Snowplow.tracker(namespace: message.tracker)?.session?.userId + } + + static func sessionId(_ message: GetParameterMessageReader) -> String? { + return Snowplow.tracker(namespace: message.tracker)?.session?.sessionId + } + + static func sessionIndex(_ message: GetParameterMessageReader) -> Int? { + return Snowplow.tracker(namespace: message.tracker)?.session?.sessionIndex + } + + static func setUserId(_ message: SetUserIdMessageReader) { + let trackerController = Snowplow.tracker(namespace: message.tracker) + trackerController?.subject?.userId = message.userId + } + + private static func trackEvent(_ event: Event, eventMessage: EventMessageReader, arguments: [String: Any]) { + eventMessage.addContextsToEvent(event, arguments: arguments) + let trackerController = Snowplow.tracker(namespace: eventMessage.tracker) + trackerController?.track(event) + } +} diff --git a/ios/Classes/SnowplowTrackerPlugin.h b/ios/Classes/SnowplowTrackerPlugin.h new file mode 100644 index 0000000..a0103b7 --- /dev/null +++ b/ios/Classes/SnowplowTrackerPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface SnowplowTrackerPlugin : NSObject +@end diff --git a/ios/Classes/SnowplowTrackerPlugin.m b/ios/Classes/SnowplowTrackerPlugin.m new file mode 100644 index 0000000..d9359d1 --- /dev/null +++ b/ios/Classes/SnowplowTrackerPlugin.m @@ -0,0 +1,15 @@ +#import "SnowplowTrackerPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "snowplow_tracker-Swift.h" +#endif + +@implementation SnowplowTrackerPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftSnowplowTrackerPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/ios/Classes/SwiftSnowplowTrackerPlugin.swift b/ios/Classes/SwiftSnowplowTrackerPlugin.swift new file mode 100644 index 0000000..8d3fcb5 --- /dev/null +++ b/ios/Classes/SwiftSnowplowTrackerPlugin.swift @@ -0,0 +1,151 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Flutter +import UIKit + +public class SwiftSnowplowTrackerPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "snowplow_tracker", binaryMessenger: registrar.messenger()) + let instance = SwiftSnowplowTrackerPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "createTracker": + onCreateTracker(call, result: result) + case "trackStructured": + onTrackStructured(call, result: result) + case "trackSelfDescribing": + onTrackSelfDescribing(call, result: result) + case "trackScreenView": + onTrackScreenView(call, result: result) + case "trackTiming": + onTrackTiming(call, result: result) + case "trackConsentGranted": + onTrackConsentGranted(call, result: result) + case "trackConsentWithdrawn": + onTrackConsentWithdrawn(call, result: result) + case "getSessionUserId": + onGetSessionUserId(call, result: result) + case "getSessionId": + onGetSessionId(call, result: result) + case "getSessionIndex": + onGetSessionIndex(call, result: result) + case "setUserId": + onSetUserId(call, result: result) + default: + result(FlutterMethodNotImplemented) + } + } + + private func onCreateTracker(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if let (message, arguments): (CreateTrackerMessageReader, [String: Any]) = decodeCall(call) { + SnowplowTrackerController.createTracker(message, arguments: arguments) + } + result(nil) + } + + private func onTrackStructured(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if let (message, arguments): (TrackStructuredMessageReader, [String: Any]) = decodeCall(call), + let (eventMessage, _): (EventMessageReader, Any) = decodeCall(call) { + SnowplowTrackerController.trackStructured(message, eventMessage: eventMessage, arguments: arguments) + } + result(nil) + } + + private func onTrackSelfDescribing(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if let (message, arguments): (TrackSelfDescribingMesssageReader, [String: Any]) = decodeCall(call), + let (eventMessage, _): (EventMessageReader, Any) = decodeCall(call) { + SnowplowTrackerController.trackSelfDescribing(message, eventMessage: eventMessage, arguments: arguments) + } + result(nil) + } + + private func onTrackScreenView(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if let (message, arguments): (TrackScreenViewMessageReader, [String: Any]) = decodeCall(call), + let (eventMessage, _): (EventMessageReader, Any) = decodeCall(call) { + SnowplowTrackerController.trackScreenView(message, eventMessage: eventMessage, arguments: arguments) + } + result(nil) + } + + private func onTrackTiming(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if let (message, arguments): (TrackTimingMessageReader, [String: Any]) = decodeCall(call), + let (eventMessage, _): (EventMessageReader, Any) = decodeCall(call) { + SnowplowTrackerController.trackTiming(message, eventMessage: eventMessage, arguments: arguments) + } + result(nil) + } + + private func onTrackConsentGranted(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if let (message, arguments): (TrackConsentGrantedMessageReader, [String: Any]) = decodeCall(call), + let (eventMessage, _): (EventMessageReader, Any) = decodeCall(call) { + SnowplowTrackerController.trackConsentGranted(message, eventMessage: eventMessage, arguments: arguments) + } + result(nil) + } + + private func onTrackConsentWithdrawn(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if let (message, arguments): (TrackConsentWithdrawnMessageReader, [String: Any]) = decodeCall(call), + let (eventMessage, _): (EventMessageReader, Any) = decodeCall(call) { + SnowplowTrackerController.trackConsentWithdrawn(message, eventMessage: eventMessage, arguments: arguments) + } + result(nil) + } + + private func onGetSessionUserId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if let (message, _): (GetParameterMessageReader, Any) = decodeCall(call) { + result(SnowplowTrackerController.sessionUserId(message)) + } else { + result(nil) + } + } + + private func onGetSessionId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if let (message, _): (GetParameterMessageReader, Any) = decodeCall(call) { + result(SnowplowTrackerController.sessionId(message)) + } else { + result(nil) + } + } + + private func onGetSessionIndex(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if let (message, _): (GetParameterMessageReader, Any) = decodeCall(call) { + result(SnowplowTrackerController.sessionIndex(message)) + } else { + result(nil) + } + } + + private func onSetUserId(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + if let (message, _): (SetUserIdMessageReader, Any) = decodeCall(call) { + SnowplowTrackerController.setUserId(message) + } + result(nil) + } + + private func decodeCall(_ call: FlutterMethodCall) -> (T, [String: Any])? { + let decoder = JSONDecoder() + let arguments: [String: Any] = call.arguments as? [String: Any] ?? [:] + + do { + let data = try JSONSerialization.data(withJSONObject: arguments, options: .prettyPrinted) + let message = try decoder.decode(T.self, from: data) + return (message, arguments) + } catch { + print(error.localizedDescription) + } + + return nil + } +} diff --git a/ios/Classes/TrackerVersion.swift b/ios/Classes/TrackerVersion.swift new file mode 100644 index 0000000..63bf3e6 --- /dev/null +++ b/ios/Classes/TrackerVersion.swift @@ -0,0 +1,16 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation + +class TrackerVersion { + static let TRACKER_VERSION = "flutter-0.1.0-dev.1" +} diff --git a/ios/Classes/readers/configurations/GdprConfigurationReader.swift b/ios/Classes/readers/configurations/GdprConfigurationReader.swift new file mode 100644 index 0000000..3eb6160 --- /dev/null +++ b/ios/Classes/readers/configurations/GdprConfigurationReader.swift @@ -0,0 +1,48 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct GdprConfigurationReader: Decodable { + let basisForProcessing: String + let documentId: String + let documentVersion: String + let documentDescription: String + + var basisForProcessingType: GDPRProcessingBasis { + switch (basisForProcessing) { + case "contract": + return .contract + case "legal_obligation": + return .legalObligation + case "legitimate_interests": + return .legitimateInterests + case "public_task": + return .publicTask + case "vital_interests": + return .vitalInterest + default: + return .consent + } + } +} + +extension GdprConfigurationReader { + func toConfiguration() -> GDPRConfiguration { + return GDPRConfiguration( + basis: basisForProcessingType, + documentId: documentId, + documentVersion: documentVersion, + documentDescription: documentDescription + ) + } +} diff --git a/ios/Classes/readers/configurations/NetworkConfigurationReader.swift b/ios/Classes/readers/configurations/NetworkConfigurationReader.swift new file mode 100644 index 0000000..e98d7fe --- /dev/null +++ b/ios/Classes/readers/configurations/NetworkConfigurationReader.swift @@ -0,0 +1,28 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct NetworkConfigurationReader: Decodable { + let endpoint: String + let method: String? +} + +extension NetworkConfigurationReader { + func toConfiguration() -> NetworkConfiguration { + if let m = method { + return NetworkConfiguration(endpoint: endpoint, method: m == "get" ? .get : .post) + } else { + return NetworkConfiguration(endpoint: endpoint, method: .post) + } + } +} diff --git a/ios/Classes/readers/configurations/SubjectConfigurationReader.swift b/ios/Classes/readers/configurations/SubjectConfigurationReader.swift new file mode 100644 index 0000000..5de3cb8 --- /dev/null +++ b/ios/Classes/readers/configurations/SubjectConfigurationReader.swift @@ -0,0 +1,63 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker +import CoreImage + +struct SubjectConfigurationReader: Decodable { + let userId: String? + let networkUserId: String? + let domainUserId: String? + let userAgent: String? + let ipAddress: String? + let timezone: String? + let language: String? + let screenResolution: [Double]? + let screenViewport: [Double]? + let colorDepth: Double? + + var screenResolutionSize: SPSize? { + if let screenResolution = self.screenResolution, + let width = screenResolution.first, + let height = screenResolution.last { + return SPSize(width: Int(width), height: Int(height)) + } + return nil + } + var screenViewportSize: SPSize? { + if let screenViewport = self.screenViewport, + let width = screenViewport.first, + let height = screenViewport.last { + return SPSize(width: Int(width), height: Int(height)) + } + return nil + } +} + +extension SubjectConfigurationReader { + func toConfiguration() -> SubjectConfiguration { + let configuration = SubjectConfiguration() + + if let uid = self.userId { configuration.userId(uid) } + if let nid = self.networkUserId { configuration.networkUserId(nid) } + if let did = self.domainUserId { configuration.domainUserId(did) } + if let ua = self.userAgent { configuration.useragent(ua) } + if let ip = self.ipAddress { configuration.ipAddress(ip) } + if let tz = self.timezone { configuration.timezone(tz) } + if let lang = self.language { configuration.language(lang) } + if let sr = self.screenResolutionSize { configuration.screenResolution(sr) } + if let sv = self.screenViewportSize { configuration.screenViewPort(sv) } + if let cd = self.colorDepth { configuration.colorDepth(NSNumber(value: Int(cd))) } + + return configuration + } +} diff --git a/ios/Classes/readers/configurations/TrackerConfigurationReader.swift b/ios/Classes/readers/configurations/TrackerConfigurationReader.swift new file mode 100644 index 0000000..af66711 --- /dev/null +++ b/ios/Classes/readers/configurations/TrackerConfigurationReader.swift @@ -0,0 +1,57 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct TrackerConfigurationReader: Decodable { + let appId: String? + let devicePlatform: String? + let base64Encoding: Bool? + let platformContext: Bool? + let geoLocationContext: Bool? + let sessionContext: Bool? + + var devicePlatformType: DevicePlatform? { + if let devicePlatform = self.devicePlatform { return SPStringToDevicePlatform(devicePlatform) } + return nil + } +} + +extension TrackerConfigurationReader { + static func defaultConfiguration() -> TrackerConfiguration { + let trackerConfig = TrackerConfiguration() + trackerConfig.trackerVersionSuffix(TrackerVersion.TRACKER_VERSION) + + trackerConfig.applicationContext(false) + trackerConfig.screenContext(false) + trackerConfig.screenViewAutotracking(false) + trackerConfig.lifecycleAutotracking(false) + trackerConfig.installAutotracking(false) + trackerConfig.exceptionAutotracking(false) + trackerConfig.diagnosticAutotracking(false) + + return trackerConfig + } + + func toConfiguration() -> TrackerConfiguration { + let trackerConfig = TrackerConfigurationReader.defaultConfiguration() + + if let appId = self.appId { trackerConfig.appId(appId) } + if let dp = self.devicePlatformType { trackerConfig.devicePlatform(dp) } + if let enc = self.base64Encoding { trackerConfig.base64Encoding(enc) } + if let pc = self.platformContext { trackerConfig.platformContext(pc) } + if let gc = self.geoLocationContext { trackerConfig.geoLocationContext(gc) } + if let sc = self.sessionContext { trackerConfig.sessionContext(sc) } + + return trackerConfig + } +} diff --git a/ios/Classes/readers/events/ConsentDocumentReader.swift b/ios/Classes/readers/events/ConsentDocumentReader.swift new file mode 100644 index 0000000..8aba74e --- /dev/null +++ b/ios/Classes/readers/events/ConsentDocumentReader.swift @@ -0,0 +1,29 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct ConsentDocumentReader: Decodable { + let documentId: String + let documentVersion: String + let documentName: String? + let documentDescription: String? +} + +extension ConsentDocumentReader { + func toConsentDocument() -> ConsentDocument { + let document = ConsentDocument(documentId: documentId, version: documentVersion) + if let name = self.documentName { document.name(name) } + if let description = self.documentDescription { document.documentDescription(description) } + return document + } +} diff --git a/ios/Classes/readers/events/ConsentGrantedReader.swift b/ios/Classes/readers/events/ConsentGrantedReader.swift new file mode 100644 index 0000000..79a273d --- /dev/null +++ b/ios/Classes/readers/events/ConsentGrantedReader.swift @@ -0,0 +1,35 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct ConsentGrantedReader: Decodable { + let expiry: String + let documentId: String + let version: String + let name: String? + let documentDescription: String? + let consentDocuments: [ConsentDocumentReader]? +} + +extension ConsentGrantedReader { + func toConsentGranted() -> ConsentGranted { + let event = ConsentGranted(expiry: expiry, documentId: documentId, version: version) + if let name = self.name { event.name(name) } + if let description = self.documentDescription { event.documentDescription(description) } + if let documents = self.consentDocuments { + let jsons = documents.map { $0.toConsentDocument().getPayload() } + event.documents(jsons) + } + return event + } +} diff --git a/ios/Classes/readers/events/ConsentWithdrawnReader.swift b/ios/Classes/readers/events/ConsentWithdrawnReader.swift new file mode 100644 index 0000000..1bca0ec --- /dev/null +++ b/ios/Classes/readers/events/ConsentWithdrawnReader.swift @@ -0,0 +1,38 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct ConsentWithdrawnReader: Decodable { + let all: Bool + let documentId: String + let version: String + let name: String? + let documentDescription: String? + let consentDocuments: [ConsentDocumentReader]? +} + +extension ConsentWithdrawnReader { + func toConsentWithdrawn() -> ConsentWithdrawn { + let event = ConsentWithdrawn() + .all(all) + .documentId(documentId) + .version(version) + if let name = self.name { event.name(name) } + if let description = self.documentDescription { event.documentDescription(description) } + if let documents = self.consentDocuments { + let jsons = documents.map { $0.toConsentDocument().getPayload() } + event.documents(jsons) + } + return event + } +} diff --git a/ios/Classes/readers/events/ScreenViewReader.swift b/ios/Classes/readers/events/ScreenViewReader.swift new file mode 100644 index 0000000..69c5c46 --- /dev/null +++ b/ios/Classes/readers/events/ScreenViewReader.swift @@ -0,0 +1,42 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct ScreenViewReader: Decodable { + let name: String + let id: String? + let type: String? + let previousName: String? + let previousType: String? + let previousId: String? + let transitionType: String? + + var idUUID: UUID? { + if let id = self.id { + return UUID(uuidString: id) + } + return nil + } +} + +extension ScreenViewReader { + func toScreenView() -> ScreenView { + let event = ScreenView(name: name, screenId: idUUID) + if let t = self.type { event.type(t) } + if let pn = self.previousName { event.previousName(pn) } + if let pt = self.previousType { event.previousType(pt) } + if let pi = self.previousId { event.previousId(pi) } + if let tt = self.transitionType { event.transitionType(tt) } + return event + } +} diff --git a/ios/Classes/readers/events/SelfDescribingJsonReader.swift b/ios/Classes/readers/events/SelfDescribingJsonReader.swift new file mode 100644 index 0000000..9391465 --- /dev/null +++ b/ios/Classes/readers/events/SelfDescribingJsonReader.swift @@ -0,0 +1,33 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct SelfDescribingJsonReader: Decodable { + let schema: String +} + +extension SelfDescribingJsonReader { + func toSelfDescribingJson(arguments: [String: Any]) -> SelfDescribingJson? { + if let data = arguments["data"] as? [String: Any] { + return SelfDescribingJson(schema: schema, andDictionary: data) + } + return nil + } + + func toSelfDescribing(arguments: [String: Any]) -> SelfDescribing? { + if let json = toSelfDescribingJson(arguments: arguments) { + return SelfDescribing(eventData: json) + } + return nil + } +} diff --git a/ios/Classes/readers/events/StructuredReader.swift b/ios/Classes/readers/events/StructuredReader.swift new file mode 100644 index 0000000..12de044 --- /dev/null +++ b/ios/Classes/readers/events/StructuredReader.swift @@ -0,0 +1,31 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct StructuredReader: Decodable { + let category: String + let action: String + let label: String? + let property: String? + let value: Double? +} + +extension StructuredReader { + func toStructured() -> Structured { + let event = Structured(category: category, action: action) + if let label = self.label { event.label(label) } + if let prop = self.property { event.property(prop) } + if let value = self.value { event.value(NSNumber(value: value)) } + return event + } +} diff --git a/ios/Classes/readers/events/TimingReader.swift b/ios/Classes/readers/events/TimingReader.swift new file mode 100644 index 0000000..ea2222c --- /dev/null +++ b/ios/Classes/readers/events/TimingReader.swift @@ -0,0 +1,28 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct TimingReader: Decodable { + let category: String + let variable: String + let timing: Int + let label: String? +} + +extension TimingReader { + func toTiming() -> Timing { + let event = Timing(category: category, variable: variable, timing: NSNumber(value: timing)) + if let label = self.label { event.label(label) } + return event + } +} diff --git a/ios/Classes/readers/messages/CreateTrackerMessageReader.swift b/ios/Classes/readers/messages/CreateTrackerMessageReader.swift new file mode 100644 index 0000000..8fa8146 --- /dev/null +++ b/ios/Classes/readers/messages/CreateTrackerMessageReader.swift @@ -0,0 +1,21 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct CreateTrackerMessageReader: Decodable { + let namespace: String + let networkConfig: NetworkConfigurationReader + let trackerConfig: TrackerConfigurationReader? + let subjectConfig: SubjectConfigurationReader? + let gdprConfig: GdprConfigurationReader? +} diff --git a/ios/Classes/readers/messages/EventMessageReader.swift b/ios/Classes/readers/messages/EventMessageReader.swift new file mode 100644 index 0000000..7e48454 --- /dev/null +++ b/ios/Classes/readers/messages/EventMessageReader.swift @@ -0,0 +1,30 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct EventMessageReader: Decodable { + let tracker: String + let contexts: [SelfDescribingJsonReader]? +} + +extension EventMessageReader { + func addContextsToEvent(_ event: Event, arguments: [String: Any]) { + if let readers = self.contexts, + let readersArgs = arguments["contexts"] as? [[String: Any]] { + let contexts = zip(readers, readersArgs).map { (reader, readerArgs) in + reader.toSelfDescribingJson(arguments: readerArgs) + }.compactMap { $0 } + event.contexts(NSMutableArray(array: contexts)) + } + } +} diff --git a/ios/Classes/readers/messages/GetParameterMessageReader.swift b/ios/Classes/readers/messages/GetParameterMessageReader.swift new file mode 100644 index 0000000..1468d14 --- /dev/null +++ b/ios/Classes/readers/messages/GetParameterMessageReader.swift @@ -0,0 +1,16 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation + +struct GetParameterMessageReader: Decodable { + let tracker: String +} diff --git a/ios/Classes/readers/messages/SetUserIdMessageReader.swift b/ios/Classes/readers/messages/SetUserIdMessageReader.swift new file mode 100644 index 0000000..3f9f77f --- /dev/null +++ b/ios/Classes/readers/messages/SetUserIdMessageReader.swift @@ -0,0 +1,17 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation + +struct SetUserIdMessageReader: Decodable { + let tracker: String + let userId: String? +} diff --git a/ios/Classes/readers/messages/TrackConsentGrantedMessageReader.swift b/ios/Classes/readers/messages/TrackConsentGrantedMessageReader.swift new file mode 100644 index 0000000..f4e49e4 --- /dev/null +++ b/ios/Classes/readers/messages/TrackConsentGrantedMessageReader.swift @@ -0,0 +1,23 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct TrackConsentGrantedMessageReader: Decodable { + let eventData: ConsentGrantedReader +} + +extension TrackConsentGrantedMessageReader { + func toConsentGranted() -> ConsentGranted { + return eventData.toConsentGranted() + } +} diff --git a/ios/Classes/readers/messages/TrackConsentWithdrawnMessageReader.swift b/ios/Classes/readers/messages/TrackConsentWithdrawnMessageReader.swift new file mode 100644 index 0000000..a447634 --- /dev/null +++ b/ios/Classes/readers/messages/TrackConsentWithdrawnMessageReader.swift @@ -0,0 +1,23 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct TrackConsentWithdrawnMessageReader: Decodable { + let eventData: ConsentWithdrawnReader +} + +extension TrackConsentWithdrawnMessageReader { + func toConsentWithdrawn() -> ConsentWithdrawn { + return eventData.toConsentWithdrawn() + } +} diff --git a/ios/Classes/readers/messages/TrackScreenViewMessageReader.swift b/ios/Classes/readers/messages/TrackScreenViewMessageReader.swift new file mode 100644 index 0000000..1597508 --- /dev/null +++ b/ios/Classes/readers/messages/TrackScreenViewMessageReader.swift @@ -0,0 +1,23 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct TrackScreenViewMessageReader: Decodable { + let eventData: ScreenViewReader +} + +extension TrackScreenViewMessageReader { + func toScreenView() -> ScreenView { + return eventData.toScreenView() + } +} diff --git a/ios/Classes/readers/messages/TrackSelfDescribingMesssageReader.swift b/ios/Classes/readers/messages/TrackSelfDescribingMesssageReader.swift new file mode 100644 index 0000000..5e76068 --- /dev/null +++ b/ios/Classes/readers/messages/TrackSelfDescribingMesssageReader.swift @@ -0,0 +1,26 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct TrackSelfDescribingMesssageReader: Decodable { + let eventData: SelfDescribingJsonReader +} + +extension TrackSelfDescribingMesssageReader { + func toSelfDescribing(arguments: [String: Any]) -> SelfDescribing? { + if let eventArgs = arguments["eventData"] as? [String: Any] { + return eventData.toSelfDescribing(arguments: eventArgs) + } + return nil + } +} diff --git a/ios/Classes/readers/messages/TrackStructuredMessageReader.swift b/ios/Classes/readers/messages/TrackStructuredMessageReader.swift new file mode 100644 index 0000000..f997cb4 --- /dev/null +++ b/ios/Classes/readers/messages/TrackStructuredMessageReader.swift @@ -0,0 +1,23 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct TrackStructuredMessageReader: Decodable { + let eventData: StructuredReader +} + +extension TrackStructuredMessageReader { + func toStructured() -> Structured { + return eventData.toStructured() + } +} diff --git a/ios/Classes/readers/messages/TrackTimingMessageReader.swift b/ios/Classes/readers/messages/TrackTimingMessageReader.swift new file mode 100644 index 0000000..8a7fe06 --- /dev/null +++ b/ios/Classes/readers/messages/TrackTimingMessageReader.swift @@ -0,0 +1,23 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import Foundation +import SnowplowTracker + +struct TrackTimingMessageReader: Decodable { + let eventData: TimingReader +} + +extension TrackTimingMessageReader { + func toTiming() -> Timing { + return eventData.toTiming() + } +} diff --git a/ios/snowplow_tracker.podspec b/ios/snowplow_tracker.podspec new file mode 100644 index 0000000..843617d --- /dev/null +++ b/ios/snowplow_tracker.podspec @@ -0,0 +1,24 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint snowplow_tracker.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'snowplow_tracker' + s.version = '0.1.0-dev.1' + s.summary = 'A package for tracking Snowplow events in Flutter apps.' + s.description = <<-DESC +A package for tracking Snowplow events in Flutter apps. + DESC + s.homepage = 'https://github.com/snowplow-incubator/snowplow-flutter-tracker' + s.license = { :file => '../LICENSE' } + s.author = { 'Snowplow Analytics Ltd' => 'support@snowplowanalytics.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.dependency 'SnowplowTracker', '~> 3.0.2' + s.platform = :ios, '9.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/lib/configurations/configuration.dart b/lib/configurations/configuration.dart new file mode 100644 index 0000000..70a51d4 --- /dev/null +++ b/lib/configurations/configuration.dart @@ -0,0 +1,56 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; +import 'package:snowplow_tracker/configurations/gdpr_configuration.dart'; +import 'package:snowplow_tracker/configurations/network_configuration.dart'; +import 'package:snowplow_tracker/configurations/subject_configuration.dart'; +import 'package:snowplow_tracker/configurations/tracker_configuration.dart'; + +/// Wraps configuration used to initialize a tracker. +/// +/// {@category Initialization and configuration} +@immutable +class Configuration { + /// Unique namespace to identify the tracker. + final String namespace; + + /// Network configuration. + final NetworkConfiguration networkConfig; + + /// Configuration of tracker features. + final TrackerConfiguration? trackerConfig; + + /// Configuration of subject information added to events. + final SubjectConfiguration? subjectConfig; + + /// Configuration of GDPR context attached to events. + final GdprConfiguration? gdprConfig; + + const Configuration( + {required this.namespace, + required this.networkConfig, + this.trackerConfig, + this.subjectConfig, + this.gdprConfig}); + + Map toMap() { + final conf = { + 'namespace': namespace, + 'networkConfig': networkConfig.toMap(), + 'trackerConfig': trackerConfig?.toMap(), + 'subjectConfig': subjectConfig?.toMap(), + 'gdprConfig': gdprConfig?.toMap() + }; + conf.removeWhere((key, value) => value == null); + return conf; + } +} diff --git a/lib/configurations/gdpr_configuration.dart b/lib/configurations/gdpr_configuration.dart new file mode 100644 index 0000000..317d45c --- /dev/null +++ b/lib/configurations/gdpr_configuration.dart @@ -0,0 +1,47 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +/// Determines the GDPR context that will be attached to all events sent by the tracker. +/// +/// {@category Initialization and configuration} +@immutable +class GdprConfiguration { + /// Basis for processing. + final String basisForProcessing; + + /// ID of a GDPR basis document. + final String documentId; + + /// Version of the document. + final String documentVersion; + + /// Description of the document. + final String documentDescription; + + const GdprConfiguration( + {required this.basisForProcessing, + required this.documentId, + required this.documentVersion, + required this.documentDescription}); + + Map toMap() { + final conf = { + 'basisForProcessing': basisForProcessing, + 'documentId': documentId, + 'documentVersion': documentVersion, + 'documentDescription': documentDescription, + }; + conf.removeWhere((key, value) => value == null); + return conf; + } +} diff --git a/lib/configurations/network_configuration.dart b/lib/configurations/network_configuration.dart new file mode 100644 index 0000000..3c05f73 --- /dev/null +++ b/lib/configurations/network_configuration.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +/// Configuration of the collector network endpoint. +/// +/// {@category Initialization and configuration} +@immutable +class NetworkConfiguration { + /// Endpoint of the collector, e.g. `http://localhost:9090` + final String endpoint; + + /// Choice of GET or POST (default) HTTP method used to send events to the collector. + final Method? method; + + const NetworkConfiguration({required this.endpoint, this.method}); + + Map toMap() { + final conf = { + 'endpoint': endpoint, + 'method': method?.name + }; + conf.removeWhere((key, value) => value == null); + return conf; + } +} + +enum Method { get, post } diff --git a/lib/configurations/subject_configuration.dart b/lib/configurations/subject_configuration.dart new file mode 100644 index 0000000..3f82242 --- /dev/null +++ b/lib/configurations/subject_configuration.dart @@ -0,0 +1,132 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +/// Subject information about tracked user and device that is added to events. +/// +/// {@category Initialization and configuration} +@immutable +class SubjectConfiguration { + /// Business ID of the user. + final String? userId; + + /// Network user ID (UUIDv4). + /// + /// Populates the `network_userid` field. + /// Typically used to link native tracking to in-app browser events tracked using the JavaScript Tracker. + /// Normally one would retrieve the network userid from the browser and pass it to the app. + /// + /// Only configurable on iOS and Android. Ignored on Web where it is automatically assigned. + final String? networkUserId; + + /// Domain user ID (UUIDv4). + /// + /// Populates the `domain_userid` field. + /// Typically used to link native tracking to in-app browser events tracked using the JavaScript Tracker. + /// + /// Only configurable on iOS and Android. Ignored on Web where it is automatically assigned. + final String? domainUserId; + + /// Custom user-agent. It overrides the user-agent used by default. + /// + /// Populates the `useragent` field. + /// + /// Only configurable on iOS and Android. Ignored on Web where it is automatically assigned. + final String? userAgent; + + /// Custom IP address. It overrides the IP address used by default. + /// + /// Populates the `user_ipaddress` field. + /// + /// Only configurable on iOS and Android. Ignored on Web where it is automatically assigned. + final String? ipAddress; + + /// The timezone label. + /// + /// Populates the `os_timezone` field. + /// + /// Only configurable on iOS and Android. Ignored on Web where it is automatically assigned. + final String? timezone; + + /// The language set on the device. + /// + /// Populates the `lang` field. + /// + /// Only configurable on iOS and Android. Ignored on Web where it is automatically assigned. + final String? language; + + /// The screen resolution on the device. + /// + /// Populates the event fields `dvce_screenwidth` and `dvce_screenheight`. + /// + /// Only configurable on iOS and Android. Ignored on Web where it is automatically assigned. + final Size? screenResolution; + + /// The screen viewport. + /// + /// Populates the event fields `br_viewwidth` and `br_viewheight`. + /// + /// Only configurable on iOS and Android. Ignored on Web where it is automatically assigned. + final Size? screenViewport; + + /// The color depth. + /// + /// Populates the `br_colordepth` field. + /// + /// Only configurable on iOS and Android. Ignored on Web where it is automatically assigned. + final double? colorDepth; + + const SubjectConfiguration( + {this.userId, + this.networkUserId, + this.domainUserId, + this.userAgent, + this.ipAddress, + this.timezone, + this.language, + this.screenResolution, + this.screenViewport, + this.colorDepth}); + + Map toMap() { + final conf = { + 'userId': userId, + 'networkUserId': networkUserId, + 'domainUserId': domainUserId, + 'userAgent': userAgent, + 'ipAddress': ipAddress, + 'timezone': timezone, + 'language': language, + 'screenResolution': screenResolution?.toList(), + 'screenViewport': screenViewport?.toList(), + 'colorDepth': colorDepth + }; + conf.removeWhere((key, value) => value == null); + return conf; + } +} + +/// Resolution or viewport of screen. +@immutable +class Size { + /// Width in pixels. + final double width; + + /// Height in pixels. + final double height; + + const Size({required this.width, required this.height}); + + List toList() { + return [width, height]; + } +} diff --git a/lib/configurations/tracker_configuration.dart b/lib/configurations/tracker_configuration.dart new file mode 100644 index 0000000..75681a0 --- /dev/null +++ b/lib/configurations/tracker_configuration.dart @@ -0,0 +1,105 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +/// Configuration of the tracker and the core tracker properties. +/// +/// Indicates what should be tracked in terms of automatic tracking and contexts/entities to attach to the events. +/// {@category Sessions and data model} +/// {@category Initialization and configuration} +@immutable +class TrackerConfiguration { + /// Identifier of the app. + /// + /// Defaults to null on Web, product bundle identifier on iOS/Android + final String? appId; + + /// The device platform the tracker runs on. + /// + /// Defaults to "web" on Web and "mob" on iOS/Android + final DevicePlatform? devicePlatform; + + /// Indicates whether payload JSON data should be base64 encoded. + /// + /// Defaults to true. + final bool? base64Encoding; + + /// Indicates whether platform context should be attached to tracked events. + /// + /// Defaults to true on iOS and Android. Not available on Web. + final bool? platformContext; + + /// Indicates whether geo-location context should be attached to tracked events. + /// + /// Defaults to false. + final bool? geoLocationContext; + + /// Indicates whether session context should be attached to tracked events. + /// + /// Defaults to true. + final bool? sessionContext; + + /// Indicates whether context about current web page should be attached to tracked events. + /// + /// Only available on Web, defaults to true. + final bool? webPageContext; + + const TrackerConfiguration( + {this.appId, + this.devicePlatform, + this.base64Encoding, + this.platformContext, + this.geoLocationContext, + this.sessionContext, + this.webPageContext}); + + Map toMap() { + final conf = { + 'appId': appId, + 'devicePlatform': devicePlatform?.name, + 'base64Encoding': base64Encoding, + 'platformContext': platformContext, + 'geoLocationContext': geoLocationContext, + 'sessionContext': sessionContext, + 'webPageContext': webPageContext, + }; + conf.removeWhere((key, value) => value == null); + return conf; + } +} + +/// Device platform the tracker runs on +enum DevicePlatform { + /// Mobile/Tablet, + mob, + + /// Web + web, + + /// Desktop/Laptop + pc, + + /// Server-side app + srv, + + /// General app + app, + + /// Connected TV + tv, + + /// Games Console + cnsl, + + /// Internet of things + iot +} diff --git a/lib/events/consent_granted.dart b/lib/events/consent_granted.dart new file mode 100644 index 0000000..2dd93fb --- /dev/null +++ b/lib/events/consent_granted.dart @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +import 'package:snowplow_tracker/events/event.dart'; + +/// Event used to track a user opting into data collection. +/// +/// A consent document context will be attached to the event using the [documentId] and [version] arguments supplied. +/// {@category Tracking events} +/// {@category Adding data to your events} +@immutable +class ConsentGranted implements Event { + /// The expiry date-time of the consent. + final DateTime expiry; + + /// The consent document ID. + final String documentId; + + /// The consent document version. + final String version; + + /// Optional consent document name. + final String? name; + + /// Optional consent document description. + final String? documentDescription; + + const ConsentGranted( + {required this.expiry, + required this.documentId, + required this.version, + this.name, + this.documentDescription}); + + @override + String endpoint() { + return 'trackConsentGranted'; + } + + @override + Map toMap() { + final data = { + 'expiry': expiry.toIso8601String(), + 'documentId': documentId, + 'version': version, + 'name': name, + 'documentDescription': documentDescription + }; + data.removeWhere((key, value) => value == null); + return data; + } +} diff --git a/lib/events/consent_withdrawn.dart b/lib/events/consent_withdrawn.dart new file mode 100644 index 0000000..89da5fa --- /dev/null +++ b/lib/events/consent_withdrawn.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +import 'package:snowplow_tracker/events/event.dart'; + +/// Event to track a user withdrawing consent for data collection. +/// +/// A consent document context will be attached to the event using the id and version arguments supplied. +/// To specify that a user opts out of all data collection, [all] should be set to true. +/// {@category Tracking events} +/// {@category Adding data to your events} +@immutable +class ConsentWithdrawn implements Event { + /// Whether user opts out of all data collection. + final bool all; + + /// The consent document ID. + final String documentId; + + /// The consent document version. + final String version; + + /// Optional consent document name. + final String? name; + + /// Optional consent document description. + final String? documentDescription; + + const ConsentWithdrawn( + {required this.all, + required this.documentId, + required this.version, + this.name, + this.documentDescription}); + + @override + String endpoint() { + return 'trackConsentWithdrawn'; + } + + @override + Map toMap() { + final data = { + 'all': all, + 'documentId': documentId, + 'version': version, + 'name': name, + 'documentDescription': documentDescription + }; + data.removeWhere((key, value) => value == null); + return data; + } +} diff --git a/lib/events/event.dart b/lib/events/event.dart new file mode 100644 index 0000000..da249e3 --- /dev/null +++ b/lib/events/event.dart @@ -0,0 +1,19 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +/// Base class for all tracked events. +@immutable +abstract class Event { + String endpoint(); + Map toMap(); +} diff --git a/lib/events/screen_view.dart b/lib/events/screen_view.dart new file mode 100644 index 0000000..0a20ec5 --- /dev/null +++ b/lib/events/screen_view.dart @@ -0,0 +1,71 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +import 'package:snowplow_tracker/events/event.dart'; + +/// Event to track user viewing a screen within the application. +/// +/// {@category Tracking events} +/// {@category Adding data to your events} +@immutable +class ScreenView implements Event { + /// The name of the screen viewed. + final String name; + + /// The id (UUID v4) of screen that was viewed. + final String id; + + /// The type of screen that was viewed. + final String? type; + + /// The name of the previous screen that was viewed. + final String? previousName; + + /// The type of screen that was viewed. + final String? previousType; + + /// The id (UUID v4) of the previous screen that was viewed. + final String? previousId; + + /// The type of transition that led to the screen being viewed. + final String? transitionType; + + const ScreenView( + {required this.name, + required this.id, + this.type, + this.previousName, + this.previousType, + this.previousId, + this.transitionType}); + + @override + String endpoint() { + return 'trackScreenView'; + } + + @override + Map toMap() { + final data = { + 'name': name, + 'id': id, + 'type': type, + 'previousName': previousName, + 'previousType': previousType, + 'previousId': previousId, + 'transitionType': transitionType, + }; + data.removeWhere((key, value) => value == null); + return data; + } +} diff --git a/lib/events/self_describing.dart b/lib/events/self_describing.dart new file mode 100644 index 0000000..77910b9 --- /dev/null +++ b/lib/events/self_describing.dart @@ -0,0 +1,51 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +import 'package:snowplow_tracker/events/event.dart'; + +/// Event to track custom information that does not fit into the out-of-the box events. +/// +/// Self-describing events are a [data structure based on JSON Schemas](https://docs.snowplowanalytics.com/docs/understanding-tracking-design/understanding-schemas-and-validation/) and can have arbitrarily many fields. +/// To define your own custom self-describing event, you must create a JSON schema for that event and upload it to an [Iglu Schema Repository](https://github.com/snowplow/iglu) using [igluctl](https://docs.snowplowanalytics.com/docs/open-source-components-and-applications/iglu/) (or if a Snowplow BDP customer, you can use the [Snowplow BDP Console UI](https://docs.snowplowanalytics.com/docs/understanding-tracking-design/managing-data-structures/) or [Data Structures API](https://docs.snowplowanalytics.com/docs/understanding-tracking-design/managing-data-structures-via-the-api-2/)). +/// Snowplow uses the schema to validate that the JSON containing the event properties is well-formed. +/// {@category Tracking events} +/// {@category Adding data to your events} +@immutable +class SelfDescribing implements Event { + /// A valid Iglu schema path. + /// + /// This must point to the location of the custom event’s schema, of the format: `iglu:{vendor}/{name}/{format}/{version}`. + final String schema; + + /// The custom data for the event. + /// + /// This data must conform to the schema specified in the schema argument, or the event will fail validation and land in bad rows. + final dynamic data; + + const SelfDescribing({required this.schema, required this.data}); + + @override + String endpoint() { + return 'trackSelfDescribing'; + } + + @override + Map toMap() { + final map = { + 'schema': schema, + 'data': data, + }; + map.removeWhere((key, value) => value == null); + return map; + } +} diff --git a/lib/events/structured.dart b/lib/events/structured.dart new file mode 100644 index 0000000..d832a26 --- /dev/null +++ b/lib/events/structured.dart @@ -0,0 +1,68 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +import 'package:snowplow_tracker/events/event.dart'; + +/// Event to capture custom consumer interactions without the need to define a custom schema. +/// {@category Tracking events} +/// {@category Adding data to your events} +@immutable +class Structured implements Event { + /// Name you for the group of objects you want to track e.g. "media", "ecomm". + final String category; + + /// Defines the type of user interaction for the web object. + /// + /// E.g., "play-video", "add-to-basket". + final String action; + + /// Identifies the specific object being actioned. + /// + /// E.g., ID of the video being played, or the SKU or the product added-to-basket. + final String? label; + + /// Describes the object or the action performed on it. + /// + /// This might be the quantity of an item added to basket + final String? property; + + /// Quantifies or further describes the user action. + /// + /// This might be the price of an item added-to-basket, or the starting time of the video where play was just pressed. + final double? value; + + const Structured( + {required this.category, + required this.action, + this.label, + this.property, + this.value}); + + @override + String endpoint() { + return 'trackStructured'; + } + + @override + Map toMap() { + final data = { + 'category': category, + 'action': action, + 'label': label, + 'property': property, + 'value': value, + }; + data.removeWhere((key, value) => value == null); + return data; + } +} diff --git a/lib/events/timing.dart b/lib/events/timing.dart new file mode 100644 index 0000000..12c9ebf --- /dev/null +++ b/lib/events/timing.dart @@ -0,0 +1,55 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +import 'package:snowplow_tracker/events/event.dart'; + +/// Event used to track user timing events such as how long resources take to load. +/// {@category Tracking events} +/// {@category Adding data to your events} +@immutable +class Timing implements Event { + /// Defines the timing category. + final String category; + + /// Defines the timing variable measured. + final String variable; + + /// Represents the time. + final int timing; + + /// An optional string to further identify the timing event. + final String? label; + + const Timing( + {required this.category, + required this.variable, + required this.timing, + this.label}); + + @override + String endpoint() { + return 'trackTiming'; + } + + @override + Map toMap() { + final data = { + 'category': category, + 'variable': variable, + 'timing': timing, + 'label': label, + }; + data.removeWhere((key, value) => value == null); + return data; + } +} diff --git a/lib/js/sp_session_context_plugin.js b/lib/js/sp_session_context_plugin.js new file mode 100644 index 0000000..8cab708 --- /dev/null +++ b/lib/js/sp_session_context_plugin.js @@ -0,0 +1,115 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +var plugin = { + SessionContextPlugin: function () { + return { + + getFirstEventId: function (sessionId, eventId) { + if (typeof (Storage) !== "undefined") { + var storage = window.localStorage; + var stored = storage.getItem('spl_session'); + if (stored) { + stored = JSON.parse(stored); + if (stored.sessionId == sessionId) { + return stored.eventId; + } + } + storage.setItem('spl_session', JSON.stringify({ + sessionId: sessionId, + eventId: eventId + })); + return eventId; + } + return null; + }, + + beforeTrack: function (payloadBuilder) { + var payload = payloadBuilder.build(); + + var sessionUserId = payload['duid']; + var sessionId = payload['sid']; + var sessionIndex = parseInt(payload['vid']); + var eventId = payload['eid']; + + if (!sessionUserId || !sessionId) { return; } + + var context = { + 'schema': 'iglu:com.snowplowanalytics.snowplow/contexts/jsonschema/1-0-0', + 'data': [] + }; + if (payload.cx) { + context = JSON.parse(atob(payload.cx)); + } + var sessionContext = { + 'schema': 'iglu:com.snowplowanalytics.snowplow/client_session/jsonschema/1-0-1', + 'data': { + 'userId': sessionUserId, + 'sessionId': sessionId, + 'sessionIndex': sessionIndex, + 'previousSessionId': null, + 'storageMechanism': 'COOKIE_1', + 'firstEventId': this.getFirstEventId(sessionId, eventId) + } + }; + context.data.push(sessionContext); + + payloadBuilder.addJson('cx', 'co', context); + } + }; + } +}; + +function addSessionContextPlugin(tracker) { + window.snowplow('addPlugin:' + tracker, plugin, 'SessionContextPlugin'); +} + +function getSnowplowCookieParts(cookieName) { + cookieName = cookieName || '_sp_'; + var matcher = new RegExp(cookieName + 'id\\.[a-f0-9]+=([^;]+);?'); + var match = document.cookie.match(matcher); + if (match && match[1]) { + return match[1].split('.'); + } else { + return null; + } +} + +function getSnowplowSid(cookieName) { // session id + var parts = getSnowplowCookieParts(cookieName); + if (parts != null) { + return parts[parts.length - 1]; + } else { + return null; + } +} + +function getSnowplowVid(cookieName) { // session index + var parts = getSnowplowCookieParts(cookieName); + if (parts != null) { + return parseInt(parts[2]); + } else { + return null; + } +} + +function getSnowplowDuid(cookieName) { // domain user id + var parts = getSnowplowCookieParts(cookieName); + if (parts != null) { + return parts[0]; + } else { + return null; + } +} + +function isSnowplowInstalled() { + return window.snowplow != undefined; +} diff --git a/lib/snowplow.dart b/lib/snowplow.dart new file mode 100644 index 0000000..dcb94f0 --- /dev/null +++ b/lib/snowplow.dart @@ -0,0 +1,92 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:snowplow_tracker/configurations/configuration.dart'; +import 'package:snowplow_tracker/configurations/gdpr_configuration.dart'; +import 'package:snowplow_tracker/configurations/network_configuration.dart'; +import 'package:snowplow_tracker/configurations/subject_configuration.dart'; +import 'package:snowplow_tracker/configurations/tracker_configuration.dart'; +import 'package:snowplow_tracker/events/event.dart'; +import 'package:snowplow_tracker/events/self_describing.dart'; +import 'package:snowplow_tracker/tracker.dart'; + +/// Main interface for the package mainly used to initialize trackers and track events. +/// +/// {@category Getting started} +/// {@category Initialization and configuration} +class Snowplow { + static const MethodChannel _channel = MethodChannel('snowplow_tracker'); + + /// Creates a new tracker instance with the given unique [namespace]. + /// + /// [endpoint] refers to the Snowplow collector endpoint. + /// [method] is the HTTP method used to send events to collector and it defaults to POST. + static Future createTracker( + {required String namespace, + required String endpoint, + Method? method, + TrackerConfiguration? trackerConfig, + SubjectConfiguration? subjectConfig, + GdprConfiguration? gdprConfig}) async { + final configuration = Configuration( + namespace: namespace, + networkConfig: NetworkConfiguration(endpoint: endpoint, method: method), + trackerConfig: trackerConfig, + subjectConfig: subjectConfig, + gdprConfig: gdprConfig); + await _channel.invokeMethod('createTracker', configuration.toMap()); + return Tracker(namespace: configuration.namespace); + } + + /// Tracks the given event using the specified [tracker] namespace and with optional context entities. + static Future track(Event event, + {required String tracker, List? contexts}) async { + var message = { + 'tracker': tracker, + 'eventData': event.toMap(), + 'contexts': contexts?.map((c) => c.toMap()).toList() + }; + message.removeWhere((key, value) => value == null); + await _channel.invokeMethod(event.endpoint(), message); + } + + /// Sets the business user ID to the string for the [tracker] namespace. + static Future setUserId(String? userId, + {required String tracker}) async { + await _channel + .invokeMethod('setUserId', {'tracker': tracker, 'userId': userId}); + } + + /// Returns the identifier (string UUIDv4) for the user of the session. + /// + /// The [tracker] namespace is required but ignored on Web where all trackers share the same session. + static Future getSessionUserId({required String tracker}) async { + return await _channel + .invokeMethod('getSessionUserId', {'tracker': tracker}); + } + + /// Returns the identifier (string UUIDv4) for the session. + /// + /// The [tracker] namespace is required but ignored on Web where all trackers share the same session. + static Future getSessionId({required String tracker}) async { + return await _channel.invokeMethod('getSessionId', {'tracker': tracker}); + } + + /// Returns the index (number) of the current session for this user. + /// + /// The [tracker] namespace is required but ignored on Web where all trackers share the same session. + static Future getSessionIndex({required String tracker}) async { + return await _channel.invokeMethod('getSessionIndex', {'tracker': tracker}); + } +} diff --git a/lib/snowplow_tracker.dart b/lib/snowplow_tracker.dart new file mode 100644 index 0000000..00903e2 --- /dev/null +++ b/lib/snowplow_tracker.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +export 'snowplow.dart'; +export 'tracker.dart'; + +export 'configurations/gdpr_configuration.dart'; +export 'configurations/network_configuration.dart'; +export 'configurations/subject_configuration.dart'; +export 'configurations/tracker_configuration.dart'; + +export 'events/consent_granted.dart'; +export 'events/consent_withdrawn.dart'; +export 'events/screen_view.dart'; +export 'events/self_describing.dart'; +export 'events/structured.dart'; +export 'events/timing.dart'; diff --git a/lib/snowplow_tracker_plugin_web.dart b/lib/snowplow_tracker_plugin_web.dart new file mode 100644 index 0000000..e05e247 --- /dev/null +++ b/lib/snowplow_tracker_plugin_web.dart @@ -0,0 +1,142 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'dart:async'; +import 'dart:html' as html; + +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:snowplow_tracker/src/web/sp.dart'; +import 'src/web/readers/configurations/configuration_reader.dart'; +import 'src/web/readers/messages/event_message_reader.dart'; +import 'src/web/readers/messages/set_user_id_message_reader.dart'; +import 'src/web/snowplow_tracker_controller.dart'; + +class SnowplowTrackerPluginWeb { + static void registerWith(Registrar registrar) { + final MethodChannel channel = MethodChannel( + 'snowplow_tracker', + const StandardMethodCodec(), + registrar, + ); + + final pluginInstance = SnowplowTrackerPluginWeb(); + channel.setMethodCallHandler(pluginInstance.handleMethodCall); + + // Add JS plugin for client session context + const clientSessionJSFile = + 'assets/packages/snowplow_tracker/js/sp_session_context_plugin.js'; + var foundClientSessionJSFile = false; + for (html.ScriptElement script + in html.document.head!.querySelectorAll('script')) { + if (script.src.contains(clientSessionJSFile)) { + foundClientSessionJSFile = true; + } + } + if (!foundClientSessionJSFile) { + html.document.body!.append(html.ScriptElement() + ..src = clientSessionJSFile + ..type = 'application/javascript'); + } + } + + Future handleMethodCall(MethodCall call) async { + if (!isSnowplowInstalled()) { + throw PlatformException( + code: 'Unimplemented', + details: 'Snowplow JS tracker is not installed', + ); + } + + switch (call.method) { + case 'createTracker': + return onCreateTracker(call); + case 'trackStructured': + return onTrackStructured(call); + case 'trackSelfDescribing': + return onTrackSelfDescribing(call); + case 'trackScreenView': + return onTrackScreenView(call); + case 'trackTiming': + return onTrackTiming(call); + case 'trackConsentGranted': + return onTrackConsentGranted(call); + case 'trackConsentWithdrawn': + return onTrackConsentWithdrawn(call); + case 'setUserId': + return onSetUserId(call); + case "getSessionUserId": + return onGetSessionUserId(call); + case "getSessionId": + return onGetSessionId(call); + case "getSessionIndex": + return onGetSessionIndex(call); + default: + throw PlatformException( + code: 'Unimplemented', + details: + 'snowplow_tracker for web doesn\'t implement \'${call.method}\'', + ); + } + } + + void onCreateTracker(MethodCall call) { + var configuration = ConfigurationReader(call.arguments); + SnowplowTrackerController.createTracker(configuration); + } + + void onTrackStructured(MethodCall call) { + var message = EventMessageReader.withStructured(call.arguments); + SnowplowTrackerController.trackEvent(message); + } + + void onTrackSelfDescribing(MethodCall call) { + var message = EventMessageReader.withSelfDescribing(call.arguments); + SnowplowTrackerController.trackEvent(message); + } + + void onTrackScreenView(MethodCall call) { + var message = EventMessageReader.withScreenView(call.arguments); + SnowplowTrackerController.trackEvent(message); + } + + void onTrackTiming(MethodCall call) { + var message = EventMessageReader.withTiming(call.arguments); + SnowplowTrackerController.trackEvent(message); + } + + void onTrackConsentGranted(MethodCall call) { + var message = EventMessageReader.withConsentGranted(call.arguments); + SnowplowTrackerController.trackEvent(message); + } + + void onTrackConsentWithdrawn(MethodCall call) { + var message = EventMessageReader.withConsentWithdrawn(call.arguments); + SnowplowTrackerController.trackEvent(message); + } + + void onSetUserId(MethodCall call) { + var message = SetUserIdMessageReader(call.arguments); + SnowplowTrackerController.setUserId(message); + } + + String? onGetSessionUserId(MethodCall call) { + return SnowplowTrackerController.getSessionUserId(); + } + + String? onGetSessionId(MethodCall call) { + return SnowplowTrackerController.getSessionId(); + } + + int? onGetSessionIndex(MethodCall call) { + return SnowplowTrackerController.getSessionIndex(); + } +} diff --git a/lib/src/web/readers/configurations/configuration_reader.dart b/lib/src/web/readers/configurations/configuration_reader.dart new file mode 100644 index 0000000..ab8f021 --- /dev/null +++ b/lib/src/web/readers/configurations/configuration_reader.dart @@ -0,0 +1,45 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:snowplow_tracker/configurations/configuration.dart'; +import 'gdpr_configuration_reader.dart'; +import 'network_configuration_reader.dart'; +import 'subject_configuration_reader.dart'; +import 'tracker_configuration_reader.dart'; + +class ConfigurationReader extends Configuration { + ConfigurationReader(dynamic map) + : super( + namespace: map['namespace'], + networkConfig: NetworkConfigurationReader(map['networkConfig']), + trackerConfig: map['trackerConfig'] != null + ? TrackerConfigurationReader(map['trackerConfig']) + : null, + subjectConfig: map['subjectConfig'] != null + ? SubjectConfigurationReader(map['subjectConfig']) + : null, + gdprConfig: map['gdprConfig'] != null + ? GdprConfigurationReader(map['gdprConfig']) + : null); + + bool get addSessionContext => trackerConfig?.sessionContext ?? true; + + dynamic getTrackerOptions() { + var options = {}; + + (networkConfig as NetworkConfigurationReader).addTrackerOptions(options); + if (trackerConfig != null) { + (trackerConfig as TrackerConfigurationReader).addTrackerOptions(options); + } + + return options; + } +} diff --git a/lib/src/web/readers/configurations/gdpr_configuration_reader.dart b/lib/src/web/readers/configurations/gdpr_configuration_reader.dart new file mode 100644 index 0000000..0a26138 --- /dev/null +++ b/lib/src/web/readers/configurations/gdpr_configuration_reader.dart @@ -0,0 +1,21 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:snowplow_tracker/configurations/gdpr_configuration.dart'; + +class GdprConfigurationReader extends GdprConfiguration { + GdprConfigurationReader(dynamic map) + : super( + basisForProcessing: map['basisForProcessing'], + documentId: map['documentId'], + documentVersion: map['documentVersion'], + documentDescription: map['documentDescription']); +} diff --git a/lib/src/web/readers/configurations/network_configuration_reader.dart b/lib/src/web/readers/configurations/network_configuration_reader.dart new file mode 100644 index 0000000..3d07452 --- /dev/null +++ b/lib/src/web/readers/configurations/network_configuration_reader.dart @@ -0,0 +1,27 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:snowplow_tracker/configurations/network_configuration.dart'; + +class NetworkConfigurationReader extends NetworkConfiguration { + NetworkConfigurationReader(dynamic map) + : super( + endpoint: map['endpoint'], + method: map['method'] == null + ? null + : Method.values.byName(map['method'])); + + void addTrackerOptions(dynamic options) { + if (method != null) { + options['eventMethod'] = method?.name; + } + } +} diff --git a/lib/src/web/readers/configurations/subject_configuration_reader.dart b/lib/src/web/readers/configurations/subject_configuration_reader.dart new file mode 100644 index 0000000..1a752fa --- /dev/null +++ b/lib/src/web/readers/configurations/subject_configuration_reader.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:snowplow_tracker/configurations/subject_configuration.dart'; + +class SubjectConfigurationReader extends SubjectConfiguration { + SubjectConfigurationReader(dynamic map) + : super( + userId: map['userId'], + networkUserId: map['networkUserId'], + domainUserId: map['domainUserId'], + userAgent: map['userAgent'], + ipAddress: map['ipAddress'], + timezone: map['timezone'], + language: map['language'], + colorDepth: map['colorDepth']); +} diff --git a/lib/src/web/readers/configurations/tracker_configuration_reader.dart b/lib/src/web/readers/configurations/tracker_configuration_reader.dart new file mode 100644 index 0000000..080657b --- /dev/null +++ b/lib/src/web/readers/configurations/tracker_configuration_reader.dart @@ -0,0 +1,48 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:snowplow_tracker/configurations/tracker_configuration.dart'; + +class TrackerConfigurationReader extends TrackerConfiguration { + TrackerConfigurationReader(dynamic map) + : super( + appId: map['appId'], + devicePlatform: map['devicePlatform'] == null + ? null + : DevicePlatform.values.byName(map['devicePlatform']), + base64Encoding: map['base64Encoding'], + platformContext: map['platformContext'], + geoLocationContext: map['geoLocationContext'], + sessionContext: map['sessionContext'], + webPageContext: map['webPageContext']); + + void addTrackerOptions(dynamic options) { + if (appId != null) { + options['appId'] = appId; + } + if (devicePlatform != null) { + options['platform'] = devicePlatform?.name; + } + if (base64Encoding != null) { + options['encodeBase64'] = base64Encoding; + } + var contexts = {}; + if (geoLocationContext != null) { + contexts['geolocation'] = geoLocationContext; + } + if (webPageContext != null) { + contexts['webPage'] = webPageContext; + } + if (contexts.isNotEmpty) { + options['contexts'] = contexts; + } + } +} diff --git a/lib/src/web/readers/events/consent_granted_reader.dart b/lib/src/web/readers/events/consent_granted_reader.dart new file mode 100644 index 0000000..9d1395e --- /dev/null +++ b/lib/src/web/readers/events/consent_granted_reader.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:snowplow_tracker/events/consent_granted.dart'; +import 'event_reader.dart'; + +class ConsentGrantedReader extends ConsentGranted implements EventReader { + ConsentGrantedReader(dynamic map) + : super( + expiry: DateTime.parse(map['expiry']), + documentId: map['documentId'], + version: map['version'], + name: map['name'], + documentDescription: map['documentDescription']); + + @override + String endpoint() { + return 'trackConsentGranted'; + } + + @override + Map eventData() { + return { + 'id': documentId, + 'version': version, + 'name': name, + 'description': documentDescription, + 'expiry': expiry.toIso8601String() + }; + } +} diff --git a/lib/src/web/readers/events/consent_withdrawn_reader.dart b/lib/src/web/readers/events/consent_withdrawn_reader.dart new file mode 100644 index 0000000..51daff6 --- /dev/null +++ b/lib/src/web/readers/events/consent_withdrawn_reader.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'event_reader.dart'; +import 'package:snowplow_tracker/events/consent_withdrawn.dart'; + +class ConsentWithdrawnReader extends ConsentWithdrawn implements EventReader { + ConsentWithdrawnReader(dynamic map) + : super( + all: map['all'], + documentId: map['documentId'], + version: map['version'], + name: map['name'], + documentDescription: map['documentDescription']); + + @override + String endpoint() { + return 'trackConsentWithdrawn'; + } + + @override + Map eventData() { + return { + 'all': all, + 'id': documentId, + 'version': version, + 'name': name, + 'description': documentDescription + }; + } +} diff --git a/lib/src/web/readers/events/contexts_reader.dart b/lib/src/web/readers/events/contexts_reader.dart new file mode 100644 index 0000000..b010199 --- /dev/null +++ b/lib/src/web/readers/events/contexts_reader.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; +import 'self_describing_reader.dart'; + +@immutable +class ContextsReader { + final Iterable selfDescribingJsons; + + ContextsReader(List jsons) + : selfDescribingJsons = + jsons.map((x) => SelfDescribingReader(x)).toList(); +} diff --git a/lib/src/web/readers/events/event_reader.dart b/lib/src/web/readers/events/event_reader.dart new file mode 100644 index 0000000..7a2c6c0 --- /dev/null +++ b/lib/src/web/readers/events/event_reader.dart @@ -0,0 +1,15 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +abstract class EventReader { + String endpoint(); + Map eventData(); +} diff --git a/lib/src/web/readers/events/screen_view_reader.dart b/lib/src/web/readers/events/screen_view_reader.dart new file mode 100644 index 0000000..b0a5a7c --- /dev/null +++ b/lib/src/web/readers/events/screen_view_reader.dart @@ -0,0 +1,51 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:snowplow_tracker/events/screen_view.dart'; +import 'event_reader.dart'; + +class ScreenViewReader extends ScreenView implements EventReader { + ScreenViewReader(dynamic map) + : super( + name: map['name'], + id: map['id'], + type: map['type'], + previousName: map['previousName'], + previousType: map['previousType'], + previousId: map['previousId'], + transitionType: map['transitionType']); + + @override + String endpoint() { + return 'trackSelfDescribingEvent'; + } + + @override + Map eventData() { + var data = { + 'name': name, + 'id': id, + 'type': type, + 'previousName': previousName, + 'previousId': previousId, + 'previousType': previousType, + 'transitionType': transitionType + }; + data.removeWhere((key, value) => value == null); + return { + 'event': { + 'schema': + 'iglu:com.snowplowanalytics.mobile/screen_view/jsonschema/1-0-0', + 'data': data + } + }; + } +} diff --git a/lib/src/web/readers/events/self_describing_reader.dart b/lib/src/web/readers/events/self_describing_reader.dart new file mode 100644 index 0000000..d272ae7 --- /dev/null +++ b/lib/src/web/readers/events/self_describing_reader.dart @@ -0,0 +1,32 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:snowplow_tracker/events/self_describing.dart'; +import 'event_reader.dart'; + +class SelfDescribingReader extends SelfDescribing implements EventReader { + SelfDescribingReader(dynamic map) + : super(schema: map['schema'], data: map['data']); + + @override + String endpoint() { + return 'trackSelfDescribingEvent'; + } + + Map json() { + return {'schema': schema, 'data': data}; + } + + @override + Map eventData() { + return {'event': json()}; + } +} diff --git a/lib/src/web/readers/events/structured_reader.dart b/lib/src/web/readers/events/structured_reader.dart new file mode 100644 index 0000000..da8c10d --- /dev/null +++ b/lib/src/web/readers/events/structured_reader.dart @@ -0,0 +1,41 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:snowplow_tracker/events/structured.dart'; +import 'event_reader.dart'; + +class StructuredReader extends Structured implements EventReader { + StructuredReader(dynamic map) + : super( + category: map['category'], + action: map['action'], + label: map['label'], + property: map['property'], + value: map['value']); + + @override + String endpoint() { + return 'trackStructEvent'; + } + + @override + Map eventData() { + final data = { + 'category': category, + 'action': action, + 'label': label, + 'property': property, + 'value': value + }; + data.removeWhere((key, value) => value == null); + return data; + } +} diff --git a/lib/src/web/readers/events/timing_reader.dart b/lib/src/web/readers/events/timing_reader.dart new file mode 100644 index 0000000..15a01b4 --- /dev/null +++ b/lib/src/web/readers/events/timing_reader.dart @@ -0,0 +1,37 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:snowplow_tracker/events/timing.dart'; +import 'event_reader.dart'; + +class TimingReader extends Timing implements EventReader { + TimingReader(dynamic map) + : super( + category: map['category'], + variable: map['variable'], + timing: map['timing'], + label: map['label']); + + @override + String endpoint() { + return 'trackTiming'; + } + + @override + Map eventData() { + return { + 'category': category, + 'variable': variable, + 'timing': timing, + 'label': label, + }; + } +} diff --git a/lib/src/web/readers/messages/event_message_reader.dart b/lib/src/web/readers/messages/event_message_reader.dart new file mode 100644 index 0000000..4a5d87c --- /dev/null +++ b/lib/src/web/readers/messages/event_message_reader.dart @@ -0,0 +1,127 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; +import '../events/consent_granted_reader.dart'; +import '../events/consent_withdrawn_reader.dart'; +import '../events/contexts_reader.dart'; +import '../events/event_reader.dart'; +import '../events/screen_view_reader.dart'; +import '../events/self_describing_reader.dart'; +import '../events/structured_reader.dart'; +import '../events/timing_reader.dart'; + +@immutable +class EventMessageReader { + final String tracker; + final StructuredReader? structured; + final SelfDescribingReader? selfDescribing; + final ScreenViewReader? screenView; + final TimingReader? timing; + final ConsentGrantedReader? consentGranted; + final ConsentWithdrawnReader? consentWithdrawn; + final ContextsReader? contexts; + + const EventMessageReader( + {required this.tracker, + this.structured, + this.selfDescribing, + this.screenView, + this.timing, + this.consentGranted, + this.consentWithdrawn, + this.contexts}); + + EventMessageReader.withStructured(dynamic map) + : tracker = map['tracker'], + structured = StructuredReader(map['eventData']), + selfDescribing = null, + screenView = null, + timing = null, + consentGranted = null, + consentWithdrawn = null, + contexts = + map['contexts'] != null ? ContextsReader(map['contexts']) : null; + + EventMessageReader.withSelfDescribing(dynamic map) + : tracker = map['tracker'], + selfDescribing = SelfDescribingReader(map['eventData']), + structured = null, + screenView = null, + timing = null, + consentGranted = null, + consentWithdrawn = null, + contexts = + map['contexts'] != null ? ContextsReader(map['contexts']) : null; + + EventMessageReader.withScreenView(dynamic map) + : tracker = map['tracker'], + selfDescribing = null, + structured = null, + screenView = ScreenViewReader(map['eventData']), + timing = null, + consentGranted = null, + consentWithdrawn = null, + contexts = + map['contexts'] != null ? ContextsReader(map['contexts']) : null; + + EventMessageReader.withTiming(dynamic map) + : tracker = map['tracker'], + selfDescribing = null, + structured = null, + screenView = null, + timing = TimingReader(map['eventData']), + consentGranted = null, + consentWithdrawn = null, + contexts = + map['contexts'] != null ? ContextsReader(map['contexts']) : null; + + EventMessageReader.withConsentGranted(dynamic map) + : tracker = map['tracker'], + selfDescribing = null, + structured = null, + screenView = null, + timing = null, + consentGranted = ConsentGrantedReader(map['eventData']), + consentWithdrawn = null, + contexts = + map['contexts'] != null ? ContextsReader(map['contexts']) : null; + + EventMessageReader.withConsentWithdrawn(dynamic map) + : tracker = map['tracker'], + selfDescribing = null, + structured = null, + screenView = null, + timing = null, + consentGranted = null, + consentWithdrawn = ConsentWithdrawnReader(map['eventData']), + contexts = + map['contexts'] != null ? ContextsReader(map['contexts']) : null; + + EventReader event() { + if (structured != null) return structured!; + if (selfDescribing != null) return selfDescribing!; + if (screenView != null) return screenView!; + if (timing != null) return timing!; + if (consentGranted != null) return consentGranted!; + return consentWithdrawn!; + } + + dynamic eventData() { + EventReader event = this.event(); + dynamic data = event.eventData(); + if (contexts != null) { + data['context'] = + contexts?.selfDescribingJsons.map((e) => e.json()).toList(); + } + return data; + } +} diff --git a/lib/src/web/readers/messages/set_user_id_message_reader.dart b/lib/src/web/readers/messages/set_user_id_message_reader.dart new file mode 100644 index 0000000..998abcb --- /dev/null +++ b/lib/src/web/readers/messages/set_user_id_message_reader.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/foundation.dart'; + +@immutable +class SetUserIdMessageReader { + final String tracker; + final String? userId; + + SetUserIdMessageReader(dynamic map) + : tracker = map['tracker'], + userId = map['userId']; +} diff --git a/lib/src/web/snowplow_tracker_controller.dart b/lib/src/web/snowplow_tracker_controller.dart new file mode 100644 index 0000000..0508278 --- /dev/null +++ b/lib/src/web/snowplow_tracker_controller.dart @@ -0,0 +1,70 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'dart:js_util'; + +import 'readers/configurations/configuration_reader.dart'; +import 'readers/messages/event_message_reader.dart'; +import 'readers/messages/set_user_id_message_reader.dart'; +import 'sp.dart'; + +class SnowplowTrackerController { + static void createTracker(ConfigurationReader configuration) { + dynamic options = configuration.getTrackerOptions(); + snowplow('newTracker', configuration.namespace, + configuration.networkConfig.endpoint, jsify(options)); + + if (configuration.subjectConfig != null && + configuration.subjectConfig?.userId != null) { + _setUserId(configuration.namespace, configuration.subjectConfig?.userId); + } + + if (configuration.gdprConfig != null) { + snowplow( + 'enableGdprContext', + jsify({ + 'basisForProcessing': configuration.gdprConfig?.basisForProcessing, + 'documentId': configuration.gdprConfig?.documentId, + 'documentVersion': configuration.gdprConfig?.documentVersion, + 'documentDescription': configuration.gdprConfig?.documentDescription + })); + } + + if (configuration.addSessionContext) { + addSessionContextPlugin(configuration.namespace); + } + } + + static void trackEvent(EventMessageReader message) { + snowplow('${message.event().endpoint()}:${message.tracker}', + jsify(message.eventData())); + } + + static void setUserId(SetUserIdMessageReader message) { + _setUserId(message.tracker, message.userId); + } + + static void _setUserId(String tracker, String? userId) { + snowplow('setUserId:$tracker', userId); + } + + static String? getSessionUserId() { + return getSnowplowDuid(); + } + + static String? getSessionId() { + return getSnowplowSid(); + } + + static int? getSessionIndex() { + return getSnowplowVid(); + } +} diff --git a/lib/src/web/sp.dart b/lib/src/web/sp.dart new file mode 100644 index 0000000..035623b --- /dev/null +++ b/lib/src/web/sp.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +@JS() +library snowplow; + +import 'package:js/js.dart'; + +@JS('window.snowplow') +external void snowplow(String method, + [dynamic arg1, + dynamic arg2, + dynamic arg3, + dynamic arg4, + dynamic arg5, + dynamic arg6]); + +@JS('addSessionContextPlugin') +external void addSessionContextPlugin(String tracker); + +@JS('getSnowplowDuid') +external String? getSnowplowDuid(); + +@JS('getSnowplowSid') +external String? getSnowplowSid(); + +@JS('getSnowplowVid') +external int? getSnowplowVid(); + +@JS('isSnowplowInstalled') +external bool isSnowplowInstalled(); diff --git a/lib/tracker.dart b/lib/tracker.dart new file mode 100644 index 0000000..9f6d6c6 --- /dev/null +++ b/lib/tracker.dart @@ -0,0 +1,57 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'dart:async'; + +import 'package:snowplow_tracker/events/event.dart'; +import 'package:snowplow_tracker/events/self_describing.dart'; +import 'package:snowplow_tracker/snowplow.dart'; + +/// Instance of an initialized Snowplow tracker identified by [namespace]. +/// +/// {@category Getting started} +class Tracker { + /// Unique tracker namespace. + final String namespace; + + const Tracker({required this.namespace}); + + /// Tracks the given event with optional context entities. + Future track(Event event, {List? contexts}) async { + await Snowplow.track(event, tracker: namespace, contexts: contexts); + } + + /// Sets the business user ID to the string. + Future setUserId(String? userId) async { + await Snowplow.setUserId(userId, tracker: namespace); + } + + /// Returns the identifier (string UUIDv4) for the user of the session. + /// + /// All trackers on Web share the same session. + Future get sessionUserId async { + return await Snowplow.getSessionUserId(tracker: namespace); + } + + /// Returns the identifier (string UUIDv4) for the session. + /// + /// All trackers on Web share the same session. + Future get sessionId async { + return await Snowplow.getSessionId(tracker: namespace); + } + + /// Returns the index (number) of the current session for this user. + /// + /// All trackers on Web share the same session. + Future get sessionIndex async { + return await Snowplow.getSessionIndex(tracker: namespace); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..d2d8019 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,38 @@ +name: snowplow_tracker +description: A package for tracking Snowplow events in Flutter apps +version: 0.1.0-dev.1 +homepage: https://github.com/snowplow-incubator/snowplow-flutter-tracker +repository: https://github.com/snowplow-incubator/snowplow-flutter-tracker + +environment: + sdk: ">=2.15.0 <3.0.0" + flutter: ">=2.8.0" + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + js: ^0.6.3 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + http: ^0.13.3 + uuid: ^3.0.5 + +flutter: + plugin: + platforms: + android: + package: com.snowplowanalytics.snowplow_tracker + pluginClass: SnowplowTrackerPlugin + ios: + pluginClass: SnowplowTrackerPlugin + web: + pluginClass: SnowplowTrackerPluginWeb + fileName: snowplow_tracker_plugin_web.dart + + assets: + - packages/snowplow_tracker/js/sp_session_context_plugin.js diff --git a/test/snowplow_test.dart b/test/snowplow_test.dart new file mode 100644 index 0000000..b5b1311 --- /dev/null +++ b/test/snowplow_test.dart @@ -0,0 +1,226 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:snowplow_tracker/configurations/gdpr_configuration.dart'; +import 'package:snowplow_tracker/configurations/tracker_configuration.dart'; +import 'package:snowplow_tracker/events/consent_granted.dart'; +import 'package:snowplow_tracker/events/consent_withdrawn.dart'; +import 'package:snowplow_tracker/events/event.dart'; +import 'package:snowplow_tracker/events/screen_view.dart'; +import 'package:snowplow_tracker/events/self_describing.dart'; +import 'package:snowplow_tracker/events/structured.dart'; +import 'package:snowplow_tracker/events/timing.dart'; +import 'package:snowplow_tracker/snowplow.dart'; +import 'package:uuid/uuid.dart'; + +void main() { + const MethodChannel channel = MethodChannel('snowplow_tracker'); + MethodCall? methodCall; + dynamic returnValue; + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + methodCall = null; + returnValue = null; + channel.setMockMethodCallHandler((MethodCall call) async { + methodCall = call; + return returnValue; + }); + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('createsTrackerWithConfiguration', () async { + await Snowplow.createTracker( + namespace: 'tns1', + endpoint: 'https://snowplowanalytics.com', + trackerConfig: const TrackerConfiguration( + devicePlatform: DevicePlatform.iot, base64Encoding: true), + gdprConfig: const GdprConfiguration( + basisForProcessing: 'b', + documentId: 'd', + documentVersion: 'v', + documentDescription: 'e')); + + expect( + methodCall, + isMethodCall('createTracker', arguments: { + 'namespace': 'tns1', + 'networkConfig': {'endpoint': 'https://snowplowanalytics.com'}, + 'trackerConfig': {'devicePlatform': 'iot', 'base64Encoding': true}, + 'gdprConfig': { + 'basisForProcessing': 'b', + 'documentId': 'd', + 'documentVersion': 'v', + 'documentDescription': 'e', + } + })); + }); + + test('tracks structured event', () async { + Event event = const Structured(category: 'c1', action: 'a1'); + await Snowplow.track(event, tracker: 'tns3'); + + expect( + methodCall, + isMethodCall('trackStructured', arguments: { + 'tracker': 'tns3', + 'eventData': {'category': 'c1', 'action': 'a1'} + })); + }); + + test('tracks structured event with context', () async { + Event event = const Structured(category: 'c1', action: 'a1'); + await Snowplow.track(event, tracker: 'tns3', contexts: [ + const SelfDescribing(schema: 'schema://schema1', data: {'x': 'y'}) + ]); + + expect( + methodCall, + isMethodCall('trackStructured', arguments: { + 'tracker': 'tns3', + 'eventData': {'category': 'c1', 'action': 'a1'}, + 'contexts': [ + { + 'schema': 'schema://schema1', + 'data': {'x': 'y'} + } + ] + })); + }); + + test('tracks self-describing event', () async { + Event event = + const SelfDescribing(schema: 'schema://schema2', data: {'y': 'z'}); + await Snowplow.track(event, tracker: 'tns2'); + + expect( + methodCall, + isMethodCall('trackSelfDescribing', arguments: { + 'tracker': 'tns2', + 'eventData': { + 'schema': 'schema://schema2', + 'data': {'y': 'z'} + } + })); + }); + + test('tracks screen view event', () async { + String id = const Uuid().v4(); + Event event = ScreenView(name: 'screen1', id: id); + await Snowplow.track(event, tracker: 'tns2'); + + expect( + methodCall, + isMethodCall('trackScreenView', arguments: { + 'tracker': 'tns2', + 'eventData': {'name': 'screen1', 'id': id} + })); + }); + + test('tracks timing event', () async { + Event event = const Timing(category: 'c1', variable: 'v1', timing: 34); + await Snowplow.track(event, tracker: 'tns1'); + + expect( + methodCall, + isMethodCall('trackTiming', arguments: { + 'tracker': 'tns1', + 'eventData': {'category': 'c1', 'variable': 'v1', 'timing': 34} + })); + }); + + test('tracks consent granted event', () async { + Event event = ConsentGranted( + expiry: DateTime.parse('2021-12-30T09:03:51.196Z'), + documentId: 'd1', + version: '10', + ); + await Snowplow.track(event, tracker: 'tns1'); + + expect( + methodCall, + isMethodCall('trackConsentGranted', arguments: { + 'tracker': 'tns1', + 'eventData': { + 'expiry': '2021-12-30T09:03:51.196Z', + 'documentId': 'd1', + 'version': '10' + } + })); + }); + + test('tracks consent withdrawn event', () async { + Event event = const ConsentWithdrawn( + all: true, + documentId: 'd1', + version: '10', + ); + await Snowplow.track(event, tracker: 'tns1'); + + expect( + methodCall, + isMethodCall('trackConsentWithdrawn', arguments: { + 'tracker': 'tns1', + 'eventData': {'all': true, 'documentId': 'd1', 'version': '10'} + })); + }); + + test('sets user ID', () async { + await Snowplow.setUserId('u1', tracker: 'tns1'); + + expect( + methodCall, + isMethodCall('setUserId', + arguments: {'tracker': 'tns1', 'userId': 'u1'})); + }); + + test('gets session user ID', () async { + returnValue = 'u1'; + String? sessionUserId = await Snowplow.getSessionUserId(tracker: 'tns1'); + + expect( + methodCall, + isMethodCall('getSessionUserId', arguments: { + 'tracker': 'tns1', + })); + expect(sessionUserId, equals('u1')); + }); + + test('gets session ID', () async { + returnValue = 's1'; + String? sessionId = await Snowplow.getSessionId(tracker: 'tns1'); + + expect( + methodCall, + isMethodCall('getSessionId', arguments: { + 'tracker': 'tns1', + })); + expect(sessionId, equals('s1')); + }); + + test('gets session index', () async { + returnValue = 10; + int? sessionIndex = await Snowplow.getSessionIndex(tracker: 'tns1'); + + expect( + methodCall, + isMethodCall('getSessionIndex', arguments: { + 'tracker': 'tns1', + })); + expect(sessionIndex, equals(10)); + }); +} diff --git a/test/tracker_test.dart b/test/tracker_test.dart new file mode 100644 index 0000000..b934c7d --- /dev/null +++ b/test/tracker_test.dart @@ -0,0 +1,70 @@ +// Copyright (c) 2022 Snowplow Analytics Ltd. All rights reserved. +// +// This program is licensed to you under the Apache License Version 2.0, +// and you may not use this file except in compliance with the Apache License Version 2.0. +// You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the Apache License Version 2.0 is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:snowplow_tracker/events/structured.dart'; +import 'package:snowplow_tracker/tracker.dart'; +import 'package:snowplow_tracker/snowplow.dart'; + +void main() { + const MethodChannel channel = MethodChannel('snowplow_tracker'); + String? method; + dynamic arguments; + Tracker? tracker; + dynamic returnValue; + + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + returnValue = null; + + channel.setMockMethodCallHandler((MethodCall methodCall) async { + method = methodCall.method; + arguments = methodCall.arguments; + return returnValue; + }); + + tracker = await Snowplow.createTracker(namespace: 'ns1', endpoint: 'e1'); + method = null; + arguments = null; + }); + + tearDown(() { + channel.setMockMethodCallHandler(null); + }); + + test('tracks structured event', () async { + await tracker + ?.track(const Structured(category: 'category', action: 'action')); + + expect(method, equals('trackStructured')); + expect(arguments['tracker'], equals('ns1')); + expect(arguments['eventData']['category'], equals('category')); + }); + + test('sets user ID', () async { + await tracker?.setUserId('XYZ'); + + expect(method, equals('setUserId')); + expect(arguments['tracker'], equals('ns1')); + expect(arguments['userId'], equals('XYZ')); + }); + + test('gets session ID', () async { + returnValue = '1234'; + String? sessionId = await tracker?.sessionId; + + expect(method, equals('getSessionId')); + expect(arguments['tracker'], equals('ns1')); + expect(sessionId, equals('1234')); + }); +} From 1caf446a160b5f22955e616eace433dd4f93a743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=BA=C5=A1=20Tomlein?= Date: Wed, 26 Jan 2022 11:11:20 +0100 Subject: [PATCH 2/3] Add action to publish release to pub.dev and GitHub (close #3) PR #4 --- .github/workflows/publish.yml | 83 +++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f747bad..1997545 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,10 +1,67 @@ -name: Publish package to pub.dev +name: Publish package to pub.dev and make a GitHub release + on: push: - branches: - - master + tags: + - '*.*.*' + jobs: - build: + test: + name: Run unit tests + runs-on: ubuntu-latest + strategy: + matrix: + flutter: ['2.8.0'] + steps: + - uses: actions/checkout@v2 + - uses: subosito/flutter-action@v1 + with: + flutter-version: ${{ matrix.flutter }} + channel: 'stable' + - run: flutter pub get + - run: flutter test + + version_check: + runs-on: ubuntu-latest + outputs: + v_tracker: ${{ steps.version.outputs.FLUTTER_TRACKER_VERSION}} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup Node + uses: actions/setup-node@v2 + with: + node-version: 14 + + - name: Get tag and tracker versions + id: version + env: + IOS_VER_FILEPATH: 'ios/Classes/TrackerVersion.swift' + ANDR_VER_FILEPATH: 'android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/TrackerVersion.kt' + run: | + echo ::set-output name=TAG_VERSION::${GITHUB_REF#refs/*/} + echo "##[set-output name=FLUTTER_TRACKER_VERSION;]$(cat pubspec.yaml | sed -n -e 's/^.*version: \(.*\)/\1/p')" + echo "##[set-output name=FLUTTER_IOS_TRACKER_VERSION;]$(cat "${IOS_VER_FILEPATH}" | sed -n -e 's/^.*TRACKER_VERSION = "flutter-\(.*\)"/\1/p')" + echo "##[set-output name=FLUTTER_ANDROID_TRACKER_VERSION;]$(cat "${ANDR_VER_FILEPATH}" | sed -n -e 's/^.*TRACKER_VERSION = "flutter-\(.*\)"/\1/p')" + - name: Fail if version mismatch + run: | + if [ "${{ steps.version.outputs.TAG_VERSION }}" != "${{ steps.version.outputs.FLUTTER_TRACKER_VERSION }}" ] ; then + echo "Tag version (${{ steps.version.outputs.TAG_VERSION }}) doesn't match version in project (${{ steps.version.outputs.FLUTTER_TRACKER_VERSION }})" + exit 1 + fi + if [ "${{ steps.version.outputs.TAG_VERSION }}" != "${{ steps.version.outputs.FLUTTER_IOS_TRACKER_VERSION }}" ] ; then + echo "Tag version (${{ steps.version.outputs.TAG_VERSION }}) doesn't match version in project(ios) (${{ steps.version.outputs.FLUTTER_IOS_TRACKER_VERSION }})" + exit 1 + fi + if [ "${{ steps.version.outputs.TAG_VERSION }}" != "${{ steps.version.outputs.FLUTTER_ANDROID_TRACKER_VERSION }}" ] ; then + echo "Tag version (${{ steps.version.outputs.TAG_VERSION }}) doesn't match version in project(android) (${{ steps.version.outputs.FLUTTER_ANDROID_TRACKER_VERSION }})" + exit 1 + fi + + publish_pubdev: + needs: ["test", "version_check"] runs-on: ubuntu-latest container: image: google/dart:latest @@ -24,3 +81,21 @@ jobs: EOF - name: Publish package run: pub publish -f + + release: + needs: ["test", "publish_pubdev", "version_check"] + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Release + uses: softprops/action-gh-release@v0.1.7 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + name: Version ${{ needs.version_check.outputs.v_tracker }} + draft: false + prerelease: ${{ contains(needs.version_check.outputs.v_tracker, '-') }} From 6f1b76f34771cd899eded0b73e3200695725bfca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matu=CC=81s=CC=8C=20Tomlein?= Date: Wed, 26 Jan 2022 11:12:52 +0100 Subject: [PATCH 3/3] Prepare for 0.1.0-alpha.1 release --- CHANGELOG.md | 4 ++++ .../com/snowplowanalytics/snowplow_tracker/TrackerVersion.kt | 2 +- example/ios/Podfile.lock | 4 ++-- example/pubspec.lock | 2 +- ios/Classes/TrackerVersion.swift | 2 +- ios/snowplow_tracker.podspec | 2 +- pubspec.yaml | 2 +- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 061bf69..0c7441c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.0-alpha.1 + +* Add action to publish release to pub.dev and GitHub (#3) + # 0.1.0-dev.1 * Initial pre-release. diff --git a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/TrackerVersion.kt b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/TrackerVersion.kt index 069b978..5aac95e 100644 --- a/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/TrackerVersion.kt +++ b/android/src/main/kotlin/com/snowplowanalytics/snowplow_tracker/TrackerVersion.kt @@ -12,5 +12,5 @@ package com.snowplowanalytics.snowplow_tracker object TrackerVersion { - val TRACKER_VERSION = "flutter-0.1.0-dev.1" + val TRACKER_VERSION = "flutter-0.1.0-alpha.1" } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 7872c5c..a72a05d 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -5,7 +5,7 @@ PODS: - FMDB/standard (2.7.5) - integration_test (0.0.1): - Flutter - - snowplow_tracker (0.1.0-dev.1): + - snowplow_tracker (0.1.0-alpha.1): - Flutter - SnowplowTracker (~> 3.0.2) - SnowplowTracker (3.0.2): @@ -33,7 +33,7 @@ SPEC CHECKSUMS: Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 - snowplow_tracker: d177e74163dea0ef1545e0033e752b09decef2c0 + snowplow_tracker: d613c3633c9b1ad17a9cda26a76d57debf0fa629 SnowplowTracker: a96fd8819c86c56844f930af372d0f4f92c35472 PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c diff --git a/example/pubspec.lock b/example/pubspec.lock index 9015876..6f1a821 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -189,7 +189,7 @@ packages: path: ".." relative: true source: path - version: "0.1.0-dev.1" + version: "0.1.0-alpha.1" source_span: dependency: transitive description: diff --git a/ios/Classes/TrackerVersion.swift b/ios/Classes/TrackerVersion.swift index 63bf3e6..e4cff86 100644 --- a/ios/Classes/TrackerVersion.swift +++ b/ios/Classes/TrackerVersion.swift @@ -12,5 +12,5 @@ import Foundation class TrackerVersion { - static let TRACKER_VERSION = "flutter-0.1.0-dev.1" + static let TRACKER_VERSION = "flutter-0.1.0-alpha.1" } diff --git a/ios/snowplow_tracker.podspec b/ios/snowplow_tracker.podspec index 843617d..f7318bc 100644 --- a/ios/snowplow_tracker.podspec +++ b/ios/snowplow_tracker.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'snowplow_tracker' - s.version = '0.1.0-dev.1' + s.version = '0.1.0-alpha.1' s.summary = 'A package for tracking Snowplow events in Flutter apps.' s.description = <<-DESC A package for tracking Snowplow events in Flutter apps. diff --git a/pubspec.yaml b/pubspec.yaml index d2d8019..1a647b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: snowplow_tracker description: A package for tracking Snowplow events in Flutter apps -version: 0.1.0-dev.1 +version: 0.1.0-alpha.1 homepage: https://github.com/snowplow-incubator/snowplow-flutter-tracker repository: https://github.com/snowplow-incubator/snowplow-flutter-tracker