├── .github └── workflows │ ├── check.yml │ └── publish.yml ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── .metadata ├── .pubignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── appspector │ └── flutter │ ├── AppSpectorPlugin.java │ ├── MainAppSpectorHandler.java │ ├── RequestSender.java │ ├── event │ ├── EventHandler.java │ ├── EventReceiver.java │ ├── http │ │ ├── FlutterHttpTracker.java │ │ ├── HttpRequestEventHandler.java │ │ └── HttpResponseEventHandler.java │ └── log │ │ └── LogEventHandler.java │ └── screenshot │ └── FlutterScreenshotFactory.java ├── example ├── .gitignore ├── README.md ├── android │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── appspector │ │ │ │ └── flutter │ │ │ │ └── example │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── assets │ ├── patch.json │ ├── post.json │ └── put.json ├── ios │ ├── AppSpectorPluginTestPlan.xctestplan │ ├── AppSpectorPluginTests │ │ ├── ASPluginCallValidatorTests.m │ │ ├── ASPluginEventsHandlerTests.m │ │ ├── AppSpectorPluginTests.m │ │ └── Info.plist │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── Runner │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── main.m ├── lib │ ├── app_drawer.dart │ ├── color.dart │ ├── http │ │ ├── app_http_client.dart │ │ └── http_request_item.dart │ ├── http_page.dart │ ├── main.dart │ ├── main_page.dart │ ├── metadata_page.dart │ ├── routes.dart │ ├── sqlite │ │ ├── record.dart │ │ └── storage.dart │ ├── sqlite_page.dart │ └── utils.dart └── pubspec.yaml ├── github-cover.png ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── ASPluginCallValidator.h │ ├── ASPluginCallValidator.m │ ├── ASPluginEventsHandler.h │ ├── ASPluginEventsHandler.m │ ├── AppSpectorPlugin.h │ └── AppSpectorPlugin.m └── appspector.podspec ├── lib ├── appspector.dart └── src │ ├── appspector_plugin.dart │ ├── event_sender.dart │ ├── http │ ├── client.dart │ ├── events.dart │ ├── http_overrides.dart │ ├── request_wrapper.dart │ ├── response_wrapper.dart │ └── tracker.dart │ ├── log │ └── logger.dart │ ├── monitors.dart │ └── request_receiver.dart ├── publish.sh ├── pubspec.yaml ├── static └── appspector_demo.gif └── test_ios.sh /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check workflow 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - develop 8 | 9 | env: 10 | CREDENTIALS_PATH: /Users/runner/hostedtoolcache/flutter/.pub-cache 11 | 12 | jobs: 13 | check: 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-java@v3 18 | with: 19 | distribution: 'adopt' 20 | java-version: '17' 21 | - uses: subosito/flutter-action@v2 22 | with: 23 | flutter-version: '3.10.5' 24 | 25 | - run: flutter pub get 26 | - run: mkdir -p $CREDENTIALS_PATH && echo $CREDENTIALS_JSON > $CREDENTIALS_PATH/credentials.json 27 | 28 | - run: flutter pub publish -n 29 | - run: ./test_ios.sh 30 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Release workflow 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | env: 9 | CREDENTIALS_JSON: ${{secrets.CREDENTIALS_JSON}} 10 | CREDENTIALS_PATH: /Users/runner/hostedtoolcache/flutter/.pub-cache 11 | 12 | jobs: 13 | release: 14 | runs-on: macos-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-java@v3 18 | with: 19 | distribution: 'adopt' 20 | java-version: '17' 21 | - uses: subosito/flutter-action@v2 22 | with: 23 | flutter-version: '3.10.5' 24 | 25 | - run: flutter pub get 26 | - run: mkdir -p $CREDENTIALS_PATH && echo $CREDENTIALS_JSON > $CREDENTIALS_PATH/credentials.json 27 | 28 | - run: flutter pub publish -f 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .flutter-plugins 6 | .pub/ 7 | pubspec.lock 8 | 9 | build/ 10 | doc/ 11 | 12 | *.iml 13 | 14 | /.idea/workspace.xml 15 | /.idea/markdown-*.xml 16 | /.idea/checkstyle-idea.xml 17 | /.idea/encodings.xml 18 | 19 | /.idea/libraries 20 | /.idea/dictionaries 21 | /.idea/runConfigurations 22 | /.idea/sonarlint 23 | /.idea/markdown-navigator 24 | /.idea/shelf 25 | AppSpector Flutter/.idea 26 | example/.flutter-plugins-dependencies 27 | example/ios/Flutter/Flutter.podspec 28 | .packages_ 29 | lib/.idea 30 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 20 | 21 | 22 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 | xmlns:android 32 | 33 | ^$ 34 | 35 | 36 | 37 |
38 |
39 | 40 | 41 | 42 | xmlns:.* 43 | 44 | ^$ 45 | 46 | 47 | BY_NAME 48 | 49 |
50 |
51 | 52 | 53 | 54 | .*:id 55 | 56 | http://schemas.android.com/apk/res/android 57 | 58 | 59 | 60 |
61 |
62 | 63 | 64 | 65 | .*:name 66 | 67 | http://schemas.android.com/apk/res/android 68 | 69 | 70 | 71 |
72 |
73 | 74 | 75 | 76 | name 77 | 78 | ^$ 79 | 80 | 81 | 82 |
83 |
84 | 85 | 86 | 87 | style 88 | 89 | ^$ 90 | 91 | 92 | 93 |
94 |
95 | 96 | 97 | 98 | .* 99 | 100 | ^$ 101 | 102 | 103 | BY_NAME 104 | 105 |
106 |
107 | 108 | 109 | 110 | .* 111 | 112 | http://schemas.android.com/apk/res/android 113 | 114 | 115 | ANDROID_ATTRIBUTE_ORDER 116 | 117 |
118 |
119 | 120 | 121 | 122 | .* 123 | 124 | .* 125 | 126 | 127 | BY_NAME 128 | 129 |
130 |
131 |
132 |
133 |
134 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 36 | 53 | 54 | 55 | 56 | 58 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 5391447fae6209bb21a89e6a5a6583cac1af9b4b 8 | channel: beta 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /.pubignore: -------------------------------------------------------------------------------- 1 | publish.sh 2 | test_ios.sh 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.10.0 6 Jul 2023 2 | * Update Android SDK to v1.5 3 | * Use CompileSdkVersion = 33 4 | 5 | ## 0.9.0 13 Jun 2022 6 | * Adds support for Flutter 3 7 | 8 | ## 0.8.1 20 Sep 2021 9 | * Fix the issue with incorrect User-Agent 10 | 11 | ## 0.8.0 09 Sep 2021 12 | * Support Flutter 2.5 13 | 14 | ## 0.7.0 31 Mar 2021 15 | * Support Flutter 2 and Dart null-safety 16 | * Fix issue with missing OnSessionUrlListener 17 | 18 | ## 0.6.2 30 Mar 2021 19 | * Add internal updates 20 | 21 | ## 0.6.1 14 Mar 2021 22 | * Fix for incorrect HTTP response handling on iOS 23 | 24 | ## 0.6.0 28 Feb 2021 25 | * Fix losing connection after the hot restart 26 | 27 | ## 0.5.0 16 Dec 2020 28 | * Use 1.4.+ AppSpector Android SDK 29 | * Fix issue with missing Content-Length header 30 | 31 | ## 0.4.0 6 Oct 2020 32 | * Support new Flutter version (1.22.0) 33 | 34 | ## 0.3.0 27 May 2020 35 | * Add File System monitor 36 | 37 | ## 0.2.0 16 Apr 2020 38 | * Add API to provide list of monitors to enable 39 | * Add API to provide session metadata (including the device custom name) 40 | * Add API which allows to stop and start session during the application lifetime 41 | * Add ability to listen to a session url 42 | * Use Android SDK version 1.2.1 43 | 44 | ## 0.1.0 24 Dec 2019 45 | * Fixed logging module 46 | 47 | ## 0.0.9 20 Nov 2019 48 | * Fix compatibility with DIO library 49 | 50 | ## 0.0.8 11 Sep 2019 51 | * Add WidgetsFlutterBinding.ensureInitialized(); to README.md 52 | * Fix issue about 'toImage' isn't defined for the class 'ContainerLayer 53 | * Use MethodChannel only on MainThread 54 | 55 | ## 0.0.7 - 29 Jul 2019 56 | 57 | * Fix compatibility issues with the latest flutter version 58 | 59 | ## 0.0.6 - 15 Jul 2019 60 | 61 | * Fix compatibility issues with new dart version 62 | * Bug fixes 63 | 64 | ## 0.0.4 - 6 May 2019 65 | 66 | * Update README 67 | 68 | 69 | ## 0.0.3 - 25 Apr 2019 70 | 71 | * Add Http Monitor 72 | * Add SharedPreference/UserDefaults Monitor 73 | * Add Logger to collect logs only into AppSpector Service 74 | 75 | 76 | ## 0.0.2 - 22 Jan 2019 77 | 78 | * Fix issue with black screen in Screenshot Monitor 79 | 80 | 81 | ## 0.0.1 - 20 Jan 2019 82 | 83 | * Initial release with base initialization of AppSpector 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 AppSpector 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub release](https://img.shields.io/github/release/appspector/flutter-plugin.svg)](https://github.com/appspector/flutter-plugin) 2 | # ![AppSpector](https://github.com/appspector/flutter-plugin/raw/master/github-cover.png) 3 | 4 | A plugin that integrate [AppSpector](https://appspector.com/?utm_source=flutter_readme) to your Flutter project. 5 | 6 | With AppSpector you can remotely debug your app running in the same room or on another continent. 7 | You can measure app performance, view database content, logs, network requests and many more in realtime. 8 | This is the instrument that you've been looking for. Don't limit yourself only to simple logs. 9 | Debugging doesn't have to be painful! 10 | 11 | AppSpector demonstration 12 | 13 | * [Installation](#installation) 14 | * [Add AppSpector plugin to pubspec.yaml](#add-appspector-plugin-to-pubspecyaml) 15 | * [Initialize AppSpector plugin](#initialize-appspector-plugin) 16 | * [Build and Run](#build-and-run) 17 | * [Getting session URL](#getting-session-url) 18 | * [Correct SQLite setup for the SDK](#correct-sqlite-setup-for-the-sdk) 19 | * [Configure](#configure) 20 | * [Start/Stop data collection](#startstop-data-collection) 21 | * [Custom device name](#custom-device-name) 22 | * [Getting session URL](#getting-session-url) 23 | * [Correct SQLite setup for the SDK](#correct-sqlite-setup-for-the-sdk) 24 | * [Features](#features) 25 | * [SQLite monitor](#sqlite-monitor) 26 | * [HTTP monitor](#http-monitor) 27 | * [Logs monitor](#logs-monitor) 28 | * [Logger](#logger) 29 | * [Location monitor](#location-monitor) 30 | * [Screenshot monitor](#screenshot-monitor) 31 | * [SharedPreference/UserDefaults monitor](#sharedpreferenceuserdefaults-monitor) 32 | * [Performance monitor](#performance-monitor) 33 | * [Environment monitor](#environment-monitor) 34 | * [Notification Center monitor (only for iOS)](#notification-center-monitor-only-for-ios) 35 | * [File System Monitor](#file-system-monitor) 36 | * [Feedback](#feedback) 37 | 38 | 39 | # Installation 40 | 41 | Before using AppSpector SDK in your Flutter app you have to register it on ([https://app.appspector.com](https://app.appspector.com?utm_source=android_readme)) via web or [desktop app](https://appspector.com/download/?utm_source=android_readme). 42 | To use SDK on both platforms (iOS and Android) you have to register two separate apps for different platforms. 43 | API keys required for the SDK initialisation will be available on the Apps settings pages 44 | 45 | ## Add AppSpector plugin to pubspec.yaml 46 | ```yaml 47 | dependencies 48 | appspector: '0.10.0' 49 | ``` 50 | 51 | ## Initialize AppSpector plugin 52 | ```dart 53 | import 'package:appspector/appspector.dart'; 54 | 55 | void main() { 56 | WidgetsFlutterBinding.ensureInitialized(); 57 | runAppSpector(); 58 | runApp(MyApp()); 59 | } 60 | 61 | void runAppSpector() { 62 | final config = Config() 63 | ..iosApiKey = "Your iOS API_KEY" 64 | ..androidApiKey = "Your Android API_KEY"; 65 | 66 | // If you don't want to start all monitors you can specify a list of necessary ones 67 | config.monitors = [Monitors.http, Monitors.logs, Monitors.screenshot]; 68 | 69 | AppSpectorPlugin.run(config); 70 | } 71 | ``` 72 | 73 | ## Build and Run 74 | Build your project and see everything work! When your app is up and running you can go to [https://app.appspector.com](https://app.appspector.com/?utm_source=flutter_readme) and connect to your application session. 75 | 76 | 77 | # Configure 78 | 79 | ## Start/Stop data collection 80 | 81 | After calling the `run` method the SDKs start data collection and 82 | data transferring to the web service. From that point you can see 83 | your session in the AppSpector client. 84 | 85 | Since plugin initialization should locate in the main function we provide 86 | methods to help you control AppSpector state by calling `stop()` and `start()` methods. 87 | 88 | **You are able to use these methods only after AppSpector was initialized.** 89 | 90 | The `stop()` tells AppSpector to disable all data collection and close current session. 91 | 92 | ```dart 93 | await AppSpectorPlugin.shared().stop(); 94 | ``` 95 | 96 | The `start()` starts it again using config you provided at initialization. 97 | 98 | ```dart 99 | await AppSpectorPlugin.shared().start(); 100 | ``` 101 | 102 | **As the result new session will be created and all activity between 103 | `stop()` and `start()` calls will not be tracked.** 104 | 105 | To check AppSpector state you can use `isStarted()` method. 106 | 107 | ```dart 108 | await AppSpectorPlugin.shared().isStarted(); 109 | ``` 110 | 111 | ## Custom device name 112 | 113 | You can assign a custom name to your device to easily find needed sessions 114 | in the sessions list. To do this you should add the desired name as a value 115 | for `MetadataKeys.deviceName` key to the `metadata` dictionary: 116 | 117 | ```dart 118 | void runAppSpector() { 119 | var config = new Config() 120 | ..iosApiKey = "Your iOS API_KEY" 121 | ..androidApiKey = "Your Android API_KEY" 122 | ..metadata = {MetadataKeys.deviceName: "CustomName"}; 123 | 124 | AppSpectorPlugin.run(config); 125 | } 126 | ``` 127 | 128 | Also, the plugin allows managing the device name during application lifetime using 129 | 130 | the `setMetadataValue` method to change device name 131 | 132 | ```dart 133 | AppSpectorPlugin.shared().setMetadataValue(MetadataKeys.deviceName, "New Device Name"); 134 | ``` 135 | 136 | or the `removeMetadataValue` to remove your custom device name 137 | 138 | ```dart 139 | AppSpectorPlugin.shared().removeMetadataValue(MetadataKeys.deviceName); 140 | ``` 141 | 142 | ## Getting session URL 143 | 144 | Sometimes you may need to get URL pointing to current session from code. 145 | Say you want link crash in your crash reporter with it, write it to logs or 146 | display in your debug UI. To get this URL you have to add a session start callback: 147 | 148 | ```dart 149 | AppSpectorPlugin.shared()?.sessionUrlListener = (sessionUrl) => { 150 | // Save url for future use... 151 | }; 152 | ``` 153 | 154 | 155 | ## Correct SQLite setup for the SDK 156 | 157 | The SQLite monitor on Android demands that any DB files are located at the `database` folder. 158 | So, if you're using [sqflite](https://pub.dev/packages/sqflite) the code for opening db will be looks like that: 159 | 160 | ```dart 161 | var dbPath = await getDatabasesPath() + "/my_database_name"; 162 | var db = await openDatabase(dbPath, version: 1, onCreate: _onCreate); 163 | ``` 164 | 165 | The `getDatabasesPath()` method is imported from `package:sqflite/sqflite.dart`. 166 | 167 | # Features 168 | 169 | AppSpector provides many monitors that are can be different for both platforms. 170 | 171 | ### SQLite monitor 172 | Provides browser for sqlite databases found in your app. Allows to track all queries, shows DB scheme and data in DB. You can issue custom SQL query on any DB and see results in browser immediately. 173 | 174 | SQLite monitor 175 | 176 | #### HTTP monitor 177 | Shows all HTTP traffic in your app. You can examine any request, see request/response headers and body. 178 | We provide XML and JSON highliting for request/responses with formatting and folding options so even huge responses are easy to look through. 179 | 180 | HTTP monitor 181 | 182 | ### Logs monitor 183 | Displays all logs generated by your app. 184 | 185 | #### Logger 186 | AppSpector Logger allows you to collect log message only into AppSpector 187 | service. It is useful when you log some internal data witch can be leaked 188 | through Android Logcat or similar tool for iOS. 189 | 190 | It has a very simple API to use: 191 | 192 | ```dart 193 | Logger.d("MyTAG", "It won't be printed to the app console"); 194 | ``` 195 | 196 | **Don't forget to import it** from `package:appspector/appspector.dart`. 197 | 198 | Logs 199 | 200 | ### Location monitor 201 | Most of the apps are location-aware. Testing it requires changing locations yourself. In this case, location mocking is a real time saver. Just point to the location on the map and your app will change its geodata right away. 202 | 203 | Location 204 | 205 | ### Screenshot monitor 206 | Simply captures screenshot from the device. 207 | 208 | ### SharedPreference/UserDefaults monitor 209 | Provides browser and editor for SharedPreferences/UserDefaults. 210 | 211 | ### Performance monitor 212 | Displays real-time graphs of the CPU / Memory/ Network / Disk / Battery usage. 213 | 214 | ### Environment monitor 215 | Gathers all of the environment variables and arguments in one place, info.plist, cli arguments and much more. 216 | 217 | ### Notification Center monitor (only for iOS) 218 | Tracks all posted notifications and subscriptions. You can examine notification user info, sender/reciever objects, etc. 219 | And naturally you can post notifications to your app from the frontend. 220 | 221 | ### File System Monitor 222 | Provides access to the application internal storage on Android and sandbox and bundle on iOS. 223 | Using this monitor you're able to download, remove or upload files, create directories and just walk around your app FS. 224 | 225 | For mode details, you can visit [Android SDK](https://github.com/appspector/android-sdk/) and [iOS SDK](https://github.com/appspector/ios-sdk) pages. 226 | 227 | 228 | # Feedback 229 | Let us know what do you think or what would you like to be improved: [info@appspector.com](mailto:info@appspector.com). 230 | 231 | [Join our slack to discuss setup process and features](https://slack.appspector.com) 232 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | 10 | **/GeneratedPluginRegistrant.java 11 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.appspector.flutter' 2 | version '1.0' 3 | 4 | buildscript { 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:8.0.2' 12 | } 13 | } 14 | 15 | rootProject.allprojects { 16 | repositories { 17 | google() 18 | maven { url "https://maven.appspector.com/artifactory/android-sdk" } 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | 24 | android { 25 | compileSdkVersion 33 26 | if (project.android.hasProperty('namespace')) { 27 | namespace "com.appspector.flutter" 28 | } 29 | defaultConfig { 30 | minSdkVersion 21 31 | } 32 | 33 | lintOptions { 34 | disable 'InvalidPackage' 35 | } 36 | 37 | compileOptions { 38 | sourceCompatibility 1.8 39 | targetCompatibility 1.8 40 | } 41 | } 42 | 43 | dependencies { 44 | implementation("com.appspector:android-sdk:1.5.+") { 45 | exclude group:"org.jetbrains.kotlin" 46 | } 47 | implementation 'androidx.annotation:annotation:1.6.0' 48 | } 49 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'appspector_plugin' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/java/com/appspector/flutter/AppSpectorPlugin.java: -------------------------------------------------------------------------------- 1 | package com.appspector.flutter; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | import io.flutter.embedding.engine.plugins.FlutterPlugin; 6 | import io.flutter.plugin.common.PluginRegistry.Registrar; 7 | 8 | import static com.appspector.flutter.MainAppSpectorHandler.internalRegister; 9 | 10 | /** 11 | * AppSpectorPlugin 12 | */ 13 | public class AppSpectorPlugin implements FlutterPlugin { 14 | 15 | @Nullable 16 | private MainAppSpectorHandler mainAppSpectorHandler; 17 | 18 | @Override 19 | public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { 20 | mainAppSpectorHandler = internalRegister(binding.getApplicationContext(), binding.getBinaryMessenger()); 21 | } 22 | 23 | @Override 24 | public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { 25 | if (mainAppSpectorHandler != null) { 26 | mainAppSpectorHandler.unregister(); 27 | mainAppSpectorHandler = null; 28 | } 29 | } 30 | 31 | /** 32 | * Plugin registration. 33 | * Deprecated: it's old plugin registration which is needed for Flutter v1 34 | */ 35 | @SuppressWarnings("deprecation") 36 | @Deprecated 37 | public static void registerWith(Registrar registrar) { 38 | internalRegister(registrar.context().getApplicationContext(), registrar.messenger()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /android/src/main/java/com/appspector/flutter/MainAppSpectorHandler.java: -------------------------------------------------------------------------------- 1 | package com.appspector.flutter; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.os.Handler; 6 | 7 | import com.appspector.flutter.event.EventReceiver; 8 | import com.appspector.flutter.event.http.HttpRequestEventHandler; 9 | import com.appspector.flutter.event.http.HttpResponseEventHandler; 10 | import com.appspector.flutter.event.log.LogEventHandler; 11 | import com.appspector.flutter.screenshot.FlutterScreenshotFactory; 12 | import com.appspector.sdk.AppSpector; 13 | import com.appspector.sdk.Builder; 14 | import com.appspector.sdk.SessionUrlListener; 15 | import com.appspector.sdk.core.util.AppspectorLogger; 16 | import com.appspector.sdk.monitors.screenshot.ScreenshotMonitor; 17 | 18 | import java.util.Collections; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | 23 | import androidx.annotation.NonNull; 24 | import androidx.annotation.Nullable; 25 | import io.flutter.plugin.common.BinaryMessenger; 26 | import io.flutter.plugin.common.MethodCall; 27 | import io.flutter.plugin.common.MethodChannel; 28 | 29 | class MainAppSpectorHandler implements MethodChannel.MethodCallHandler { 30 | 31 | private final Application application; 32 | @SuppressWarnings({"FieldCanBeLocal", "unused"}) 33 | private final MethodChannel mainChannel; 34 | private final EventReceiver eventReceiver; 35 | private final RequestSender requestSender; 36 | private final SessionUrlListener sessionUrlListener; 37 | private final Map monitorInitializerMap; 38 | 39 | private MainAppSpectorHandler(Application application, 40 | MethodChannel mainChannel, 41 | SessionUrlListener sessionUrlListener, 42 | EventReceiver eventReceiver, 43 | RequestSender requestSender) { 44 | this.application = application; 45 | this.mainChannel = mainChannel; 46 | this.eventReceiver = eventReceiver; 47 | this.requestSender = requestSender; 48 | this.sessionUrlListener = sessionUrlListener; 49 | this.monitorInitializerMap = createMonitorInitializerMap(); 50 | registerEvents(eventReceiver); 51 | } 52 | 53 | private void registerEvents(EventReceiver eventReceiver) { 54 | eventReceiver.registerEventHandler(new HttpRequestEventHandler()); 55 | eventReceiver.registerEventHandler(new HttpResponseEventHandler()); 56 | eventReceiver.registerEventHandler(new LogEventHandler()); 57 | } 58 | 59 | void register() { 60 | mainChannel.setMethodCallHandler(this); 61 | } 62 | 63 | void unregister() { 64 | mainChannel.setMethodCallHandler(null); 65 | eventReceiver.unsubscribe(); 66 | } 67 | 68 | @Override 69 | public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { 70 | switch (call.method) { 71 | case "run": 72 | initAppSpector(result, 73 | call.argument("apiKey"), 74 | call.argument("metadata"), 75 | call.argument("enabledMonitors") 76 | ); 77 | break; 78 | case "stop": 79 | stopSdk(result); 80 | break; 81 | case "start": 82 | startSdk(result); 83 | break; 84 | case "isStarted": 85 | checkSdkStarted(result); 86 | break; 87 | case "setMetadata": 88 | setMetadata(call.argument("key"), call.argument("value"), result); 89 | break; 90 | case "removeMetadata": 91 | removeMetadata(call.argument("key"), result); 92 | break; 93 | default: 94 | result.notImplemented(); 95 | } 96 | } 97 | 98 | private void setMetadata(@Nullable String key, @Nullable String value, @NonNull MethodChannel.Result result) { 99 | if (key == null || value == null) { 100 | AppspectorLogger.e("AppSpectorPlugin :: key or value is null"); 101 | return; 102 | } 103 | withSharedInstance(result, sharedInstance -> { 104 | sharedInstance.setMetadataValue(key, value); 105 | return null; 106 | }); 107 | } 108 | 109 | private void removeMetadata(@Nullable String key, @NonNull MethodChannel.Result result) { 110 | if (key == null) { 111 | AppspectorLogger.e("AppSpectorPlugin :: key is null"); 112 | return; 113 | } 114 | withSharedInstance(result, sharedInstance -> { 115 | sharedInstance.removeMetadataValue(key); 116 | return null; 117 | }); 118 | } 119 | 120 | private void checkSdkStarted(@NonNull MethodChannel.Result result) { 121 | final AppSpector sharedInstance = AppSpector.shared(); 122 | if (sharedInstance != null) { 123 | result.success(sharedInstance.isStarted()); 124 | return; 125 | } 126 | result.success(false); 127 | } 128 | 129 | private void stopSdk(@NonNull MethodChannel.Result result) { 130 | withSharedInstance(result, sharedInstance -> { 131 | sharedInstance.stop(); 132 | return null; 133 | }); 134 | } 135 | 136 | private void startSdk(@NonNull MethodChannel.Result result) { 137 | withSharedInstance(result, sharedInstance -> { 138 | sharedInstance.start(); 139 | return null; 140 | }); 141 | } 142 | 143 | private void initAppSpector(@NonNull MethodChannel.Result result, @Nullable String apiKey, @Nullable Map metadata, @Nullable List enabledMonitors) { 144 | if (apiKey == null) { 145 | result.error("MissingAppKey", "Cannot initialize SDK without AppKey", null); 146 | return; 147 | } 148 | 149 | final Builder builder = AppSpector.build(application) 150 | .addMetadata(metadata != null ? metadata : Collections.emptyMap()); 151 | 152 | addMonitors(builder, enabledMonitors); 153 | 154 | builder.run(apiKey); 155 | 156 | //noinspection ConstantConditions 157 | AppSpector.shared().setSessionUrlListener(sessionUrlListener); 158 | } 159 | 160 | private Map createMonitorInitializerMap() { 161 | return new HashMap() {{ 162 | put("logs", Builder::addLogMonitor); 163 | put("screenshot", builder -> builder.addMonitor(new ScreenshotMonitor(new FlutterScreenshotFactory(requestSender)))); 164 | put("environment", Builder::addEnvironmentMonitor); 165 | put("http", Builder::addHttpMonitor); 166 | put("location", Builder::addLocationMonitor); 167 | put("performance", Builder::addPerformanceMonitor); 168 | put("sqlite", Builder::addSQLMonitor); 169 | put("shared-preferences", Builder::addSharedPreferenceMonitor); 170 | put("file-system", Builder::addFileSystemMonitor); 171 | }}; 172 | } 173 | 174 | private void addMonitors(@NonNull Builder builder, @Nullable List enabledMonitors) { 175 | if (enabledMonitors == null || enabledMonitors.isEmpty()) { 176 | builder 177 | .withDefaultMonitors() 178 | .addMonitor(new ScreenshotMonitor(new FlutterScreenshotFactory(requestSender))); 179 | return; 180 | } 181 | 182 | for (String monitor : enabledMonitors) { 183 | MonitorInitializer initializer = monitorInitializerMap.get(monitor); 184 | if (initializer != null) { 185 | initializer.init(builder); 186 | } else { 187 | AppspectorLogger.d("Unknown monitor: %s", monitor); 188 | } 189 | } 190 | } 191 | 192 | private void withSharedInstance(@NonNull MethodChannel.Result result, @NonNull SharedInstanceAction action) { 193 | final AppSpector sharedInstance = AppSpector.shared(); 194 | if (sharedInstance != null) { 195 | result.success(action.run(sharedInstance)); 196 | } else { 197 | result.error("NotInitialized", "AppSpector shared instance is null", null); 198 | } 199 | } 200 | 201 | static MainAppSpectorHandler internalRegister(Context appContext, BinaryMessenger messenger) { 202 | final Handler mainHandler = new Handler(); 203 | final MethodChannel mainChannel = new MethodChannel(messenger, "appspector_plugin"); 204 | final MethodChannel eventChannel = new MethodChannel(messenger, "appspector_event_channel"); 205 | final MethodChannel requestChannel = new MethodChannel(messenger, "appspector_request_channel"); 206 | 207 | final MainAppSpectorHandler handler = new MainAppSpectorHandler( 208 | (Application) appContext, 209 | mainChannel, 210 | new InternalAppSpectorSessionListener(mainHandler, mainChannel), 211 | new EventReceiver(eventChannel), 212 | new RequestSender(mainHandler, requestChannel) 213 | ); 214 | 215 | handler.register(); 216 | return handler; 217 | } 218 | 219 | private static class InternalAppSpectorSessionListener implements SessionUrlListener { 220 | 221 | private final MethodChannel sessionUrlChannel; 222 | private final Handler handler; 223 | 224 | private InternalAppSpectorSessionListener(@NonNull Handler mainHandler, @NonNull MethodChannel sessionUrlChannel) { 225 | this.handler = mainHandler; 226 | this.sessionUrlChannel = sessionUrlChannel; 227 | } 228 | 229 | @Override 230 | public void onReceived(@NonNull String sessionUrl) { 231 | handler.post(() -> sessionUrlChannel.invokeMethod("onSessionUrl", sessionUrl)); 232 | } 233 | } 234 | 235 | private interface SharedInstanceAction { 236 | @Nullable 237 | Object run(@NonNull AppSpector appSpector); 238 | } 239 | 240 | private interface MonitorInitializer { 241 | void init(@NonNull Builder builder); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /android/src/main/java/com/appspector/flutter/RequestSender.java: -------------------------------------------------------------------------------- 1 | package com.appspector.flutter; 2 | 3 | import android.os.Handler; 4 | 5 | import androidx.annotation.NonNull; 6 | 7 | import io.flutter.plugin.common.MethodChannel; 8 | 9 | public class RequestSender { 10 | 11 | private final MethodChannel methodChannel; 12 | private final Handler handler; 13 | 14 | public RequestSender(@NonNull Handler mainHandler, @NonNull MethodChannel requestMethodChannel) { 15 | this.handler = mainHandler; 16 | this.methodChannel = requestMethodChannel; 17 | } 18 | 19 | public void executeRequest(final String requestName, final Object args, final ResponseCallback callback) { 20 | handler.post(new Runnable() { 21 | @Override 22 | public void run() { 23 | methodChannel.invokeMethod(requestName, args, new MethodChannel.Result() { 24 | @Override 25 | public void success(Object o) { 26 | callback.onSuccess(o); 27 | } 28 | 29 | @Override 30 | public void error(String s, String s1, Object o) { 31 | callback.onError(s + " " + s1); 32 | } 33 | 34 | @Override 35 | public void notImplemented() { 36 | callback.onError(String.format("%s method is not implemented", requestName)); 37 | } 38 | }); 39 | } 40 | }); 41 | } 42 | 43 | public interface ResponseCallback { 44 | 45 | void onSuccess(Object result); 46 | 47 | void onError(String message); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /android/src/main/java/com/appspector/flutter/event/EventHandler.java: -------------------------------------------------------------------------------- 1 | package com.appspector.flutter.event; 2 | 3 | import io.flutter.plugin.common.MethodCall; 4 | import io.flutter.plugin.common.MethodChannel; 5 | 6 | /** 7 | * Abstract EventHandler to handle calls from AppSpector Flutter SDK 8 | */ 9 | public abstract class EventHandler { 10 | 11 | /** 12 | * Event identifier what is used in Flutter part of SDK 13 | * 14 | * @return event name. Cannot be null 15 | */ 16 | public abstract String eventName(); 17 | 18 | /** 19 | * Method for handling received event 20 | * 21 | * @param call contains method name and arguments 22 | */ 23 | public abstract void handle(MethodCall call); 24 | } 25 | -------------------------------------------------------------------------------- /android/src/main/java/com/appspector/flutter/event/EventReceiver.java: -------------------------------------------------------------------------------- 1 | package com.appspector.flutter.event; 2 | 3 | import com.appspector.sdk.core.util.AppspectorLogger; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import io.flutter.plugin.common.MethodCall; 9 | import io.flutter.plugin.common.MethodChannel; 10 | 11 | /** 12 | * EventReceiver is locator of a sdk methods. It chooses the method by name from registered ones 13 | * and execute invocation on it. 14 | */ 15 | public final class EventReceiver { 16 | 17 | @SuppressWarnings({"FieldCanBeLocal", "unused"}) 18 | private final MethodChannel methodChannel; 19 | private final Map registeredEvents = new HashMap<>(); 20 | 21 | public EventReceiver(MethodChannel eventMethodChannel) { 22 | eventMethodChannel.setMethodCallHandler(new InternalMethodCallHandler()); 23 | this.methodChannel = eventMethodChannel; 24 | } 25 | 26 | public void unsubscribe() { 27 | methodChannel.setMethodCallHandler(null); 28 | } 29 | 30 | /** 31 | * Registration of Sdk Method. In case when sdk method with the same name is already 32 | * registered at current dispatcher method will throw IllegalStateException 33 | * 34 | * @param eventHandler is Sdk Method what should be registered 35 | */ 36 | public void registerEventHandler(EventHandler eventHandler) { 37 | if (registeredEvents.containsKey(eventHandler.eventName())) { 38 | throw new IllegalStateException("Action with same method name (%s) is already registered"); 39 | } 40 | registeredEvents.put(eventHandler.eventName(), eventHandler); 41 | } 42 | 43 | private void handleEvent(MethodCall call, MethodChannel.Result result) { 44 | final EventHandler action = registeredEvents.get(call.method); 45 | if (action != null) { 46 | action.handle(call); 47 | result.success(null); 48 | return; 49 | } 50 | AppspectorLogger.d("Can't find action for method %s", call.method); 51 | result.notImplemented(); 52 | } 53 | 54 | private class InternalMethodCallHandler implements MethodChannel.MethodCallHandler { 55 | 56 | @Override 57 | public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { 58 | handleEvent(methodCall, result); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /android/src/main/java/com/appspector/flutter/event/http/FlutterHttpTracker.java: -------------------------------------------------------------------------------- 1 | package com.appspector.flutter.event.http; 2 | 3 | import com.appspector.sdk.monitors.http.HttpMonitorObserver; 4 | import com.appspector.sdk.monitors.http.HttpRequest; 5 | import com.appspector.sdk.monitors.http.HttpResponse; 6 | 7 | import java.util.Map; 8 | 9 | final class FlutterHttpTracker { 10 | 11 | private static final String TRACKER_ID = "flutter_client_io"; 12 | 13 | private FlutterHttpTracker() { 14 | } 15 | 16 | @SuppressWarnings({"ConstantConditions", "unchecked"}) 17 | static void trackResponse(Map response) { 18 | Object tookMs = response.get("tookMs"); 19 | HttpMonitorObserver.getTracker(TRACKER_ID).track(new HttpResponse.Builder() 20 | .requestUid((String) response.get("uid")) 21 | .code((int) response.get("code")) 22 | .error((String) response.get("error")) 23 | .body((byte[]) response.get("body")) 24 | .tookMs(tookMs instanceof Long ? (Long) tookMs : ((Integer) tookMs).longValue()) 25 | .addHeaders((Map) response.get("headers")) 26 | .build()); 27 | } 28 | 29 | @SuppressWarnings({"ConstantConditions", "unchecked"}) 30 | static void trackRequest(Map requestData) { 31 | HttpMonitorObserver.getTracker(TRACKER_ID).track(new HttpRequest.Builder() 32 | .uid((String) requestData.get("uid")) 33 | .url((String) requestData.get("url")) 34 | .addHeaders((Map) requestData.get("headers")) 35 | .method((String) requestData.get("method"), (byte[]) requestData.get("body")) 36 | .build()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /android/src/main/java/com/appspector/flutter/event/http/HttpRequestEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.appspector.flutter.event.http; 2 | 3 | import com.appspector.flutter.event.EventHandler; 4 | 5 | import java.util.Map; 6 | 7 | import io.flutter.plugin.common.MethodCall; 8 | 9 | public final class HttpRequestEventHandler extends EventHandler { 10 | 11 | @Override 12 | public String eventName() { 13 | return "http-request"; 14 | } 15 | 16 | @Override 17 | @SuppressWarnings("unchecked") 18 | public void handle(MethodCall call) { 19 | final Map requestData = (Map) call.arguments; 20 | FlutterHttpTracker.trackRequest(requestData); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android/src/main/java/com/appspector/flutter/event/http/HttpResponseEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.appspector.flutter.event.http; 2 | 3 | import com.appspector.flutter.event.EventHandler; 4 | 5 | import java.util.Map; 6 | 7 | import io.flutter.plugin.common.MethodCall; 8 | 9 | public final class HttpResponseEventHandler extends EventHandler { 10 | 11 | @Override 12 | public String eventName() { 13 | return "http-response"; 14 | } 15 | 16 | @Override 17 | @SuppressWarnings("unchecked") 18 | public void handle(MethodCall call) { 19 | final Map responseData = (Map) call.arguments; 20 | FlutterHttpTracker.trackResponse(responseData); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android/src/main/java/com/appspector/flutter/event/log/LogEventHandler.java: -------------------------------------------------------------------------------- 1 | package com.appspector.flutter.event.log; 2 | 3 | import com.appspector.flutter.event.EventHandler; 4 | import com.appspector.sdk.monitors.log.Logger; 5 | 6 | import java.util.Map; 7 | 8 | import io.flutter.plugin.common.MethodCall; 9 | 10 | public class LogEventHandler extends EventHandler { 11 | 12 | @Override 13 | public String eventName() { 14 | return "log-event"; 15 | } 16 | 17 | @Override 18 | @SuppressWarnings({"unchecked", "ConstantConditions"}) 19 | public void handle(MethodCall call) { 20 | Map args = (Map) call.arguments; 21 | Logger.log( 22 | (Integer) args.get("level"), 23 | (String) args.get("tag"), 24 | (String) args.get("message") 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /android/src/main/java/com/appspector/flutter/screenshot/FlutterScreenshotFactory.java: -------------------------------------------------------------------------------- 1 | package com.appspector.flutter.screenshot; 2 | 3 | import com.appspector.flutter.RequestSender; 4 | import com.appspector.sdk.monitors.screenshot.ScreenshotCallback; 5 | import com.appspector.sdk.monitors.screenshot.ScreenshotFactory; 6 | 7 | import java.util.HashMap; 8 | 9 | public class FlutterScreenshotFactory implements ScreenshotFactory { 10 | 11 | private final RequestSender requestSender; 12 | 13 | public FlutterScreenshotFactory(RequestSender requestSender) { 14 | this.requestSender = requestSender; 15 | } 16 | 17 | @Override 18 | public void takeScreenshot(int maxWidth, int quality, final ScreenshotCallback screenshotCallback) { 19 | final HashMap args = new HashMap<>(); 20 | args.put("max_width", maxWidth); 21 | args.put("quality", quality); 22 | requestSender.executeRequest("take_screenshot", args, new RequestSender.ResponseCallback() { 23 | @Override 24 | public void onSuccess(Object result) { 25 | screenshotCallback.onSuccess((byte[]) result); 26 | } 27 | 28 | @Override 29 | public void onError(String message) { 30 | screenshotCallback.onError(message); 31 | } 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # Visual Studio Code related 20 | .vscode/ 21 | 22 | # Flutter/Dart/Pub related 23 | **/doc/api/ 24 | .dart_tool/ 25 | .flutter-plugins 26 | .packages 27 | .pub-cache/ 28 | .pub/ 29 | build/ 30 | 31 | # Android related 32 | **/android/**/gradle-wrapper.jar 33 | **/android/.gradle 34 | **/android/captures/ 35 | **/android/gradlew 36 | **/android/gradlew.bat 37 | **/android/local.properties 38 | **/android/**/GeneratedPluginRegistrant.java 39 | 40 | # iOS/XCode related 41 | **/ios/**/*.mode1v3 42 | **/ios/**/*.mode2v3 43 | **/ios/**/*.moved-aside 44 | **/ios/**/*.pbxuser 45 | **/ios/**/*.perspectivev3 46 | **/ios/**/*sync/ 47 | **/ios/**/.sconsign.dblite 48 | **/ios/**/.tags* 49 | **/ios/**/.vagrant/ 50 | **/ios/**/DerivedData/ 51 | **/ios/**/Icon? 52 | **/ios/**/Pods/ 53 | **/ios/**/.symlinks/ 54 | /ios/.symlinks/ 55 | **/ios/**/profile 56 | **/ios/**/xcuserdata 57 | **/ios/.generated/ 58 | **/ios/Flutter/App.framework 59 | **/ios/Flutter/Flutter.framework 60 | **/ios/Flutter/Generated.xcconfig 61 | **/ios/Flutter/.last_build_id 62 | **/ios/Flutter/app.flx 63 | **/ios/Flutter/app.zip 64 | **/ios/Flutter/flutter_assets/ 65 | **/ios/ServiceDefinitions.json 66 | **/ios/Runner/GeneratedPluginRegistrant.* 67 | 68 | # Exceptions to above rules. 69 | !**/ios/**/default.mode1v3 70 | !**/ios/**/default.mode2v3 71 | !**/ios/**/default.pbxuser 72 | !**/ios/**/default.perspectivev3 73 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 74 | flutter_export_environment.sh 75 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # AppSpector plugin example app 2 | 3 | Demonstrates how to use the AppSpector plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.io/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.io/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.io/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 33 29 | namespace "com.appspector.flutter.example" 30 | 31 | lintOptions { 32 | disable 'InvalidPackage' 33 | } 34 | 35 | defaultConfig { 36 | applicationId "com.appspector.flutter.example" 37 | minSdkVersion 21 38 | targetSdkVersion 33 39 | 40 | versionCode flutterVersionCode.toInteger() 41 | versionName flutterVersionName 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | configurations.all { 54 | resolutionStrategy.eachDependency { DependencyResolveDetails details -> 55 | if (details.requested.group == "org.jetbrains.kotlin") { 56 | details.useVersion "1.8.0" 57 | details.because "Use the newest kotlin version" 58 | } 59 | } 60 | } 61 | 62 | flutter { 63 | source '../..' 64 | } 65 | 66 | dependencies { 67 | } 68 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 14 | 17 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/appspector/flutter/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.appspector.flutter.example; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import io.flutter.embedding.android.FlutterActivity; 6 | import io.flutter.embedding.engine.FlutterEngine; 7 | import io.flutter.plugins.GeneratedPluginRegistrant; 8 | 9 | public class MainActivity extends FlutterActivity { 10 | 11 | @Override 12 | public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { 13 | GeneratedPluginRegistrant.registerWith(flutterEngine); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:8.0.2' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | tasks.register("clean", Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip 7 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/assets/patch.json: -------------------------------------------------------------------------------- 1 | { 2 | "age": 26 3 | } 4 | -------------------------------------------------------------------------------- /example/assets/post.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jon Snow", 3 | "age": 25, 4 | "country": "Winterfell", 5 | "gender": "male", 6 | "additional": "Jon Snow is a fictional character in the A Song of Ice and Fire series of fantasy novels by American author George R. R. Martin, and its television adaptation Game of Thrones, in which he is portrayed by English actor Kit Harington. He is a prominent point of view character in the novels, and has been called one of the author's \"finest creations\" and most popular characters by The New York Times.[1][2] Jon is a main character in the TV series, and his storyline in the 2015 season 5 finale generated a strong reaction among viewers. Speculation about the character's parentage has also been a popular topic of discussion among fans of both the books and the TV series." 7 | } 8 | -------------------------------------------------------------------------------- /example/assets/put.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jon Snow", 3 | "age": "26", 4 | "country": "Winterfell", 5 | "gender": "male", 6 | "additional": "Jon Snow is a fictional character in the A Song of Ice and Fire series of fantasy novels by American author George R. R. Martin, and its television adaptation Game of Thrones, in which he is portrayed by English actor Kit Harington. He is a prominent point of view character in the novels, and has been called one of the author's \"finest creations\" and most popular characters by The New York Times.[1][2] Jon is a main character in the TV series, and his storyline in the 2015 season 5 finale generated a strong reaction among viewers. Speculation about the character's parentage has also been a popular topic of discussion among fans of both the books and the TV series." 7 | } 8 | -------------------------------------------------------------------------------- /example/ios/AppSpectorPluginTestPlan.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "AC7AA52D-790A-4B2A-B085-2AE5B3C71E2A", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | "targetForVariableExpansion" : { 8 | "containerPath" : "container:Runner.xcodeproj", 9 | "identifier" : "97C146ED1CF9000F007C117D", 10 | "name" : "Runner" 11 | } 12 | } 13 | } 14 | ], 15 | "defaultOptions" : { 16 | "codeCoverage" : { 17 | "targets" : [ 18 | { 19 | "containerPath" : "container:Pods\/Pods.xcodeproj", 20 | "identifier" : "8EE4B296639698DC467B95DEA0D6285A", 21 | "name" : "appspector" 22 | } 23 | ] 24 | } 25 | }, 26 | "testTargets" : [ 27 | { 28 | "target" : { 29 | "containerPath" : "container:Runner.xcodeproj", 30 | "identifier" : "0EB522EC2260D6F40076C990", 31 | "name" : "AppSpectorPluginTests" 32 | } 33 | } 34 | ], 35 | "version" : 1 36 | } 37 | -------------------------------------------------------------------------------- /example/ios/AppSpectorPluginTests/ASPluginCallValidatorTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // ASPluginCallValidatorTests.m 3 | // AppSpectorPluginTests 4 | // 5 | // Created by Deszip on 22.12.2019. 6 | // Copyright © 2019 The Chromium Authors. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | #import "ASPluginCallValidator.h" 14 | 15 | 16 | @interface ASPluginCallValidatorTests : XCTestCase 17 | 18 | @property (strong, nonatomic) ASPluginCallValidator *validator; 19 | 20 | @end 21 | 22 | @implementation ASPluginCallValidatorTests 23 | 24 | - (void)setUp { 25 | self.validator = [ASPluginCallValidator new]; 26 | } 27 | 28 | - (void)tearDown { 29 | self.validator = nil; 30 | } 31 | 32 | - (void)testValidatorChecksControlMethods { 33 | expect([self.validator controlMethodSupported:kRunMethodName]).to.beTruthy(); 34 | expect([self.validator controlMethodSupported:@"FAKE_METHOD"]).to.beFalsy(); 35 | } 36 | 37 | - (void)testValidatorChecksEventMethods { 38 | expect([self.validator eventMethodSupported:kHTTPRequestMethodName]).to.beTruthy(); 39 | expect([self.validator eventMethodSupported:kHTTPResponseMethodName]).to.beTruthy(); 40 | expect([self.validator eventMethodSupported:kLogEventMethodName]).to.beTruthy(); 41 | 42 | expect([self.validator eventMethodSupported:@"FAKE_METHOD"]).to.beFalsy(); 43 | } 44 | 45 | - (void)testRunCallParametersValidation { 46 | [self verifyValidParams:@{ kAPIKeyArgument : @"API_KEY", 47 | kEnabledMonitorsArgument : @[], 48 | kMetadataArgument : @"" 49 | } forCall:kRunMethodName]; 50 | [self verifyInvalidParams:@{ @"FAKE_ARG" : @"FAKE_VALUE"} forCall:kRunMethodName]; 51 | } 52 | 53 | - (void)testHTTPRequestCallParametersValidation { 54 | [self verifyValidParams:@{ kUIDArgument : @"UID", 55 | kURLArgument : @"URL", 56 | kMethodArgument : @"METHOD", 57 | kBodyArgument : @"BODY", 58 | kHeadersArgument : @"HEADERS" } 59 | forCall:kHTTPRequestMethodName]; 60 | 61 | [self verifyInvalidParams:@{ @"FAKE_ARG" : @"FAKE_VALUE"} forCall:kHTTPRequestMethodName]; 62 | } 63 | 64 | - (void)testHTTPResponseCallParametersValidation { 65 | [self verifyValidParams:@{ kUIDArgument : @"UID", 66 | kCodeArgument : @"CODE", 67 | kBodyArgument : @"BODY", 68 | kHeadersArgument : @"BODY", 69 | kTookMSArgument : @"HEADERS" } 70 | forCall:kHTTPResponseMethodName]; 71 | 72 | [self verifyInvalidParams:@{ @"FAKE_ARG" : @"FAKE_VALUE"} forCall:kHTTPResponseMethodName]; 73 | } 74 | 75 | - (void)testLogCallParametersValidation { 76 | [self verifyValidParams:@{ kLevelArgument : @"UID", 77 | kTagArgument : @"CODE", 78 | kMessageArgument : @"BODY" } 79 | forCall:kLogEventMethodName]; 80 | 81 | [self verifyInvalidParams:@{ @"FAKE_ARG" : @"FAKE_VALUE"} forCall:kLogEventMethodName]; 82 | } 83 | 84 | #pragma mark - Validators - 85 | 86 | - (void)verifyValidParams:(ASPluginMethodArgumentsList *)args forCall:(ASPluginMethodName *)methodName { 87 | NSError *validCallError = nil; 88 | BOOL success = [self.validator argumentsValid:args call:methodName error:&validCallError]; 89 | expect(success).to.beTruthy(); 90 | expect(validCallError).to.beNil(); 91 | } 92 | 93 | - (void)verifyInvalidParams:(ASPluginMethodArgumentsList *)args forCall:(ASPluginMethodName *)methodName { 94 | NSError *invalidCallError = nil; 95 | BOOL success = [self.validator argumentsValid:args call:methodName error:&invalidCallError]; 96 | expect(success).to.beFalsy(); 97 | expect(invalidCallError).toNot.beNil(); 98 | expect(invalidCallError.localizedDescription).toNot.beNil(); 99 | } 100 | 101 | @end 102 | -------------------------------------------------------------------------------- /example/ios/AppSpectorPluginTests/ASPluginEventsHandlerTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // ASPluginEventsHandlerTests.m 3 | // AppSpectorPluginTests 4 | // 5 | // Created by Deszip on 24.12.2019. 6 | // Copyright © 2019 The Chromium Authors. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | #import "ASPluginEventsHandler.h" 14 | #import 15 | 16 | @interface ASPluginEventsHandlerTests : XCTestCase 17 | 18 | @property (strong, nonatomic) id callValidatorMock; 19 | @property (strong, nonatomic) ASPluginEventsHandler *handler; 20 | 21 | @end 22 | 23 | @implementation ASPluginEventsHandlerTests 24 | 25 | - (void)setUp { 26 | self.callValidatorMock = OCMClassMock([ASPluginCallValidator class]); 27 | self.handler = [[ASPluginEventsHandler alloc] initWithCallValidator:self.callValidatorMock]; 28 | } 29 | 30 | - (void)tearDown { 31 | self.callValidatorMock = nil; 32 | self.handler = nil; 33 | } 34 | 35 | #pragma mark - Invalid calls - 36 | 37 | - (void)testHandlerReturnsErrorForInvalidCallName { 38 | XCTestExpectation *e = [self expectationWithDescription:@""]; 39 | OCMStub([self.callValidatorMock eventMethodSupported:[OCMArg any]]).andReturn(NO); 40 | 41 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"foo" arguments:@{}]; 42 | [self.handler handleMethodCall:call result:^(id result) { 43 | expect(result).to.equal(FlutterMethodNotImplemented); 44 | [e fulfill]; 45 | }]; 46 | 47 | [self waitForExpectations:@[e] timeout:1.1]; 48 | } 49 | 50 | - (void)testHandlerReturnsErrorForInvalidCallArgs { 51 | XCTestExpectation *e = [self expectationWithDescription:@""]; 52 | 53 | OCMStub([self.callValidatorMock eventMethodSupported:[OCMArg any]]).andReturn(YES); 54 | 55 | NSString *errorDescription = @"foo_error"; 56 | NSError *error = OCMClassMock([NSError class]); 57 | OCMStub([error localizedDescription]).andReturn(errorDescription); 58 | OCMStub([self.callValidatorMock argumentsValid:[OCMArg any] call:[OCMArg any] error:[OCMArg setTo:error]]).andReturn(NO); 59 | 60 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"foo" arguments:@{}]; 61 | [self.handler handleMethodCall:call result:^(id result) { 62 | expect(result).to.equal(errorDescription); 63 | [e fulfill]; 64 | }]; 65 | 66 | [self waitForExpectations:@[e] timeout:1.1]; 67 | } 68 | 69 | - (void)testHandlerSendsLogEvent { 70 | NSDictionary *payload = @{ @"level" : @"warning", 71 | @"message" : @"test" }; 72 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:kLogEventMethodName arguments:payload]; 73 | ASExternalEvent *expectedEvent = [[ASExternalEvent alloc] initWithMonitorID:AS_LOG_MONITOR eventID:@"log" payload:payload]; 74 | 75 | [self performCall:call andValidateEvent:expectedEvent]; 76 | } 77 | 78 | - (void)testHandlerSendsHTTPRequestEvent { 79 | NSData *rawData = [@"DESDBEEF" dataUsingEncoding:NSUTF8StringEncoding]; 80 | FlutterStandardTypedData *flutterData = [FlutterStandardTypedData typedDataWithBytes:rawData]; 81 | NSString *UUID = [NSUUID UUID].UUIDString; 82 | 83 | NSDictionary *args = @{ @"uid" : UUID, 84 | @"url" : @"http://google.com", 85 | @"method" : @"GET", 86 | @"body" : flutterData, 87 | @"headers" : @{} }; 88 | 89 | NSDictionary *payload = @{ @"uuid" : UUID, 90 | @"url" : @"http://google.com", 91 | @"method" : @"GET", 92 | @"body" : rawData, 93 | @"hasLargeBody" : @(NO), 94 | @"headers" : @{} }; 95 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:kHTTPRequestMethodName arguments:args]; 96 | ASExternalEvent *expectedEvent = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-request" payload:payload]; 97 | 98 | [self performCall:call andValidateEvent:expectedEvent]; 99 | } 100 | 101 | /// If handler gets event with invalid body it sould substitute it with emty data 102 | - (void)testHandlerSendsHTTPRequestEventWithoutBody { 103 | NSString *UUID = [NSUUID UUID].UUIDString; 104 | NSDictionary *args = @{ @"uid" : UUID, 105 | @"url" : @"http://google.com", 106 | @"method" : @"GET", 107 | @"body" : [NSObject new], 108 | @"headers" : @{} }; 109 | 110 | NSDictionary *payload = @{ @"uuid" : UUID, 111 | @"url" : @"http://google.com", 112 | @"method" : @"GET", 113 | @"body" : [NSData data], 114 | @"hasLargeBody" : @(NO), 115 | @"headers" : @{} }; 116 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:kHTTPRequestMethodName arguments:args]; 117 | ASExternalEvent *expectedEvent = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-request" payload:payload]; 118 | 119 | [self performCall:call andValidateEvent:expectedEvent]; 120 | } 121 | 122 | - (void)testHandlerSendsHTTPResponseEvent { 123 | NSData *rawData = [@"DESDBEEF" dataUsingEncoding:NSUTF8StringEncoding]; 124 | FlutterStandardTypedData *flutterData = [FlutterStandardTypedData typedDataWithBytes:rawData]; 125 | NSString *UUID = [NSUUID UUID].UUIDString; 126 | 127 | NSDictionary *args = @{ @"uid" : UUID, 128 | @"code" : @(200), 129 | @"body" : flutterData, 130 | @"headers" : @{}, 131 | @"tookMs" : @(100) }; 132 | 133 | NSDictionary *payload = @{ @"uuid" : UUID, 134 | @"statusCode" : @(200), 135 | @"body" : rawData, 136 | @"hasLargeBody" : @(NO), 137 | @"headers" : @{}, 138 | @"responseDuration" : @(100), 139 | @"error" : @"" }; 140 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:kHTTPResponseMethodName arguments:args]; 141 | ASExternalEvent *expectedEvent = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-response" payload:payload]; 142 | 143 | [self performCall:call andValidateEvent:expectedEvent]; 144 | } 145 | 146 | /// If handler gets event with invalid body it sould substitute it with emty data 147 | - (void)testHandlerSendsHTTPResponseEventWithoutBody { 148 | NSString *UUID = [NSUUID UUID].UUIDString; 149 | NSDictionary *args = @{ @"uid" : UUID, 150 | @"code" : @(200), 151 | @"body" : [NSObject new], 152 | @"headers" : @{}, 153 | @"tookMs" : @(100) }; 154 | 155 | NSDictionary *payload = @{ @"uuid" : UUID, 156 | @"statusCode" : @(200), 157 | @"body" : [NSData data], 158 | @"hasLargeBody" : @(NO), 159 | @"headers" : @{}, 160 | @"responseDuration" : @(100), 161 | @"error" : @"" }; 162 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:kHTTPResponseMethodName arguments:args]; 163 | ASExternalEvent *expectedEvent = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-response" payload:payload]; 164 | 165 | [self performCall:call andValidateEvent:expectedEvent]; 166 | } 167 | 168 | 169 | #pragma mark - Event sender 170 | 171 | - (void)performCall:(FlutterMethodCall *)call andValidateEvent:(ASExternalEvent *)expectedEvent { 172 | XCTestExpectation *e = [self expectationWithDescription:@""]; 173 | 174 | OCMStub([self.callValidatorMock eventMethodSupported:[OCMArg any]]).andReturn(YES); 175 | OCMStub([self.callValidatorMock argumentsValid:[OCMArg any] call:[OCMArg any] error:[OCMArg anyObjectRef]]).andReturn(YES); 176 | 177 | id sdkMock = [OCMockObject mockForClass:[AppSpector class]]; 178 | OCMExpect([sdkMock sendEvent:[OCMArg checkWithBlock:^BOOL(ASExternalEvent *event) { 179 | expect(event.monitorID).to.equal(expectedEvent.monitorID); 180 | expect(event.eventID).to.equal(expectedEvent.eventID); 181 | expect(event.payload).to.equal(expectedEvent.payload); 182 | return YES; 183 | }]]); 184 | 185 | [self.handler handleMethodCall:call result:^(id result) { 186 | expect(result).toNot.beNil(); 187 | OCMVerifyAll(sdkMock); 188 | [e fulfill]; 189 | }]; 190 | 191 | [self waitForExpectations:@[e] timeout:1.1]; 192 | } 193 | 194 | @end 195 | -------------------------------------------------------------------------------- /example/ios/AppSpectorPluginTests/AppSpectorPluginTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppSpectorPluginTests.m 3 | // AppSpectorPluginTests 4 | // 5 | // Created by Deszip on 12/04/2019. 6 | // Copyright © 2019 The Chromium Authors. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | #import 14 | #import "AppSpectorPlugin.h" 15 | 16 | @interface AppSpectorPluginTests : XCTestCase 17 | 18 | @property (strong, nonatomic) id validatorMock; 19 | @property (strong, nonatomic) AppSpectorPlugin *handler; 20 | @property (strong, nonatomic) FlutterMethodChannel *channel; 21 | 22 | @end 23 | 24 | @implementation AppSpectorPluginTests 25 | 26 | - (void)setUp { 27 | self.validatorMock = OCMClassMock([ASPluginCallValidator class]); 28 | self.channel = OCMClassMock([FlutterMethodChannel class]); 29 | self.handler = [[AppSpectorPlugin alloc] initWithCallValidator:self.validatorMock channel:self.channel]; 30 | } 31 | 32 | - (void)tearDown { 33 | self.validatorMock = nil; 34 | self.handler = nil; 35 | } 36 | 37 | - (void)testHandlerSupportsRunCall { 38 | XCTestExpectation *e = [self expectationWithDescription:@""]; 39 | 40 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"run" arguments:@{ @"apiKey" : @"DEADBEEF", 41 | @"enabledMonitors" : @[], 42 | @"metadata" : @{} 43 | }]; 44 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES); 45 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES); 46 | 47 | id sdkMock = OCMClassMock([AppSpector class]); 48 | OCMExpect(ClassMethod([sdkMock runWithConfig:[OCMArg any]])); 49 | 50 | [self.handler handleMethodCall:call result:^(id result) { 51 | expect(result).equal(@"Ok"); 52 | OCMVerifyAll(sdkMock); 53 | [e fulfill]; 54 | }]; 55 | 56 | [self waitForExpectations:@[e] timeout:0.1]; 57 | } 58 | 59 | - (void)testHandlerSupportsStopCall { 60 | XCTestExpectation *e = [self expectationWithDescription:@""]; 61 | 62 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"stop" arguments:@{}]; 63 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES); 64 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES); 65 | 66 | id sdkMock = OCMClassMock([AppSpector class]); 67 | OCMExpect(ClassMethod([sdkMock stop])); 68 | 69 | [self.handler handleMethodCall:call result:^(id result) { 70 | expect(result).equal(@"Ok"); 71 | OCMVerifyAll(sdkMock); 72 | [e fulfill]; 73 | }]; 74 | 75 | [self waitForExpectations:@[e] timeout:0.1]; 76 | } 77 | 78 | - (void)testHandlerSupportsIsStartedCall { 79 | XCTestExpectation *e = [self expectationWithDescription:@""]; 80 | 81 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"isStarted" arguments:@{}]; 82 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES); 83 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES); 84 | 85 | id sdkMock = OCMClassMock([AppSpector class]); 86 | OCMStub(ClassMethod([sdkMock isRunning])).andReturn(YES); 87 | 88 | [self.handler handleMethodCall:call result:^(id result) { 89 | expect(result).beTruthy(); 90 | [e fulfill]; 91 | }]; 92 | 93 | [self waitForExpectations:@[e] timeout:0.1]; 94 | } 95 | 96 | - (void)testHandlerSupportsSetMetadataCall { 97 | XCTestExpectation *e = [self expectationWithDescription:@""]; 98 | 99 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"setMetadata" arguments:@{ @"key" : @"userSpecifiedDeviceName", 100 | @"value" : @"device name" 101 | }]; 102 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES); 103 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES); 104 | 105 | ASMetadata *expectedMetadata = @{AS_DEVICE_NAME_KEY:@"device name"}; 106 | 107 | id sdkMock = OCMClassMock([AppSpector class]); 108 | OCMExpect(ClassMethod([sdkMock updateMetadata: expectedMetadata])); 109 | 110 | [self.handler handleMethodCall:call result:^(id result) { 111 | expect(result).equal(@"Ok"); 112 | OCMVerifyAll(sdkMock); 113 | [e fulfill]; 114 | }]; 115 | 116 | [self waitForExpectations:@[e] timeout:0.1]; 117 | } 118 | 119 | - (void)testHandlerSupportsRemoveMetadataCall { 120 | XCTestExpectation *e = [self expectationWithDescription:@""]; 121 | 122 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"removeMetadata" arguments:@{ @"key" : @"userSpecifiedDeviceName" 123 | }]; 124 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES); 125 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES); 126 | 127 | ASMetadata *expectedMetadata = @{}; 128 | 129 | id sdkMock = OCMClassMock([AppSpector class]); 130 | OCMExpect(ClassMethod([sdkMock updateMetadata: expectedMetadata])); 131 | 132 | [self.handler handleMethodCall:call result:^(id result) { 133 | expect(result).equal(@"Ok"); 134 | OCMVerifyAll(sdkMock); 135 | [e fulfill]; 136 | }]; 137 | 138 | [self waitForExpectations:@[e] timeout:0.1]; 139 | } 140 | 141 | - (void)testHandlerValidatesCallArguments { 142 | XCTestExpectation *e = [self expectationWithDescription:@""]; 143 | 144 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"run" arguments:@{ @"invalidArg" : @"DEADBEEF" }]; 145 | 146 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES); 147 | OCMExpect([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES); 148 | 149 | [self.handler handleMethodCall:call result:^(id result) { 150 | OCMVerifyAll(self.validatorMock); 151 | [e fulfill]; 152 | }]; 153 | 154 | [self waitForExpectations:@[e] timeout:0.1]; 155 | } 156 | 157 | - (void)testHandlerDoesntPerformCallWithInvalidArgs { 158 | XCTestExpectation *e = [self expectationWithDescription:@""]; 159 | 160 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"run" arguments:@{ @"invalidArg" : @"DEADBEEF" }]; 161 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES); 162 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(NO); 163 | 164 | id sdkMock = OCMClassMock([AppSpector class]); 165 | OCMReject(ClassMethod([sdkMock runWithConfig:[OCMArg any]])); 166 | 167 | [self.handler handleMethodCall:call result:^(id result) { 168 | OCMVerifyAll(sdkMock); 169 | [e fulfill]; 170 | }]; 171 | 172 | [self waitForExpectations:@[e] timeout:0.1]; 173 | } 174 | 175 | - (void)testHandlerRejectsUnknownCalls { 176 | XCTestExpectation *e = [self expectationWithDescription:@""]; 177 | 178 | OCMExpect([self.validatorMock controlMethodSupported:[OCMArg any]]); 179 | 180 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"foo" arguments:@{}]; 181 | [self.handler handleMethodCall:call result:^(id result) { 182 | expect(result).notTo.equal(@"Ok"); 183 | OCMVerifyAll(self.validatorMock); 184 | [e fulfill]; 185 | }]; 186 | 187 | [self waitForExpectations:@[e] timeout:0.1]; 188 | } 189 | 190 | - (void)testHandlerExtractsEnvFlagFromArgs { 191 | XCTestExpectation *e = [self expectationWithDescription:@""]; 192 | 193 | FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"run" arguments:@{ @"apiKey" : @"DEADBEEF", 194 | @"enabledMonitors" : @[], 195 | @"metadata" : @{ @"APPSPECTOR_CHECK_STORE_ENVIRONMENT" : @"false" } 196 | }]; 197 | OCMStub([self.validatorMock controlMethodSupported:[OCMArg any]]).andReturn(YES); 198 | OCMStub([self.validatorMock argumentsValid:call.arguments call:call.method error:[OCMArg anyObjectRef]]).andReturn(YES); 199 | 200 | id sdkMock = OCMClassMock([AppSpector class]); 201 | OCMExpect(ClassMethod([sdkMock runWithConfig:[OCMArg checkWithBlock:^BOOL(AppSpectorConfig *config) { 202 | expect([config valueForKey:@"disableProductionCheck"]).to.beTruthy(); 203 | return YES; 204 | }]])); 205 | 206 | [self.handler handleMethodCall:call result:^(id result) { 207 | expect(result).equal(@"Ok"); 208 | OCMVerifyAll(sdkMock); 209 | [e fulfill]; 210 | }]; 211 | 212 | [self waitForExpectations:@[e] timeout:0.1]; 213 | } 214 | 215 | @end 216 | -------------------------------------------------------------------------------- /example/ios/AppSpectorPluginTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | target 'AppSpectorPluginTests' do 33 | pod 'Expecta', '~> 1.0.5' 34 | pod 'OCMock', '~> 3.4' 35 | end 36 | end 37 | 38 | post_install do |installer| 39 | installer.pods_project.targets.each do |target| 40 | flutter_additional_ios_build_settings(target) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 46 | 47 | 53 | 54 | 55 | 56 | 62 | 63 | 64 | 65 | 68 | 69 | 70 | 71 | 81 | 83 | 89 | 90 | 91 | 92 | 98 | 100 | 106 | 107 | 108 | 109 | 111 | 112 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | #import "AppSpectorPlugin.h" 5 | 6 | @implementation AppDelegate 7 | 8 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 9 | [GeneratedPluginRegistrant registerWithRegistry:self]; 10 | // Override point for customization after application launch. 11 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 12 | } 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | appspector_plugin_example 15 | CFBundlePackageType 16 | APPL 17 | 18 | CFBundleShortVersionString 19 | $(FLUTTER_BUILD_NAME) 20 | 21 | CFBundleSignature 22 | AppSpector 23 | 24 | CFBundleVersion 25 | $(FLUTTER_BUILD_NUMBER) 26 | 27 | LSRequiresIPhoneOS 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | UIViewControllerBasedStatusBarAppearance 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/lib/app_drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'routes.dart'; 3 | 4 | class SampleAppDrawer extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return Drawer( 8 | child: ListView( 9 | padding: EdgeInsets.zero, 10 | children: [ 11 | DrawerHeader( 12 | child: Text(""), 13 | decoration: BoxDecoration(color: Theme.of(context).primaryColor), 14 | ), 15 | ListTile( 16 | title: Text("Main Screen"), 17 | onTap: () => Navigator.pushReplacementNamed(context, Routes.MainPage), 18 | ), 19 | ListTile( 20 | title: Text("SQLite Monitor"), 21 | onTap: () => 22 | Navigator.pushReplacementNamed(context, Routes.SQLiteMonitorPage), 23 | ), 24 | ListTile( 25 | title: Text("Http Monitor"), 26 | onTap: () => 27 | Navigator.pushReplacementNamed(context, Routes.HttpMonitorPage), 28 | ), 29 | ListTile( 30 | title: Text("Edit Metadata"), 31 | onTap: () => 32 | Navigator.pushReplacementNamed(context, Routes.MetadataPage), 33 | ), 34 | ], 35 | )); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /example/lib/color.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const _primaryColor = 0xFF424250; 4 | const _accentColor = 0xFFF6637D; 5 | 6 | const MaterialAccentColor appSpectorAccent = MaterialAccentColor( 7 | _accentColor, 8 | {}, 9 | ); 10 | 11 | const appSpectorPrimary = MaterialColor(_primaryColor, { 12 | 50: Color(_primaryColor), 13 | 100: Color(_primaryColor), 14 | 200: Color(_primaryColor), 15 | 300: Color(_primaryColor), 16 | 400: Color(_primaryColor), 17 | 500: Color(_primaryColor), 18 | 600: Color(_primaryColor), 19 | 700: Color(_primaryColor), 20 | 800: Color(_primaryColor), 21 | 900: Color(_primaryColor), 22 | }); 23 | -------------------------------------------------------------------------------- /example/lib/http/app_http_client.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'dart:convert'; 3 | import 'dart:io' show HttpClient, HttpClientRequest, HttpHeaders; 4 | 5 | import 'package:dio/dio.dart'; 6 | import 'package:flutter/services.dart' show rootBundle; 7 | import 'package:http/http.dart' as http; 8 | 9 | abstract class AppHttpClient { 10 | Future executeGet(String url); 11 | 12 | Future executeGetImage(); 13 | 14 | Future executePost(String url); 15 | 16 | Future executeDelete(String url); 17 | 18 | Future executePut(String url); 19 | 20 | Future executePatch(String url); 21 | 22 | Future executeHead(String url); 23 | 24 | Future executeTrace(String url); 25 | 26 | Future executeOptions(String url); 27 | } 28 | 29 | class FlutterHttpClient extends AppHttpClient { 30 | @override 31 | Future executeDelete(String url) { 32 | return http.delete(Uri.parse(url)).then((response) { 33 | return response.statusCode; 34 | }); 35 | } 36 | 37 | @override 38 | Future executeGet(String url) { 39 | return http.get(Uri.parse(url)).then((response) { 40 | return response.statusCode; 41 | }); 42 | } 43 | 44 | @override 45 | Future executeGetImage() { 46 | return http 47 | .get( 48 | Uri.parse("https://raw.githubusercontent.com/appspector/android-sdk/master/images/github-cover.png")) 49 | .then((response) { 50 | return response.statusCode; 51 | }); 52 | } 53 | 54 | @override 55 | Future executeHead(String url) { 56 | return http.head(Uri.parse(url)).then((response) { 57 | return response.statusCode; 58 | }); 59 | } 60 | 61 | @override 62 | Future executeOptions(String url) { 63 | throw Exception("OPTION request is not supported by current client"); 64 | } 65 | 66 | @override 67 | Future executePatch(String url) async { 68 | final body = await rootBundle.loadString("assets/patch.json"); 69 | return http.patch(Uri.parse(url), body: body).then((response) { 70 | return response.statusCode; 71 | }); 72 | } 73 | 74 | @override 75 | Future executePost(String url) async { 76 | // final body = await rootBundle.loadString("assets/post.json"); 77 | final data = """{ 78 | "eventId": 1, 79 | "companyId": 201, 80 | "jobRoleId": "3", 81 | "expressBadge": false, 82 | "fcmToken": "svsdfvdsvf" 83 | }"""; 84 | final headers = {"Content-Type": "application/json; charset=utf-8"}; 85 | return http.post(Uri.parse(url), headers: headers, body: data).then((response) { 86 | return response.statusCode; 87 | }); 88 | } 89 | 90 | @override 91 | Future executePut(String url) async { 92 | final body = await rootBundle.loadString("assets/put.json"); 93 | return http.put(Uri.parse(url), body: body).then((response) { 94 | return response.statusCode; 95 | }); 96 | } 97 | 98 | @override 99 | Future executeTrace(String url) { 100 | throw Exception("TRACE request is not supported by current client"); 101 | } 102 | } 103 | 104 | class IOHttpClient extends AppHttpClient { 105 | final client = HttpClient(); 106 | 107 | @override 108 | Future executeDelete(String url) { 109 | return _executeRequest(client.deleteUrl(Uri.parse(url))); 110 | } 111 | 112 | @override 113 | Future executeGet(String url) { 114 | return _executeRequest(client.getUrl(Uri.parse(url))); 115 | } 116 | 117 | @override 118 | Future executeGetImage() { 119 | return _executeRequest(client.getUrl(Uri.parse( 120 | "https://raw.githubusercontent.com/appspector/android-sdk/master/images/github-cover.png"))); 121 | } 122 | 123 | @override 124 | Future executeHead(String url) { 125 | return _executeRequest(client.headUrl(Uri.parse(url))); 126 | } 127 | 128 | @override 129 | Future executeOptions(String url) { 130 | return _executeRequest(client.openUrl("option", Uri.parse(url))); 131 | } 132 | 133 | @override 134 | Future executePatch(String url) { 135 | return _executeRequestWithBody( 136 | client.patchUrl(Uri.parse(url)), "assets/patch.json"); 137 | } 138 | 139 | @override 140 | Future executePost(String url) { 141 | return _executeRequestWithBody( 142 | client.postUrl(Uri.parse(url)), "assets/post.json"); 143 | } 144 | 145 | @override 146 | Future executePut(String url) { 147 | return _executeRequestWithBody( 148 | client.putUrl(Uri.parse(url)), "assets/put.json"); 149 | } 150 | 151 | @override 152 | Future executeTrace(String url) { 153 | return _executeRequest(client.openUrl("trace", Uri.parse(url))); 154 | } 155 | 156 | Future _executeRequest(Future requestFuture) { 157 | return requestFuture.then((request) { 158 | return request.close(); 159 | }).then((response) { 160 | Utf8Decoder().bind(response).listen((data) { 161 | print("Client IO has received: $data"); 162 | }); 163 | return response.statusCode; 164 | }); 165 | } 166 | 167 | Future _executeRequestWithBody( 168 | Future requestFuture, String bodyAssetName) async { 169 | final body = await rootBundle.load(bodyAssetName); 170 | return requestFuture.then((request) { 171 | request.headers.add(HttpHeaders.contentTypeHeader, "application/json"); 172 | request.add(body.buffer.asUint8List()); 173 | return request.close(); 174 | }).then((response) { 175 | Utf8Decoder().bind(response).listen((data) { 176 | print("Client IO has received: $data"); 177 | }); 178 | return response.statusCode; 179 | }); 180 | } 181 | } 182 | 183 | class DioHttpClient extends AppHttpClient { 184 | 185 | final Dio dio = new Dio(); 186 | 187 | @override 188 | Future executeDelete(String url) { 189 | return dio.delete(url).then((response) { 190 | return response.statusCode ?? 0; 191 | }); 192 | } 193 | 194 | @override 195 | Future executeGet(String url) { 196 | return dio.get(url).then((response) { 197 | return response.statusCode ?? 0; 198 | }); 199 | } 200 | 201 | @override 202 | Future executeGetImage() { 203 | return dio.get("https://raw.githubusercontent.com/appspector/android-sdk/master/images/github-cover.png").then((response) { 204 | return response.statusCode ?? 0; 205 | }); 206 | } 207 | 208 | @override 209 | Future executeHead(String url) { 210 | return dio.head(url).then((response) { 211 | return response.statusCode ?? 0; 212 | }); 213 | } 214 | 215 | @override 216 | Future executeOptions(String url) { 217 | throw Exception("OPTION request is not supported by current client"); 218 | } 219 | 220 | @override 221 | Future executePatch(String url) { 222 | return dio.patch(url).then((response) { 223 | return response.statusCode ?? 0; 224 | }); 225 | } 226 | 227 | @override 228 | Future executePost(String url) { 229 | final data = { 230 | 'eventId': 1, 231 | 'companyId': 201, 232 | 'jobRoleId': '3', 233 | 'expressBadge': false, 234 | 'fcmToken': 'svsdfvdsvf' 235 | }; 236 | return dio.post(url, data: data).then((response) { 237 | return response.statusCode ?? 0; 238 | }); 239 | } 240 | 241 | @override 242 | Future executePut(String url) { 243 | return dio.put(url).then((response) { 244 | return response.statusCode ?? 0; 245 | }); 246 | } 247 | 248 | @override 249 | Future executeTrace(String url) { 250 | throw Exception("TRACE request is not supported by current client"); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /example/lib/http/http_request_item.dart: -------------------------------------------------------------------------------- 1 | import 'app_http_client.dart'; 2 | 3 | class HttpRequestItems { 4 | final String title; 5 | final Future Function(AppHttpClient, String) action; 6 | 7 | HttpRequestItems(this.title, this.action); 8 | } 9 | -------------------------------------------------------------------------------- /example/lib/http_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:dio/dio.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'app_drawer.dart'; 5 | import 'http/app_http_client.dart'; 6 | import 'http/http_request_item.dart'; 7 | 8 | class HttpMonitorPage extends StatefulWidget { 9 | @override 10 | State createState() => HttpMonitorPageState(); 11 | } 12 | 13 | class HttpMonitorPageState extends State { 14 | static const _CLIENT_HTTP_LIB = 0; 15 | static const _CLIENT_IO = 1; 16 | static const _CLIENT_DIO = 2; 17 | 18 | final _url = "https://google.com"; 19 | final _flutterHttpClient = FlutterHttpClient(); 20 | final _ioHttpClient = IOHttpClient(); 21 | final _dioHttpClient = DioHttpClient(); 22 | 23 | final List requestMethods = [ 24 | new HttpRequestItems("GET Request", (httpClient, url) { 25 | return httpClient.executeGet(url); 26 | }), 27 | new HttpRequestItems("GET Image Request", (httpClient, _) { 28 | return httpClient.executeGetImage(); 29 | }), 30 | new HttpRequestItems("POST Request", (httpClient, url) { 31 | return httpClient.executePost(url); 32 | }), 33 | new HttpRequestItems("DELETE Request", (httpClient, url) { 34 | return httpClient.executeDelete(url); 35 | }), 36 | new HttpRequestItems("PUT Request", (httpClient, url) { 37 | return httpClient.executePut(url); 38 | }), 39 | new HttpRequestItems("PATCH Request", (httpClient, url) { 40 | return httpClient.executePatch(url); 41 | }), 42 | new HttpRequestItems("HEAD Request", (httpClient, url) { 43 | return httpClient.executeHead(url); 44 | }), 45 | new HttpRequestItems("TRACE Request", (httpClient, url) { 46 | return httpClient.executeTrace(url); 47 | }), 48 | new HttpRequestItems("OPTIONS Request", (httpClient, url) { 49 | return httpClient.executeOptions(url); 50 | }) 51 | ]; 52 | 53 | int _selectedClient = _CLIENT_HTTP_LIB; 54 | int? _statusCode; 55 | String? _error; 56 | int? _requestDuration; 57 | 58 | @override 59 | Widget build(BuildContext context) { 60 | return Scaffold( 61 | appBar: AppBar( 62 | title: Text("HTTP Monitor"), 63 | ), 64 | drawer: SampleAppDrawer(), 65 | body: SingleChildScrollView( 66 | child: Padding( 67 | padding: EdgeInsets.symmetric(horizontal: 15.0), 68 | child: Column(children: [ 69 | const Text( 70 | "Choose HTTP client:", 71 | style: TextStyle( 72 | fontWeight: FontWeight.bold, 73 | fontSize: 20.0, 74 | height: 2, 75 | ), 76 | ), 77 | Row(children: [ 78 | Expanded( 79 | child: RadioListTile( 80 | value: _CLIENT_HTTP_LIB, 81 | groupValue: _selectedClient, 82 | title: const Text("HTTP Lib"), 83 | onChanged: _onClientSelectChanged)), 84 | Expanded( 85 | child: RadioListTile( 86 | value: _CLIENT_IO, 87 | title: const Text("IO"), 88 | groupValue: _selectedClient, 89 | onChanged: _onClientSelectChanged)), 90 | Expanded( 91 | child: RadioListTile( 92 | value: _CLIENT_DIO, 93 | title: const Text("DIO"), 94 | groupValue: _selectedClient, 95 | onChanged: _onClientSelectChanged)) 96 | ]), 97 | Container( 98 | margin: EdgeInsets.only(top: 24.0), 99 | child: 100 | Text.rich(TextSpan(children: _createResultedText()))), 101 | GridView.count( 102 | shrinkWrap: true, 103 | crossAxisCount: 2, 104 | primary: false, 105 | childAspectRatio: 3, 106 | children: createRequestMethodWidgetList(), 107 | ) 108 | ]))), 109 | ); 110 | } 111 | 112 | List createRequestMethodWidgetList() { 113 | return requestMethods.map((item) { 114 | return Container( 115 | alignment: Alignment.center, 116 | // margin: EdgeInsets.only(top: 24.0), 117 | child: ElevatedButton( 118 | child: Text(item.title), 119 | onPressed: () { 120 | Stopwatch stopwatch = Stopwatch()..start(); 121 | item.action(_provideClient(), _url).then((responseCode) { 122 | _onHttpResponse(responseCode, stopwatch.elapsedMilliseconds); 123 | }).onError((error, stackTrace) => _onHttpError( 124 | error as Exception, stopwatch.elapsedMilliseconds)); 125 | })); 126 | }).toList(); 127 | } 128 | 129 | AppHttpClient _provideClient() { 130 | switch (_selectedClient) { 131 | case _CLIENT_HTTP_LIB: 132 | return _flutterHttpClient; 133 | case _CLIENT_IO: 134 | return _ioHttpClient; 135 | case _CLIENT_DIO: 136 | return _dioHttpClient; 137 | } 138 | throw Exception("Unknown client id"); 139 | } 140 | 141 | _onHttpResponse(int statusCode, int requestDuration) { 142 | setState(() { 143 | _statusCode = statusCode; 144 | _requestDuration = requestDuration; 145 | _error = null; 146 | }); 147 | } 148 | 149 | _onHttpError(Exception e, int requestDuration) { 150 | setState(() { 151 | _statusCode = null; 152 | _requestDuration = requestDuration; 153 | _error = e is DioException 154 | ? "${e.message} (${requestDuration}ms)" 155 | : e.toString(); 156 | }); 157 | } 158 | 159 | List _createResultedText() { 160 | if (_error != null) { 161 | return [TextSpan(text: _error)]; 162 | } 163 | if (_statusCode != null) { 164 | List lines = []; 165 | lines 166 | .add(TextSpan(text: "Request finished with code: $_statusCode \n\n")); 167 | lines.add(TextSpan(text: "$_requestDuration ms")); 168 | return lines; 169 | } 170 | return [TextSpan(text: "Click any button")]; 171 | } 172 | 173 | _onClientSelectChanged(int? newValue) { 174 | setState(() { 175 | _selectedClient = newValue ?? _CLIENT_HTTP_LIB; 176 | }); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:appspector/appspector.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_appspector_example/metadata_page.dart'; 4 | import 'package:flutter_appspector_example/utils.dart'; 5 | import 'package:logging/logging.dart' as logger; 6 | 7 | import 'color.dart'; 8 | import 'http_page.dart'; 9 | import 'main_page.dart'; 10 | import 'routes.dart'; 11 | import 'sqlite_page.dart'; 12 | 13 | void main() { 14 | WidgetsFlutterBinding.ensureInitialized(); 15 | var globalSessionUrlObserver = DataObservable(); 16 | runAppSpector(globalSessionUrlObserver); 17 | runApp(MyApp(globalSessionUrlObserver)); 18 | 19 | logger.Logger.root.level = logger.Level.ALL; 20 | logger.Logger.root.onRecord.listen((logger.LogRecord rec) { 21 | Logger.log( 22 | LogLevel.DEBUG, rec.loggerName, "(${rec.level.name}) ${rec.message}"); 23 | print('${rec.level.name}: ${rec.time}: ${rec.message}'); 24 | }); 25 | } 26 | 27 | void runAppSpector(DataObservable sessionObserver) { 28 | final config = Config() 29 | ..iosApiKey = "YjU1NDVkZGEtN2U3Zi00MDM3LTk5ZGQtNzdkNzY3YmUzZGY2" 30 | ..androidApiKey = "MWM1YTZlOTItMmU4OS00NGI2LWJiNGQtYjdhZDljNjBhYjcz" 31 | ..monitors = [ 32 | Monitors.http, 33 | Monitors.logs, 34 | Monitors.fileSystem, 35 | Monitors.screenshot, 36 | Monitors.environment, 37 | Monitors.location, 38 | Monitors.performance, 39 | Monitors.sqLite, 40 | Monitors.sharedPreferences, 41 | Monitors.analytics, 42 | Monitors.notification, 43 | Monitors.userDefaults, 44 | Monitors.coreData 45 | ]; 46 | 47 | AppSpectorPlugin.run(config); 48 | AppSpectorPlugin.shared().sessionUrlListener = 49 | (sessionUrl) => {sessionObserver.setValue(sessionUrl)}; 50 | } 51 | 52 | class MyApp extends StatelessWidget { 53 | final DataObservable _sessionUrlObserver; 54 | 55 | MyApp(this._sessionUrlObserver); 56 | 57 | @override 58 | Widget build(BuildContext context) { 59 | return MaterialApp( 60 | title: 'Flutter Demo', 61 | theme: ThemeData( 62 | // This is the theme of your application. 63 | // 64 | // Try running your application with "flutter run". You'll see the 65 | // application has a blue toolbar. Then, without quitting the app, try 66 | // changing the primarySwatch below to Colors.green and then invoke 67 | // "hot reload" (press "r" in the console where you ran "flutter run", 68 | // or press Run > Flutter Hot Reload in IntelliJ). Notice that the 69 | // counter didn't reset back to zero; the application is not restarted. 70 | primarySwatch: appSpectorPrimary, 71 | highlightColor: appSpectorAccent), 72 | home: MyHomePage(_sessionUrlObserver, title: 'Flutter Demo Home Page'), 73 | routes: { 74 | Routes.SQLiteMonitorPage: (BuildContext context) => SQLitePage(), 75 | Routes.HttpMonitorPage: (BuildContext context) => HttpMonitorPage(), 76 | Routes.MetadataPage: (BuildContext context) => MetadataPage(), 77 | }, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /example/lib/main_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:appspector/appspector.dart' show Logger, AppSpectorPlugin; 2 | import 'package:flutter/material.dart'; 3 | import 'package:logging/logging.dart' as logger; 4 | 5 | import 'app_drawer.dart'; 6 | import 'utils.dart'; 7 | 8 | class MyHomePage extends StatefulWidget { 9 | final DataObservable _sessionUrlObserver; 10 | 11 | MyHomePage(this._sessionUrlObserver, {Key? key, required this.title}) 12 | : super(key: key); 13 | 14 | // This widget is the home page of your application. It is stateful, meaning 15 | // that it has a State object (defined below) that contains fields that affect 16 | // how it looks. 17 | 18 | // This class is the configuration for the state. It holds the values (in this 19 | // case the title) provided by the parent (in this case the App widget) and 20 | // used by the build method of the State. Fields in a Widget subclass are 21 | // always marked "final". 22 | 23 | final String title; 24 | 25 | @override 26 | _MyHomePageState createState() => _MyHomePageState(_sessionUrlObserver); 27 | } 28 | 29 | class _MyHomePageState extends State { 30 | final logger.Logger log = new logger.Logger('MyHomePageState'); 31 | 32 | int _counter = 0; 33 | 34 | bool isSdkRunning = true; 35 | late String _sessionUrl; 36 | 37 | _MyHomePageState(DataObservable sessionUrlObserver) { 38 | sessionUrlObserver.observer = (sessionUrl) => { 39 | setState(() { 40 | _sessionUrl = sessionUrl; 41 | }) 42 | }; 43 | _sessionUrl = sessionUrlObserver.getValue() ?? "Unknown"; 44 | } 45 | 46 | void _incrementCounter() { 47 | setState(() { 48 | // This call to setState tells the Flutter framework that something has 49 | // changed in this State, which causes it to rerun the build method below 50 | // so that the display can reflect the updated values. If we changed 51 | // _counter without calling setState(), then the build method would not be 52 | // called again, and so nothing would appear to happen. 53 | _counter++; 54 | }); 55 | 56 | debugPrint("Button IncrementCounter was clicked"); 57 | log.fine("Button IncrementCounter was clicked"); 58 | } 59 | 60 | void _clickLogErrorButton() { 61 | try { 62 | _throwError(); 63 | } catch (error, stackTrace) { 64 | log.finer("TAG _clickLogErrorButton log.finer", error, stackTrace); 65 | Logger.d("TAG", "_clickLogErrorButton Logger.d", error, stackTrace); 66 | } 67 | } 68 | 69 | void _throwError() { 70 | throw Error(); 71 | } 72 | 73 | void _stopSdk() async { 74 | await AppSpectorPlugin.shared().stop(); 75 | setState(() { 76 | isSdkRunning = false; 77 | }); 78 | } 79 | 80 | void _startSdk() async { 81 | await AppSpectorPlugin.shared().start(); 82 | setState(() { 83 | isSdkRunning = true; 84 | }); 85 | } 86 | 87 | void _checkSdkState() async { 88 | var isStarted = await AppSpectorPlugin.shared().isStarted(); 89 | 90 | setState(() { 91 | isSdkRunning = isStarted; 92 | }); 93 | 94 | ScaffoldMessenger.of(context).showSnackBar(SnackBar( 95 | content: Text(isStarted ? "SDK is started" : "SDK is stopped"), 96 | )); 97 | } 98 | 99 | @override 100 | Widget build(BuildContext context) { 101 | // This method is rerun every time setState is called, for instance as done 102 | // by the _incrementCounter method above. 103 | // 104 | // The Flutter framework has been optimized to make rerunning build methods 105 | // fast, so that you can just rebuild anything that needs updating rather 106 | // than having to individually change instances of widgets. 107 | return Scaffold( 108 | drawer: SampleAppDrawer(), 109 | appBar: AppBar( 110 | // Here we take the value from the MyHomePage object that was created by 111 | // the App.build method, and use it to set our appbar title. 112 | title: Text(widget.title), 113 | ), 114 | body: Center( 115 | // Center is a layout widget. It takes a single child and positions it 116 | // in the middle of the parent. 117 | child: Column( 118 | // Column is also layout widget. It takes a list of children and 119 | // arranges them vertically. By default, it sizes itself to fit its 120 | // children horizontally, and tries to be as tall as its parent. 121 | // 122 | // Invoke "debug paint" (press "p" in the console where you ran 123 | // "flutter run", or select "Toggle Debug Paint" from the Flutter tool 124 | // window in IntelliJ) to see the wireframe for each widget. 125 | // 126 | // Column has various properties to control how it sizes itself and 127 | // how it positions its children. Here we use mainAxisAlignment to 128 | // center the children vertically; the main axis here is the vertical 129 | // axis because Columns are vertical (the cross axis would be 130 | // horizontal). 131 | mainAxisAlignment: MainAxisAlignment.start, 132 | children: [ 133 | Text(_sessionUrl), 134 | const SizedBox(height: 124), 135 | _createSwitchSdkStateButton(), 136 | const SizedBox(height: 124), 137 | Text( 138 | 'You have pushed the button this many times:', 139 | ), 140 | Text( 141 | '$_counter', 142 | style: Theme.of(context).textTheme.headline3, 143 | ), 144 | TextButton( 145 | child: Text('Click here to log error'), 146 | onPressed: _clickLogErrorButton, 147 | ), 148 | ElevatedButton( 149 | child: Text('Check SDK state'), 150 | onPressed: _checkSdkState, 151 | ) 152 | ], 153 | ), 154 | ), 155 | floatingActionButton: FloatingActionButton( 156 | onPressed: _incrementCounter, 157 | tooltip: 'Increment', 158 | child: Icon(Icons.add), 159 | ), // This trailing comma makes auto-formatting nicer for build methods. 160 | ); 161 | } 162 | 163 | Widget _createSwitchSdkStateButton() { 164 | if (isSdkRunning) { 165 | return _buildSwitchSdkStateButton("Pause", Colors.redAccent, _stopSdk); 166 | } else { 167 | return _buildSwitchSdkStateButton("Resume", Colors.green, _startSdk); 168 | } 169 | } 170 | 171 | Widget _buildSwitchSdkStateButton( 172 | String text, Color color, Function() onPressed) { 173 | return ButtonTheme( 174 | minWidth: 120, 175 | height: 120, 176 | child: ElevatedButton( 177 | child: Text( 178 | text, 179 | style: TextStyle( 180 | color: Colors.white, fontWeight: FontWeight.bold, fontSize: 24), 181 | ), 182 | style: ElevatedButton.styleFrom( 183 | primary: color, 184 | shape: RoundedRectangleBorder( 185 | borderRadius: new BorderRadius.circular(60), 186 | side: BorderSide(color: Colors.transparent)), 187 | ), 188 | onPressed: onPressed, 189 | )); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /example/lib/metadata_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:appspector/appspector.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'app_drawer.dart'; 5 | 6 | class MetadataPage extends StatefulWidget { 7 | @override 8 | State createState() => MetadataPageState(); 9 | } 10 | 11 | class MetadataPageState extends State { 12 | 13 | var _deviceNameController = TextEditingController(); 14 | 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Scaffold( 19 | appBar: AppBar( 20 | title: Text("Edit Metadata"), 21 | ), 22 | drawer: SampleAppDrawer(), 23 | body: Padding( 24 | padding: EdgeInsets.symmetric(horizontal: 15.0), 25 | child: Column( 26 | children: [ 27 | TextField( 28 | controller: _deviceNameController, 29 | decoration: InputDecoration(labelText: "Device Name"), 30 | keyboardType: TextInputType.text, 31 | maxLength: 50, 32 | onEditingComplete: _deviceNameChanged, 33 | autofocus: true, 34 | ) 35 | ] 36 | ) 37 | ) 38 | ); 39 | } 40 | 41 | void _deviceNameChanged() { 42 | var newDeviceName = _deviceNameController.text; 43 | if (newDeviceName.isNotEmpty) { 44 | AppSpectorPlugin.shared().setMetadataValue(MetadataKeys.deviceName, newDeviceName); 45 | } else { 46 | AppSpectorPlugin.shared().removeMetadataValue(MetadataKeys.deviceName); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /example/lib/routes.dart: -------------------------------------------------------------------------------- 1 | class Routes { 2 | static const MainPage = "/"; 3 | static const SQLiteMonitorPage = "/sqlite_monitor_page"; 4 | static const HttpMonitorPage = "/http_monitor_page"; 5 | static const MetadataPage = "/metadata_page"; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /example/lib/sqlite/record.dart: -------------------------------------------------------------------------------- 1 | class Record { 2 | final String name; 3 | final String address; 4 | final String phone; 5 | 6 | Record(this.name, this.address, this.phone); 7 | 8 | @override 9 | String toString() { 10 | return 'Record({"name": "$name", "address": "$address", "phone": "$phone"})'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/lib/sqlite/storage.dart: -------------------------------------------------------------------------------- 1 | 2 | import 'dart:async'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:sqflite/sqflite.dart'; 5 | 6 | import 'record.dart'; 7 | 8 | abstract class RecordStorage { 9 | void save(Record record); 10 | Future getRecordsCount(); 11 | } 12 | 13 | class RecordStorageImpl implements RecordStorage { 14 | static Database? _db; 15 | static const _tableName = "records"; 16 | static const _columnId = "id"; 17 | static const _columnName = "name"; 18 | static const _columnAddress = "address"; 19 | static const _columnPhone = "phone"; 20 | 21 | Future get db async { 22 | return _db ?? await initDb(); 23 | } 24 | 25 | //Creating a database with name test.db in your directory 26 | initDb() async { 27 | var dbPath = await getDatabasesPath() + "/test.db"; 28 | var db = await openDatabase(dbPath, version: 1, onCreate: _onCreate); 29 | _db = db; 30 | return db; 31 | } 32 | 33 | // Creating a table name Employee with fields 34 | void _onCreate(Database db, int version) async { 35 | // When creating the db, create the table 36 | await db.execute( 37 | "CREATE TABLE $_tableName ($_columnId INTEGER PRIMARY KEY, $_columnName TEXT, $_columnAddress TEXT, $_columnPhone TEXT)"); 38 | debugPrint("Created tables"); 39 | } 40 | 41 | @override 42 | void save(Record record) async { 43 | var dbClient = await db; 44 | await dbClient.transaction((txn) async { 45 | return txn.insert(_tableName, _recordToMap(record)); 46 | }); 47 | debugPrint("Record is written: $record"); 48 | } 49 | 50 | _recordToMap(Record record) { 51 | final dict = Map(); 52 | dict[_columnName] = record.name; 53 | dict[_columnAddress] = record.address; 54 | dict[_columnPhone] = record.phone; 55 | return dict; 56 | } 57 | 58 | @override 59 | Future getRecordsCount() async { 60 | var dbClient = await db; 61 | var records = await dbClient.query(_tableName); 62 | return records.length; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /example/lib/sqlite_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'app_drawer.dart'; 3 | import 'sqlite/storage.dart'; 4 | import 'sqlite/record.dart'; 5 | 6 | class SQLitePage extends StatefulWidget { 7 | @override 8 | State createState() => SQLitePageState(); 9 | } 10 | 11 | class SQLitePageState extends State { 12 | final _nameController = TextEditingController(); 13 | final _addressController = TextEditingController(); 14 | final _phoneController = TextEditingController(); 15 | final _storage = RecordStorageImpl(); 16 | 17 | var _nameIsValid = false; 18 | var _addressIsValid = false; 19 | var _phoneIsValid = false; 20 | var _saveButtonEnable = false; 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return Scaffold( 25 | appBar: AppBar( 26 | title: Text("SQLite Monitor"), 27 | ), 28 | drawer: SampleAppDrawer(), 29 | body: Padding( 30 | padding: EdgeInsets.symmetric(horizontal: 15.0), 31 | child: Column(children: [ 32 | TextField( 33 | controller: _nameController, 34 | decoration: InputDecoration(labelText: "Name"), 35 | keyboardType: TextInputType.text, 36 | onChanged: _onNameChanged, 37 | autofocus: true, 38 | ), 39 | TextField( 40 | controller: _addressController, 41 | decoration: InputDecoration(labelText: "Address"), 42 | keyboardType: TextInputType.text, 43 | onChanged: _onAddressChanged, 44 | ), 45 | TextField( 46 | controller: _phoneController, 47 | decoration: InputDecoration(labelText: "Phone"), 48 | keyboardType: TextInputType.phone, 49 | onChanged: _onPhoneChanged, 50 | ), 51 | Container( 52 | margin: EdgeInsets.only(top: 24.0), 53 | child: ElevatedButton( 54 | child: Text("Add"), 55 | onPressed: _saveButtonEnable ? _onSave : null)), 56 | ]))); 57 | } 58 | 59 | _onNameChanged(String text) { 60 | _nameIsValid = text.isNotEmpty; 61 | _changeButtonState(); 62 | } 63 | 64 | _onAddressChanged(String text) { 65 | _addressIsValid = text.isNotEmpty; 66 | _changeButtonState(); 67 | } 68 | 69 | _onPhoneChanged(String text) { 70 | _phoneIsValid = text.isNotEmpty; 71 | _changeButtonState(); 72 | } 73 | 74 | _changeButtonState() { 75 | setState(() { 76 | _saveButtonEnable = _nameIsValid && _addressIsValid && _phoneIsValid; 77 | }); 78 | } 79 | 80 | _onSave() { 81 | final name = _nameController.text; 82 | final address = _addressController.text; 83 | final phone = _phoneController.text; 84 | _storage.save(Record(name, address, phone)); 85 | setState(() { 86 | _nameController.clear(); 87 | _addressController.clear(); 88 | _phoneController.clear(); 89 | }); 90 | debugPrint("Saving record"); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /example/lib/utils.dart: -------------------------------------------------------------------------------- 1 | class DataObservable { 2 | 3 | T? _value; 4 | 5 | Function(T)? observer; 6 | 7 | void setValue(T value) { 8 | this._value = value; 9 | if (observer != null) { 10 | observer!(value); 11 | } 12 | } 13 | 14 | T? getValue() => _value; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_appspector_example 2 | description: A sample app which demonstrates AppSpector initialization and features. 3 | publish_to: none 4 | 5 | environment: 6 | sdk: '>=2.14.0 <3.0.0' 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | appspector: 12 | path: ../ 13 | http: ^0.13.6 14 | sqflite: ^2.2.8+4 15 | logging: ^1.2.0 16 | dio: ^5.2.1+1 17 | 18 | flutter: 19 | uses-material-design: true 20 | assets: 21 | - assets/ 22 | -------------------------------------------------------------------------------- /github-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/github-cover.png -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/ASPluginCallValidator.h: -------------------------------------------------------------------------------- 1 | // 2 | // ASPluginCallValidator.h 3 | // appspector 4 | // 5 | // Created by Deszip on 10/04/2019. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | typedef NSString ASPluginMethodName; 13 | typedef NSString ASPluginMethodArgumentName; 14 | typedef NSDictionary ASPluginMethodArgumentsList; 15 | 16 | extern ASPluginMethodName * const kRunMethodName; 17 | extern ASPluginMethodName * const kStopMethodName; 18 | extern ASPluginMethodName * const kStartMethodName; 19 | extern ASPluginMethodName * const kIsStartedMethodName; 20 | extern ASPluginMethodName * const kSetMetadataMethodName; 21 | extern ASPluginMethodName * const kRemoveMetadataMethodName; 22 | extern ASPluginMethodName * const kHTTPRequestMethodName; 23 | extern ASPluginMethodName * const kHTTPResponseMethodName; 24 | extern ASPluginMethodName * const kLogEventMethodName; 25 | 26 | extern ASPluginMethodArgumentName * const kAPIKeyArgument; 27 | extern ASPluginMethodArgumentName * const kEnabledMonitorsArgument; 28 | extern ASPluginMethodArgumentName * const kMetadataArgument; 29 | 30 | extern ASPluginMethodArgumentName * const kUIDArgument; 31 | extern ASPluginMethodArgumentName * const kURLArgument; 32 | extern ASPluginMethodArgumentName * const kMethodArgument; 33 | extern ASPluginMethodArgumentName * const kBodyArgument; 34 | extern ASPluginMethodArgumentName * const kHeadersArgument; 35 | extern ASPluginMethodArgumentName * const kCodeArgument; 36 | extern ASPluginMethodArgumentName * const kTookMSArgument; 37 | extern ASPluginMethodArgumentName * const kLevelArgument; 38 | extern ASPluginMethodArgumentName * const kTagArgument; 39 | extern ASPluginMethodArgumentName * const kMessageArgument; 40 | 41 | 42 | @interface ASPluginCallValidator : NSObject 43 | 44 | - (BOOL)controlMethodSupported:(ASPluginMethodName *)methodName; 45 | - (BOOL)eventMethodSupported:(ASPluginMethodName *)methodName; 46 | 47 | - (BOOL)argumentsValid:(ASPluginMethodArgumentsList *)arguments 48 | call:(ASPluginMethodName *)methodName 49 | error:(__autoreleasing NSError *_Nonnull*_Nullable)error; 50 | 51 | 52 | 53 | @end 54 | 55 | NS_ASSUME_NONNULL_END 56 | -------------------------------------------------------------------------------- /ios/Classes/ASPluginCallValidator.m: -------------------------------------------------------------------------------- 1 | // 2 | // ASPluginCallValidator.m 3 | // appspector 4 | // 5 | // Created by Deszip on 10/04/2019. 6 | // 7 | 8 | #import "ASPluginCallValidator.h" 9 | 10 | ASPluginMethodName * const kRunMethodName = @"run"; 11 | ASPluginMethodName * const kStopMethodName = @"stop"; 12 | ASPluginMethodName * const kStartMethodName = @"start"; 13 | ASPluginMethodName * const kIsStartedMethodName = @"isStarted"; 14 | ASPluginMethodName * const kSetMetadataMethodName = @"setMetadata"; 15 | ASPluginMethodName * const kRemoveMetadataMethodName = @"removeMetadata"; 16 | ASPluginMethodName * const kHTTPRequestMethodName = @"http-request"; 17 | ASPluginMethodName * const kHTTPResponseMethodName = @"http-response"; 18 | ASPluginMethodName * const kLogEventMethodName = @"log-event"; 19 | 20 | ASPluginMethodArgumentName * const kAPIKeyArgument = @"apiKey"; 21 | ASPluginMethodArgumentName * const kEnabledMonitorsArgument = @"enabledMonitors"; 22 | ASPluginMethodArgumentName * const kMetadataArgument = @"metadata"; 23 | 24 | ASPluginMethodArgumentName * const kUIDArgument = @"uid"; 25 | ASPluginMethodArgumentName * const kURLArgument = @"url"; 26 | ASPluginMethodArgumentName * const kMethodArgument = @"method"; 27 | ASPluginMethodArgumentName * const kBodyArgument = @"body"; 28 | ASPluginMethodArgumentName * const kHeadersArgument = @"headers"; 29 | ASPluginMethodArgumentName * const kCodeArgument = @"code"; 30 | ASPluginMethodArgumentName * const kTookMSArgument = @"tookMs"; 31 | ASPluginMethodArgumentName * const kLevelArgument = @"level"; 32 | ASPluginMethodArgumentName * const kTagArgument = @"tag"; 33 | ASPluginMethodArgumentName * const kMessageArgument = @"message"; 34 | 35 | @interface ASPluginCallValidator () 36 | 37 | @property (strong, nonatomic) NSArray *controlMethodNames; 38 | @property (strong, nonatomic) NSArray *eventMethodNames; 39 | @property (strong, nonatomic) NSDictionary *> *methodParameters; 40 | 41 | @end 42 | 43 | @implementation ASPluginCallValidator 44 | 45 | - (instancetype)init { 46 | if (self = [super init]) { 47 | _controlMethodNames = @[kRunMethodName, 48 | kStopMethodName, 49 | kStartMethodName, 50 | kIsStartedMethodName, 51 | kSetMetadataMethodName, 52 | kRemoveMetadataMethodName]; 53 | _eventMethodNames = @[kHTTPRequestMethodName, kHTTPResponseMethodName, kLogEventMethodName]; 54 | _methodParameters = @{ kHTTPRequestMethodName : @[ @"uid", 55 | @"url", 56 | @"method", 57 | @"body", 58 | @"headers" ], 59 | kHTTPResponseMethodName : @[ @"uid", 60 | @"code", 61 | @"body", 62 | @"headers", 63 | @"tookMs" ], 64 | kLogEventMethodName : @[ @"level", 65 | @"tag", 66 | @"message" ], 67 | kRunMethodName : @[ @"apiKey", 68 | @"enabledMonitors", 69 | @"metadata"], 70 | kSetMetadataMethodName : @[@"key", 71 | @"value"], 72 | kRemoveMetadataMethodName : @[@"key"] }; 73 | } 74 | 75 | return self; 76 | } 77 | 78 | #pragma mark - Validation API - 79 | 80 | - (BOOL)controlMethodSupported:(ASPluginMethodName *)methodName { 81 | return [self.controlMethodNames containsObject:methodName]; 82 | } 83 | 84 | - (BOOL)eventMethodSupported:(ASPluginMethodName *)methodName { 85 | return [self.eventMethodNames containsObject:methodName]; 86 | } 87 | 88 | - (BOOL)argumentsValid:(ASPluginMethodArgumentsList *)arguments 89 | call:(ASPluginMethodName *)methodName 90 | error:(NSError **)error { 91 | __block BOOL isValid = YES; 92 | __block NSError *strongError = nil; 93 | [self.methodParameters[methodName] enumerateObjectsUsingBlock:^(ASPluginMethodArgumentName *argName, NSUInteger idx, BOOL *stop) { 94 | if (![arguments.allKeys containsObject:argName]) { 95 | isValid = NO; 96 | strongError = [self errorForMissingArgument:argName inCall:methodName]; 97 | *stop = YES; 98 | } 99 | }]; 100 | 101 | *error = strongError; 102 | 103 | return isValid; 104 | } 105 | 106 | #pragma mark - Private API - 107 | 108 | - (NSError *)errorForMissingArgument:(ASPluginMethodArgumentName *)argName inCall:(ASPluginMethodName *)methodName { 109 | NSString *errorMessage = [NSString stringWithFormat:@"%@ call: '%@' argument is missing", methodName, argName]; 110 | NSError *error = [NSError errorWithDomain:@"" code:0 userInfo:@{ NSLocalizedDescriptionKey : errorMessage }]; 111 | 112 | return error; 113 | } 114 | 115 | @end 116 | -------------------------------------------------------------------------------- /ios/Classes/ASPluginEventsHandler.h: -------------------------------------------------------------------------------- 1 | // 2 | // ASPluginEventsHandler.h 3 | // appspector 4 | // 5 | // Created by Deszip on 10/04/2019. 6 | // 7 | 8 | #import 9 | 10 | #import 11 | #import "ASPluginCallValidator.h" 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface ASPluginEventsHandler : NSObject 16 | 17 | - (instancetype)new NS_UNAVAILABLE; 18 | - (instancetype)init NS_UNAVAILABLE; 19 | 20 | - (instancetype)initWithCallValidator:(ASPluginCallValidator *)validator NS_DESIGNATED_INITIALIZER; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /ios/Classes/ASPluginEventsHandler.m: -------------------------------------------------------------------------------- 1 | // 2 | // ASPluginEventsHandler.m 3 | // appspector 4 | // 5 | // Created by Deszip on 10/04/2019. 6 | // 7 | 8 | #import "ASPluginEventsHandler.h" 9 | 10 | #import 11 | 12 | @interface ASPluginEventsHandler () 13 | 14 | @property (strong, nonatomic) ASPluginCallValidator *callValidator; 15 | 16 | @end 17 | 18 | @implementation ASPluginEventsHandler 19 | 20 | - (instancetype)initWithCallValidator:(ASPluginCallValidator *)validator { 21 | if (self = [super init]) { 22 | _callValidator = validator; 23 | } 24 | 25 | return self; 26 | } 27 | 28 | #pragma mark - FlutterPlugin - 29 | 30 | + (void)registerWithRegistrar:(nonnull NSObject *)registrar { } 31 | 32 | - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { 33 | if ([self.callValidator eventMethodSupported:call.method]) { 34 | // Validate arguments 35 | NSError *validationError = nil; 36 | if (![self.callValidator argumentsValid:call.arguments 37 | call:call.method 38 | error:&validationError]) { 39 | result(validationError.localizedDescription); 40 | return; 41 | } 42 | 43 | // Handle call 44 | // HTTP monitor 45 | if ([call.method isEqualToString:kHTTPRequestMethodName]) { 46 | [self handleHTTPRequestCall:call.arguments result:result]; 47 | } 48 | 49 | if ([call.method isEqualToString:kHTTPResponseMethodName]) { 50 | [self handleHTTPResponseCall:call.arguments result:result]; 51 | } 52 | 53 | // Logs monitor 54 | if ([call.method isEqualToString:kLogEventMethodName]) { 55 | [self handleLogEventCall:call.arguments result:result]; 56 | } 57 | } else { 58 | result(FlutterMethodNotImplemented); 59 | } 60 | } 61 | 62 | #pragma mark - Call handlers - 63 | #pragma mark - HTTP monitor 64 | 65 | - (void)handleHTTPRequestCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result { 66 | // Build event payload 67 | NSDictionary *payload = @{ @"uuid" : arguments[@"uid"], 68 | @"url" : arguments[@"url"], 69 | @"method" : arguments[@"method"], 70 | @"body" : [self unwrapData:arguments[@"body"]], 71 | @"hasLargeBody" : @(NO), 72 | @"headers" : arguments[@"headers"] }; 73 | 74 | // Send event 75 | ASExternalEvent *event = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-request" payload:payload]; 76 | [AppSpector sendEvent:event]; 77 | 78 | result(@"Ok"); 79 | } 80 | 81 | - (void)handleHTTPResponseCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result { 82 | // Build event payload 83 | NSDictionary *payload = @{ @"uuid" : arguments[@"uid"], 84 | @"statusCode" : arguments[@"code"], 85 | @"body" : [self unwrapData:arguments[@"body"]], 86 | @"hasLargeBody" : @(NO), 87 | @"headers" : arguments[@"headers"], 88 | @"responseDuration" : arguments[@"tookMs"], 89 | @"error" : @"" }; 90 | 91 | // Send event 92 | ASExternalEvent *event = [[ASExternalEvent alloc] initWithMonitorID:AS_HTTP_MONITOR eventID:@"http-response" payload:payload]; 93 | [AppSpector sendEvent:event]; 94 | 95 | result(@"Ok"); 96 | } 97 | 98 | #pragma mark - Logs monitor 99 | 100 | - (void)handleLogEventCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result { 101 | // Build event payload 102 | NSDictionary *payload = @{ @"level" : arguments[@"level"], 103 | @"message" : arguments[@"message"] }; 104 | 105 | // Send event 106 | ASExternalEvent *event = [[ASExternalEvent alloc] initWithMonitorID:AS_LOG_MONITOR eventID:@"log" payload:payload]; 107 | [AppSpector sendEvent:event]; 108 | 109 | result(@"Ok"); 110 | } 111 | 112 | #pragma mark - Tools 113 | 114 | - (NSData *)unwrapData:(id)flutterData { 115 | NSData *unwrappedData = [NSData data]; 116 | if ([flutterData isKindOfClass:[FlutterStandardTypedData class]]) { 117 | unwrappedData = [(FlutterStandardTypedData *)flutterData data]; 118 | } 119 | 120 | return unwrappedData; 121 | } 122 | 123 | @end 124 | -------------------------------------------------------------------------------- /ios/Classes/AppSpectorPlugin.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppSpectorPlugin.h 3 | // appspector 4 | // 5 | // Created by Deszip on 10/04/2019. 6 | // 7 | 8 | #import 9 | 10 | #import "ASPluginCallValidator.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface AppSpectorPlugin : NSObject 15 | 16 | - (instancetype)new NS_UNAVAILABLE; 17 | - (instancetype)init NS_UNAVAILABLE; 18 | 19 | - (instancetype)initWithCallValidator:(ASPluginCallValidator *)validator 20 | channel:(FlutterMethodChannel *)controlChannel NS_DESIGNATED_INITIALIZER; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /ios/Classes/AppSpectorPlugin.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppSpectorPlugin.m 3 | // appspector 4 | // 5 | // Created by Deszip on 10/04/2019. 6 | // 7 | 8 | #import "AppSpectorPlugin.h" 9 | #import 10 | 11 | #import "ASPluginEventsHandler.h" 12 | 13 | static NSString * const kControlChannelName = @"appspector_plugin"; 14 | static NSString * const kEventChannelName = @"appspector_event_channel"; 15 | 16 | static NSString * const kEnvCheckOptionKey = @"APPSPECTOR_CHECK_STORE_ENVIRONMENT"; 17 | 18 | @interface AppSpectorPlugin () 19 | 20 | @property (strong, nonatomic) ASPluginCallValidator *callValidator; 21 | @property (strong, nonatomic) ASPluginEventsHandler *eventsHandler; 22 | @property (strong, nonatomic) FlutterMethodChannel *controlChannel; 23 | 24 | @end 25 | 26 | @implementation AppSpectorPlugin 27 | 28 | + (instancetype)rootPluginWithChannel:(FlutterMethodChannel *)controlChannel { 29 | static AppSpectorPlugin *rootPlugin = nil; 30 | static dispatch_once_t onceToken; 31 | dispatch_once(&onceToken, ^{ 32 | rootPlugin = [[[self class] alloc] initWithCallValidator:[ASPluginCallValidator new] 33 | channel:controlChannel]; 34 | }); 35 | return rootPlugin; 36 | } 37 | 38 | - (instancetype)initWithCallValidator:(ASPluginCallValidator *)validator 39 | channel:(FlutterMethodChannel *)controlChannel { 40 | if (self = [super init]) { 41 | _callValidator = validator; 42 | _eventsHandler = [[ASPluginEventsHandler alloc] initWithCallValidator:validator]; 43 | _controlChannel = controlChannel; 44 | } 45 | 46 | return self; 47 | } 48 | 49 | + (void)registerWithRegistrar:(NSObject *)registrar { 50 | FlutterMethodChannel *controlChannel = [FlutterMethodChannel methodChannelWithName:kControlChannelName binaryMessenger:[registrar messenger]]; 51 | FlutterMethodChannel *eventChannel = [FlutterMethodChannel methodChannelWithName:kEventChannelName binaryMessenger:[registrar messenger]]; 52 | 53 | AppSpectorPlugin *plugin = [AppSpectorPlugin rootPluginWithChannel:controlChannel]; 54 | 55 | [registrar addMethodCallDelegate:plugin channel:controlChannel]; 56 | [registrar addMethodCallDelegate:plugin.eventsHandler channel:eventChannel]; 57 | } 58 | 59 | #pragma mark - Call handlers - 60 | 61 | - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { 62 | if ([self.callValidator controlMethodSupported:call.method]) { 63 | // Validate arguments 64 | NSError *validationError = nil; 65 | if (![self.callValidator argumentsValid:call.arguments 66 | call:call.method 67 | error:&validationError]) { 68 | result(validationError.localizedDescription); 69 | return; 70 | } 71 | 72 | // Handle calls 73 | if ([call.method isEqualToString:kRunMethodName]) { 74 | [self handleRunCall:call.arguments result:result]; 75 | } 76 | if ([call.method isEqualToString:kStopMethodName]) { 77 | [self handleStopCallWithResult:result]; 78 | } 79 | if ([call.method isEqualToString:kStartMethodName]) { 80 | [self handleStartCallWithResult:result]; 81 | } 82 | if ([call.method isEqualToString:kIsStartedMethodName]) { 83 | [self handleIsStartedCallWithResult:result]; 84 | } 85 | if ([call.method isEqualToString:kSetMetadataMethodName]) { 86 | [self handleSetMetadataCall:call.arguments result:result]; 87 | } 88 | if ([call.method isEqualToString:kRemoveMetadataMethodName]) { 89 | [self handleRemoveMetadataCall:call.arguments result:result]; 90 | } 91 | } else { 92 | result(FlutterMethodNotImplemented); 93 | } 94 | } 95 | 96 | - (void)handleRunCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result { 97 | NSString *apiKey = arguments[@"apiKey"]; 98 | NSSet *monitorIds = [self validateAndMapRawMonitorIds:arguments[@"enabledMonitors"]]; 99 | 100 | AppSpectorConfig *config = [AppSpectorConfig configWithAPIKey:apiKey monitorIDs:monitorIds]; 101 | 102 | // Handle special case when private SDK options are transferred via metadata 103 | if (arguments[@"metadata"] != [NSNull null] && [arguments[@"metadata"][kEnvCheckOptionKey] isKindOfClass:[NSString class]]) { 104 | NSString *checkOption = arguments[@"metadata"][kEnvCheckOptionKey]; 105 | NSNumber *productionCheck = [checkOption isEqualToString:@"true"] ? @(NO) : @(YES); 106 | [config setValue:productionCheck forKey:@"disableProductionCheck"]; 107 | 108 | // Drop flag to not include in session metadata 109 | NSMutableDictionary *mutableArgs = [arguments mutableCopy]; 110 | [[arguments mutableCopy] removeObjectForKey:kEnvCheckOptionKey]; 111 | arguments = [mutableArgs copy]; 112 | } 113 | 114 | config.metadata = [self validateAndMapRawMeatdata:arguments[@"metadata"]]; 115 | 116 | __weak __auto_type weakSelf = self; 117 | config.startCallback = ^(NSURL * _Nonnull sessionURL) { 118 | [weakSelf.controlChannel invokeMethod:@"onSessionUrl" arguments:sessionURL.absoluteString]; 119 | }; 120 | 121 | [AppSpector runWithConfig:config]; 122 | 123 | result(@"Ok"); 124 | } 125 | 126 | - (void)handleStopCallWithResult:(FlutterResult)result { 127 | [AppSpector stop]; 128 | result(@"Ok"); 129 | } 130 | 131 | - (void)handleStartCallWithResult:(FlutterResult)result { 132 | [AppSpector start]; 133 | result(@"Ok"); 134 | } 135 | 136 | - (void)handleIsStartedCallWithResult:(FlutterResult)result { 137 | BOOL isStarted = [AppSpector isRunning]; 138 | result(@(isStarted)); 139 | } 140 | 141 | - (void)handleSetMetadataCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result { 142 | NSString *key = arguments[@"key"]; 143 | NSString *value = arguments[@"value"]; 144 | 145 | if (key != nil && (id)key != NSNull.null && value != nil && (id)value != NSNull.null) { 146 | ASMetadata *metadata = @{key : value}; 147 | [AppSpector updateMetadata:metadata]; 148 | } 149 | 150 | result(@"Ok"); 151 | } 152 | 153 | - (void)handleRemoveMetadataCall:(ASPluginMethodArgumentsList *)arguments result:(FlutterResult)result { 154 | ASMetadata *emptyMetadata = @{}; 155 | [AppSpector updateMetadata:emptyMetadata]; 156 | result(@"Ok"); 157 | } 158 | 159 | #pragma mark - Validators - 160 | 161 | - (ASMetadata *)validateAndMapRawMeatdata:(NSDictionary *)rawMetadata { 162 | if (rawMetadata == nil || (id)rawMetadata == NSNull.null) { 163 | return @{}; 164 | } 165 | 166 | __block BOOL isValid = YES; 167 | [rawMetadata enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent 168 | usingBlock:^(id key, id object, BOOL *stop) { 169 | if (key == NSNull.null || object == NSNull.null) { 170 | isValid = NO; 171 | } 172 | }]; 173 | 174 | if (isValid) { 175 | return rawMetadata; 176 | } else { 177 | NSString *metadataString = rawMetadata.description; 178 | NSString *message = @"It looks like AppSpector iOS plugin initialized with invalid metadata: \n %@ \n Please review AppSpectorPlugin initialization code. If the problem persists, please contact us at https://slack.appspector.com/"; 179 | NSLog(message, metadataString); 180 | return @{}; 181 | } 182 | } 183 | 184 | - (NSSet *)validateAndMapRawMonitorIds:(NSArray *)rawMonitorIds { 185 | NSSet *allMonitors = [NSSet setWithObjects: 186 | AS_SCREENSHOT_MONITOR, 187 | AS_SQLITE_MONITOR, 188 | AS_HTTP_MONITOR, 189 | AS_COREDATA_MONITOR, 190 | AS_PERFORMANCE_MONITOR, 191 | AS_LOG_MONITOR, 192 | AS_LOCATION_MONITOR, 193 | AS_ENVIRONMENT_MONITOR , 194 | AS_DEFAULTS_MONITOR, 195 | AS_NOTIFICATION_MONITOR, 196 | AS_ANALYTICS_MONITOR, 197 | AS_COMMANDS_MONITOR, 198 | AS_FS_MONITOR, 199 | nil]; 200 | 201 | NSMutableSet *selectedMonotirIds = [NSMutableSet new]; 202 | NSMutableSet *invalidMonitorIds = [NSMutableSet new]; 203 | 204 | for (NSString *monitorId in rawMonitorIds) { 205 | if ([allMonitors containsObject:monitorId]) { 206 | [selectedMonotirIds addObject:monitorId]; 207 | } else { 208 | [invalidMonitorIds addObject:monitorId]; 209 | } 210 | } 211 | 212 | if (invalidMonitorIds.count > 0) { 213 | NSString *monitors = [[invalidMonitorIds allObjects] componentsJoinedByString:@"\n - "]; 214 | NSString *message = @"It looks like AppSpector iOS plugin initialized with invalid monitors: \n - %@ \n Please review AppSpectorPlugin initialization code. If the problem persists, please contact us at https://slack.appspector.com/"; 215 | NSLog(message, monitors); 216 | } 217 | 218 | return selectedMonotirIds; 219 | } 220 | 221 | @end 222 | -------------------------------------------------------------------------------- /ios/appspector.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'appspector' 3 | s.version = '0.0.1' 4 | s.summary = 'AppSpector SDK' 5 | s.description = <<-DESC 6 | AppSpector SDK 7 | DESC 8 | s.homepage = 'https://appspector.com' 9 | s.license = { :file => '../LICENSE' } 10 | s.author = { 'AppSpector' => 'info@appspector.com' } 11 | s.source = { :path => '.' } 12 | s.source_files = 'Classes/**/*' 13 | s.public_header_files = 'Classes/**/*.h' 14 | s.dependency 'Flutter' 15 | s.dependency 'AppSpectorSDK' 16 | 17 | s.ios.deployment_target = '10.0' 18 | end 19 | 20 | -------------------------------------------------------------------------------- /lib/appspector.dart: -------------------------------------------------------------------------------- 1 | library appspector; 2 | 3 | export 'src/appspector_plugin.dart'; 4 | export 'src/monitors.dart'; 5 | export 'src/http/http_overrides.dart'; 6 | export 'src/log/logger.dart'; 7 | -------------------------------------------------------------------------------- /lib/src/appspector_plugin.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show Platform, HttpOverrides; 2 | 3 | import 'package:appspector/src/http/http_overrides.dart'; 4 | import 'package:appspector/src/monitors.dart'; 5 | import 'package:appspector/src/request_receiver.dart'; 6 | import 'package:flutter/services.dart' show MethodCall, MethodChannel; 7 | 8 | /// This class needed to aggregate AppSpector properties and arguments. 9 | class Config { 10 | /// API_KEY of your iOS Application. 11 | /// 12 | /// Property is optional if your app don't support iOS. 13 | /// Your can find API_KEY on settings page of your Application 14 | /// 15 | /// If you don't specify it and try to launch the app on iOS 16 | /// SDK will throw ArgumentError 17 | String? iosApiKey; 18 | 19 | /// API_KEY of your Android Application. 20 | /// 21 | /// Property is optional if your app don't support Android. 22 | /// Your can find API_KEY on settings page of your Application 23 | /// 24 | /// If you don't specify it and try to launch the app on Android 25 | /// SDK will throw ArgumentError 26 | String? androidApiKey; 27 | 28 | /// Collection of metadata information 29 | /// 30 | /// Property is optional. It allows to attach some additional 31 | /// information to session. For example, you can specify device name by 32 | /// putting it with MetaDataKeys.deviceName key 33 | Map? metadata; 34 | 35 | /// List of monitor which will be enabled 36 | /// 37 | /// Property is optional. By default all available monitors will be enabled. 38 | /// E.g. to enable necessary monitors you need to provide list 39 | /// like [Monitors.http, Monitors.screenshot] 40 | List? monitors; 41 | } 42 | 43 | /// This is the main class for using AppSpector. AppSpector captures various 44 | /// types of data to assist in debugging, analyzing application state and 45 | /// understanding user behavior. 46 | ///

47 | ///

Here is an example of how AppSpector is used: 48 | ///

 49 | /// void main() {
 50 | ///   runAppSpector();
 51 | ///   runApp(MyApp());
 52 | /// }
 53 | ///
 54 | /// void runAppSpector() {
 55 | ///   var config = new Config();
 56 | ///   config.iosApiKey = "Your iOS API_KEY";
 57 | ///   config.androidApiKey = "Your Android API_KEY";
 58 | ///   AppSpectorPlugin.run(config);
 59 | /// }
 60 | /// 

61 | ///

For more information visit the AppSpector Page.

62 | class AppSpectorPlugin { 63 | static AppSpectorPlugin _appSpectorPlugin = 64 | AppSpectorPlugin._privateConstructor(); 65 | 66 | final MethodChannel _channel = const MethodChannel('appspector_plugin'); 67 | final RequestReceiver _requestReceiver = new RequestReceiver(); 68 | Function(String)? _sessionUrlListener; 69 | 70 | set sessionUrlListener(Function(String)? listener) { 71 | _sessionUrlListener = listener; 72 | } 73 | 74 | AppSpectorPlugin._privateConstructor(); 75 | 76 | AppSpectorPlugin._withConfig( 77 | Config config, Function(String)? sessionUrlListener) { 78 | HttpOverrides.global = AppSpectorHttpOverrides(); 79 | _requestReceiver.observeChannel(); 80 | _channel.setMethodCallHandler(_handlePluginCalls); 81 | _sessionUrlListener = sessionUrlListener; 82 | _appSpectorPlugin = this; 83 | } 84 | 85 | Future _init(Config config) { 86 | final monitors = config.monitors ?? Monitors.all(); 87 | if (Platform.isAndroid) { 88 | ArgumentError.checkNotNull(config.androidApiKey, "androidApiKey"); 89 | return _initAppSpector( 90 | config.androidApiKey, 91 | _filterByPlatform(monitors, SupportedPlatform.android), 92 | config.metadata); 93 | } else if (Platform.isIOS) { 94 | ArgumentError.checkNotNull(config.iosApiKey, "iosApiKey"); 95 | return _initAppSpector(config.iosApiKey, 96 | _filterByPlatform(monitors, SupportedPlatform.ios), config.metadata); 97 | } else { 98 | return Future.error("AppSpector doesn't support current platform"); 99 | } 100 | } 101 | 102 | Future _handlePluginCalls(MethodCall methodCall) async { 103 | if (methodCall.method == "onSessionUrl") { 104 | _sessionUrlListener?.call(methodCall.arguments); 105 | } 106 | } 107 | 108 | /// Returns shared instance of SDK plugin 109 | static AppSpectorPlugin shared() => _appSpectorPlugin; 110 | 111 | /// Method for starting AppSpector with supplied configs 112 | static Future run(Config config) async { 113 | final sharedInstance = shared(); 114 | final isStarted = await sharedInstance.isStarted(); 115 | if (!isStarted) { 116 | return new AppSpectorPlugin._withConfig( 117 | config, sharedInstance._sessionUrlListener) 118 | ._init(config); 119 | } 120 | } 121 | 122 | _initAppSpector(String? apiKey, Iterable monitors, 123 | Map? metadata) => 124 | _channel.invokeMethod("run", { 125 | "apiKey": apiKey, 126 | "enabledMonitors": monitors.map((m) => m.id).toList(), 127 | "metadata": metadata 128 | }); 129 | 130 | /// Stop all monitors and events sending 131 | Future stop() => _channel.invokeMethod("stop"); 132 | 133 | /// Resume sending events and work of all monitors 134 | Future start() => _channel.invokeMethod("start"); 135 | 136 | /// Returns true if sdk is started 137 | Future isStarted() => 138 | _channel.invokeMethod("isStarted").then((value) => value ?? false); 139 | 140 | /// Set metadata value 141 | Future setMetadataValue(String key, String value) => 142 | _channel.invokeMethod("setMetadata", {"key": key, "value": value}); 143 | 144 | /// Remove metadata value 145 | Future removeMetadataValue(String key) => 146 | _channel.invokeMethod("removeMetadata", {"key": key}); 147 | 148 | Iterable _filterByPlatform( 149 | List monitors, SupportedPlatform platform) { 150 | return monitors.where((m) => m.platforms.contains(platform)); 151 | } 152 | } 153 | 154 | /// Identifiers for supported metadata keys 155 | /// Sdk provides opportunity to send additional session information 156 | /// 157 | /// For more information see Config.metadata method 158 | class MetadataKeys { 159 | MetadataKeys._(); 160 | 161 | /// Supported key to change device name 162 | static const deviceName = "userSpecifiedDeviceName"; 163 | } 164 | -------------------------------------------------------------------------------- /lib/src/event_sender.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart' show MethodChannel; 2 | 3 | class EventSender { 4 | static const MethodChannel _channel = 5 | const MethodChannel('appspector_event_channel'); 6 | 7 | static sendEvent(Event event) async { 8 | await _channel.invokeMethod(event.name, event.arguments); 9 | } 10 | } 11 | 12 | abstract class Event { 13 | String get name; 14 | 15 | Map get arguments; 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/http/client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show Future; 2 | import 'dart:io'; 3 | 4 | import 'package:appspector/src/http/request_wrapper.dart'; 5 | import 'package:appspector/src/http/tracker.dart'; 6 | 7 | class AppSpectorHttpClient implements HttpClient { 8 | final HttpClient _httpClient; 9 | final String Function() _uidGenerator; 10 | 11 | AppSpectorHttpClient(this._httpClient, this._uidGenerator); 12 | 13 | @override 14 | get autoUncompress => _httpClient.autoUncompress; 15 | 16 | @override 17 | set autoUncompress(value) { 18 | _httpClient.autoUncompress = value; 19 | } 20 | 21 | @override 22 | get connectionTimeout => _httpClient.connectionTimeout; 23 | 24 | @override 25 | set connectionTimeout(value) { 26 | _httpClient.connectionTimeout = value; 27 | } 28 | 29 | @override 30 | get idleTimeout => _httpClient.idleTimeout; 31 | 32 | @override 33 | set idleTimeout(value) { 34 | _httpClient.idleTimeout = value; 35 | } 36 | 37 | @override 38 | get maxConnectionsPerHost => _httpClient.maxConnectionsPerHost; 39 | 40 | @override 41 | set maxConnectionsPerHost(value) { 42 | _httpClient.maxConnectionsPerHost = value; 43 | } 44 | 45 | @override 46 | get userAgent => _httpClient.userAgent; 47 | 48 | @override 49 | set userAgent(value) { 50 | _httpClient.userAgent = value; 51 | } 52 | 53 | @override 54 | void addCredentials( 55 | Uri url, String realm, HttpClientCredentials credentials) { 56 | return _httpClient.addCredentials(url, realm, credentials); 57 | } 58 | 59 | @override 60 | void addProxyCredentials( 61 | String host, int port, String realm, HttpClientCredentials credentials) { 62 | return _httpClient.addProxyCredentials(host, port, realm, credentials); 63 | } 64 | 65 | @override 66 | set authenticate(value) { 67 | _httpClient.authenticate = value; 68 | } 69 | 70 | @override 71 | set authenticateProxy(value) { 72 | _httpClient.authenticateProxy = value; 73 | } 74 | 75 | @override 76 | set badCertificateCallback(value) { 77 | _httpClient.badCertificateCallback = value; 78 | } 79 | 80 | @override 81 | set findProxy(String Function(Uri url)? f) { 82 | _httpClient.findProxy = f; 83 | } 84 | 85 | @override 86 | void close({bool force = false}) { 87 | return _httpClient.close(force: force); 88 | } 89 | 90 | @override 91 | Future delete(String host, int port, String path) => 92 | open("delete", host, port, path); 93 | 94 | @override 95 | Future deleteUrl(Uri url) => openUrl("delete", url); 96 | 97 | @override 98 | Future get(String host, int port, String path) => 99 | open("get", host, port, path); 100 | 101 | @override 102 | Future getUrl(Uri url) => openUrl("get", url); 103 | 104 | @override 105 | Future head(String host, int port, String path) => 106 | open("head", host, port, path); 107 | 108 | @override 109 | Future headUrl(Uri url) => openUrl("head", url); 110 | 111 | @override 112 | Future patch(String host, int port, String path) => 113 | open("patch", host, port, path); 114 | 115 | @override 116 | Future patchUrl(Uri url) => openUrl("patch", url); 117 | 118 | @override 119 | Future post(String host, int port, String path) => 120 | open("post", host, port, path); 121 | 122 | @override 123 | Future postUrl(Uri url) => openUrl("post", url); 124 | 125 | @override 126 | Future put(String host, int port, String path) => 127 | open("put", host, port, path); 128 | 129 | @override 130 | Future putUrl(Uri url) => openUrl("put", url); 131 | 132 | @override 133 | Future open( 134 | String method, String host, int port, String path) { 135 | final tracker = 136 | HttpEventTracker.fromHost(method, _uidGenerator(), host, port, path); 137 | return _httpClient.open(method, host, port, path).then((request) { 138 | return HttpRequestWrapper(request, tracker); 139 | }).onError((Exception error, stackTrace) { 140 | tracker.onError(error); 141 | return Future.error(error, stackTrace); 142 | }); 143 | } 144 | 145 | @override 146 | Future openUrl(String method, Uri url) async { 147 | final tracker = HttpEventTracker.fromUri(method, _uidGenerator(), url); 148 | return _httpClient.openUrl(method, url).then((request) { 149 | return HttpRequestWrapper(request, tracker); 150 | }).onError((Exception error, stackTrace) { 151 | tracker.onError(error); 152 | return Future.error(error, stackTrace); 153 | }); 154 | } 155 | 156 | set connectionFactory( 157 | Future> Function( 158 | Uri url, String? proxyHost, int? proxyPort)? 159 | f) => 160 | _httpClient.connectionFactory = f; 161 | 162 | set keyLog(Function(String line)? callback) => _httpClient.keyLog = callback; 163 | } 164 | -------------------------------------------------------------------------------- /lib/src/http/events.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:appspector/src/event_sender.dart' show Event; 4 | 5 | class HttpResponseEvent extends Event { 6 | final String uid; 7 | final int timeMs; 8 | final int code; 9 | final Map headers; 10 | final String? error; 11 | final Uint8List? body; 12 | 13 | HttpResponseEvent( 14 | this.uid, this.timeMs, this.code, this.headers, this.error, this.body); 15 | 16 | @override 17 | String get name => "http-response"; 18 | 19 | @override 20 | Map get arguments => { 21 | "uid": uid, 22 | "code": code, 23 | "headers": headers, 24 | "error": error, 25 | "body": body, 26 | "tookMs": timeMs 27 | }; 28 | } 29 | 30 | class HttpRequestEvent extends Event { 31 | final String uid; 32 | final String url; 33 | final String method; 34 | final Map headers; 35 | final Uint8List? body; 36 | 37 | @override 38 | String get name => "http-request"; 39 | 40 | HttpRequestEvent(this.uid, this.url, this.method, this.headers, this.body); 41 | 42 | @override 43 | Map get arguments => { 44 | "uid": uid, 45 | "url": url, 46 | "method": method, 47 | "headers": headers, 48 | "body": body 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /lib/src/http/http_overrides.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show HttpClient, HttpOverrides, SecurityContext; 2 | import 'dart:math' show Random; 3 | 4 | import 'package:appspector/src/http/client.dart'; 5 | 6 | class AppSpectorHttpOverrides extends HttpOverrides { 7 | final Random _random = new Random(); 8 | 9 | @override 10 | HttpClient createHttpClient(SecurityContext? context) { 11 | return AppSpectorHttpClient(super.createHttpClient(context), _generateUuid); 12 | } 13 | 14 | String _generateUuid() { 15 | // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12. 16 | final int special = 8 + _random.nextInt(4); 17 | 18 | return '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-' 19 | '${_bitsDigits(16, 4)}-' 20 | '4${_bitsDigits(12, 3)}-' 21 | '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-' 22 | '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}'; 23 | } 24 | 25 | String _bitsDigits(int bitCount, int digitCount) => 26 | _printDigits(_generateBits(bitCount), digitCount); 27 | 28 | int _generateBits(int bitCount) => _random.nextInt(1 << bitCount); 29 | 30 | String _printDigits(int value, int count) => 31 | value.toRadixString(16).padLeft(count, '0'); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/http/request_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:appspector/src/http/response_wrapper.dart'; 6 | import 'package:appspector/src/http/tracker.dart'; 7 | 8 | class HttpRequestWrapper extends HttpClientRequest { 9 | final HttpClientRequest _httpClientRequest; 10 | final HttpEventTracker _httpEventTracker; 11 | 12 | @override 13 | Encoding get encoding => _httpClientRequest.encoding; 14 | 15 | @override 16 | set encoding(Encoding value) => _httpClientRequest.encoding = value; 17 | 18 | @override 19 | int get contentLength => _httpClientRequest.contentLength; 20 | 21 | @override 22 | set contentLength(int value) => _httpClientRequest.contentLength = value; 23 | 24 | @override 25 | bool get bufferOutput => _httpClientRequest.bufferOutput; 26 | 27 | @override 28 | set bufferOutput(bool value) => _httpClientRequest.bufferOutput = value; 29 | 30 | @override 31 | bool get followRedirects => _httpClientRequest.followRedirects; 32 | 33 | @override 34 | set followRedirects(bool value) => _httpClientRequest.followRedirects = value; 35 | 36 | @override 37 | bool get persistentConnection => _httpClientRequest.persistentConnection; 38 | 39 | @override 40 | set persistentConnection(bool value) => 41 | _httpClientRequest.persistentConnection = value; 42 | 43 | @override 44 | int get maxRedirects => _httpClientRequest.maxRedirects; 45 | 46 | @override 47 | set maxRedirects(int value) => _httpClientRequest.maxRedirects = value; 48 | 49 | HttpRequestWrapper(this._httpClientRequest, this._httpEventTracker); 50 | 51 | @override 52 | void add(List data) { 53 | _httpEventTracker.addData(data); 54 | _httpClientRequest.add(data); 55 | } 56 | 57 | @override 58 | void addError(Object error, [StackTrace? stackTrace]) { 59 | _httpClientRequest.addError(error, stackTrace); 60 | } 61 | 62 | @override 63 | Future addStream(Stream> stream) { 64 | final List body = []; 65 | final StreamTransformer, List> streamTransformer = 66 | StreamTransformer.fromHandlers( 67 | handleData: (List data, EventSink> sink) { 68 | sink.add(data); 69 | body.addAll(data); 70 | }, handleDone: (sink) { 71 | _httpEventTracker.addData(body); 72 | sink.close(); 73 | }); 74 | final Stream> resultedStream = streamTransformer.bind(stream); 75 | return _httpClientRequest.addStream(resultedStream); 76 | } 77 | 78 | @override 79 | Future close() async { 80 | _httpEventTracker.sendRequestEvent(headers); 81 | 82 | final List body = []; 83 | final HttpClientResponse response = await _httpClientRequest.close(); 84 | return new HttpResponseWrapper( 85 | response, 86 | response.transform(StreamTransformer.fromHandlers( 87 | handleData: (List data, EventSink> sink) { 88 | sink.add(data); 89 | body.addAll(data); 90 | }, handleError: (error, stackTrace, sink) { 91 | print("HttpRequestWrapper :: ERROR RESPONSE $error $stackTrace"); 92 | }, handleDone: (sink) { 93 | _httpEventTracker.sendSuccessResponse( 94 | response.statusCode, response.headers, body); 95 | sink.close(); 96 | }))); 97 | } 98 | 99 | @override 100 | HttpConnectionInfo? get connectionInfo => _httpClientRequest.connectionInfo; 101 | 102 | @override 103 | List get cookies => _httpClientRequest.cookies; 104 | 105 | @override 106 | Future get done => _httpClientRequest.done; 107 | 108 | @override 109 | Future flush() => _httpClientRequest.flush(); 110 | 111 | @override 112 | HttpHeaders get headers => _httpClientRequest.headers; 113 | 114 | @override 115 | String get method => _httpClientRequest.method; 116 | 117 | @override 118 | Uri get uri => _httpClientRequest.uri; 119 | 120 | @override 121 | void write(Object? obj) { 122 | _httpClientRequest.write(obj); 123 | } 124 | 125 | @override 126 | void writeAll(Iterable objects, [String separator = ""]) { 127 | _httpClientRequest.writeAll(objects, separator); 128 | } 129 | 130 | @override 131 | void writeCharCode(int charCode) { 132 | _httpClientRequest.writeCharCode(charCode); 133 | } 134 | 135 | @override 136 | void writeln([Object? obj = ""]) { 137 | _httpClientRequest.writeln(obj); 138 | } 139 | 140 | @override 141 | void abort([Object? exception, StackTrace? stackTrace]) { 142 | _httpClientRequest.abort(exception, stackTrace); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /lib/src/http/response_wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async' show StreamView; 2 | import 'dart:io'; 3 | 4 | class HttpResponseWrapper extends StreamView> 5 | implements HttpClientResponse { 6 | final HttpClientResponse _httpClientResponse; 7 | 8 | HttpResponseWrapper(this._httpClientResponse, Stream> stream) 9 | : super(stream); 10 | 11 | @override 12 | X509Certificate? get certificate => _httpClientResponse.certificate; 13 | 14 | @override 15 | HttpConnectionInfo? get connectionInfo => _httpClientResponse.connectionInfo; 16 | 17 | @override 18 | int get contentLength => _httpClientResponse.contentLength; 19 | 20 | @override 21 | List get cookies => _httpClientResponse.cookies; 22 | 23 | @override 24 | Future detachSocket() => _httpClientResponse.detachSocket(); 25 | 26 | @override 27 | HttpHeaders get headers => _httpClientResponse.headers; 28 | 29 | @override 30 | bool get isRedirect => _httpClientResponse.isRedirect; 31 | 32 | @override 33 | bool get persistentConnection => _httpClientResponse.persistentConnection; 34 | 35 | @override 36 | String get reasonPhrase => _httpClientResponse.reasonPhrase; 37 | 38 | @override 39 | Future redirect( 40 | [String? method, Uri? url, bool? followLoops]) { 41 | return _httpClientResponse.redirect(method, url, followLoops); 42 | } 43 | 44 | @override 45 | List get redirects => _httpClientResponse.redirects; 46 | 47 | @override 48 | int get statusCode => _httpClientResponse.statusCode; 49 | 50 | @override 51 | HttpClientResponseCompressionState get compressionState => 52 | _httpClientResponse.compressionState; 53 | } 54 | -------------------------------------------------------------------------------- /lib/src/http/tracker.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show HttpHeaders; 2 | import 'dart:typed_data' show Uint8List; 3 | 4 | import 'package:appspector/src/event_sender.dart' show EventSender; 5 | import 'package:appspector/src/http/events.dart'; 6 | 7 | class HttpEventTracker { 8 | final String _url; 9 | final String _uid; 10 | final String _method; 11 | final int _startTime; 12 | Uint8List? data; 13 | 14 | HttpEventTracker.fromUri(this._method, this._uid, Uri uri) 15 | : this._url = uri.toString(), 16 | this._startTime = DateTime.now().millisecondsSinceEpoch; 17 | 18 | HttpEventTracker.fromHost( 19 | this._method, this._uid, String host, int port, String path) 20 | : this._url = 21 | Uri(scheme: "http", host: host, port: port, path: path).toString(), 22 | this._startTime = DateTime.now().millisecondsSinceEpoch; 23 | 24 | void onError(Exception e) { 25 | _sendRequestEvent({}); 26 | 27 | EventSender.sendEvent(new HttpResponseEvent( 28 | _uid, _calcDurationTime(), 0, {}, e.toString(), null)); 29 | } 30 | 31 | void addData(List bytes) { 32 | this.data = Uint8List.fromList(bytes); 33 | } 34 | 35 | void sendRequestEvent(HttpHeaders headers) => 36 | _sendRequestEvent(_headersToMap(headers)); 37 | 38 | void _sendRequestEvent(Map headers) { 39 | EventSender.sendEvent( 40 | new HttpRequestEvent(_uid, _url, _method, headers, data)); 41 | } 42 | 43 | void sendSuccessResponse( 44 | int statusCode, HttpHeaders headers, List data) { 45 | EventSender.sendEvent(new HttpResponseEvent(_uid, _calcDurationTime(), 46 | statusCode, _headersToMap(headers), null, Uint8List.fromList(data))); 47 | } 48 | 49 | int _calcDurationTime() { 50 | return DateTime.now().millisecondsSinceEpoch - _startTime; 51 | } 52 | 53 | Map _headersToMap(HttpHeaders httpHeaders) { 54 | final Map headers = {}; 55 | 56 | httpHeaders.forEach((header, values) { 57 | headers[header] = values.first; 58 | }); 59 | 60 | return headers; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/log/logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:appspector/src/event_sender.dart'; 2 | 3 | class Logger { 4 | static void v(String tag, String message, 5 | [Object? error, StackTrace? stackTrace]) { 6 | log(LogLevel.VERBOSE, tag, message, error, stackTrace); 7 | } 8 | 9 | static void d(String tag, String message, 10 | [Object? error, StackTrace? stackTrace]) { 11 | log(LogLevel.DEBUG, tag, message, error, stackTrace); 12 | } 13 | 14 | static void i(String tag, String message, 15 | [Object? error, StackTrace? stackTrace]) { 16 | log(LogLevel.INFO, tag, message, error, stackTrace); 17 | } 18 | 19 | static void w(String tag, String message, 20 | [Object? error, StackTrace? stackTrace]) { 21 | log(LogLevel.WARN, tag, message, error, stackTrace); 22 | } 23 | 24 | static void e(String tag, String message, 25 | [Object? error, StackTrace? stackTrace]) { 26 | log(LogLevel.ERROR, tag, message, error, stackTrace); 27 | } 28 | 29 | static void wtf(String tag, String message, 30 | [Object? error, StackTrace? stackTrace]) { 31 | log(LogLevel.ASSERT, tag, message, error, stackTrace); 32 | } 33 | 34 | static void log(LogLevel level, String tag, String message, 35 | [Object? error, StackTrace? stackTrace]) { 36 | if (error != null) { 37 | message = "$message\n$error"; 38 | } 39 | if (stackTrace != null) { 40 | message = "$message\n$stackTrace"; 41 | } 42 | EventSender.sendEvent(new _LogEvent(level, tag, message)); 43 | } 44 | } 45 | 46 | class _LogEvent extends Event { 47 | final LogLevel _level; 48 | final String _tag; 49 | final String _message; 50 | 51 | _LogEvent(this._level, this._tag, this._message); 52 | 53 | @override 54 | Map get arguments => 55 | {"level": _level.value, "tag": _tag, "message": _message}; 56 | 57 | @override 58 | String get name => "log-event"; 59 | } 60 | 61 | class LogLevel { 62 | final int value; 63 | 64 | const LogLevel._internal(this.value); 65 | 66 | toString() => 'LogLevel.$value'; 67 | 68 | static const VERBOSE = const LogLevel._internal(2); 69 | static const DEBUG = const LogLevel._internal(3); 70 | static const INFO = const LogLevel._internal(4); 71 | static const WARN = const LogLevel._internal(5); 72 | static const ERROR = const LogLevel._internal(6); 73 | static const ASSERT = const LogLevel._internal(6); 74 | } 75 | -------------------------------------------------------------------------------- /lib/src/monitors.dart: -------------------------------------------------------------------------------- 1 | enum SupportedPlatform { android, ios } 2 | 3 | /// Class which contains description of monitor 4 | class Monitor { 5 | /// Monitor identifier 6 | final String id; 7 | 8 | /// List of platforms which support this monitor 9 | final List platforms; 10 | 11 | const Monitor._androidMonitor(this.id) 12 | : platforms = const [SupportedPlatform.android]; 13 | 14 | const Monitor._iosMonitor(this.id) 15 | : platforms = const [SupportedPlatform.ios]; 16 | 17 | const Monitor._commonMonitor(this.id) 18 | : platforms = const [SupportedPlatform.android, SupportedPlatform.ios]; 19 | } 20 | 21 | /// Identifiers for supported monitors 22 | class Monitors { 23 | Monitors._(); 24 | 25 | static const http = const Monitor._commonMonitor("http"); 26 | static const logs = const Monitor._commonMonitor("logs"); 27 | static const fileSystem = const Monitor._commonMonitor("file-system"); 28 | static const screenshot = const Monitor._commonMonitor("screenshot"); 29 | static const environment = const Monitor._commonMonitor("environment"); 30 | static const location = const Monitor._commonMonitor("location"); 31 | static const performance = const Monitor._commonMonitor("performance"); 32 | static const sqLite = const Monitor._commonMonitor("sqlite"); 33 | static const sharedPreferences = 34 | const Monitor._androidMonitor("shared-preferences"); 35 | static const analytics = const Monitor._iosMonitor("analytics"); 36 | static const notification = const Monitor._iosMonitor("notification-center"); 37 | static const userDefaults = const Monitor._iosMonitor("user-defaults"); 38 | static const coreData = const Monitor._iosMonitor("ios-core-data"); 39 | 40 | static List all() => [ 41 | http, 42 | logs, 43 | screenshot, 44 | environment, 45 | location, 46 | performance, 47 | sqLite, 48 | fileSystem, 49 | analytics, 50 | notification, 51 | userDefaults, 52 | coreData, 53 | sharedPreferences 54 | ]; 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/request_receiver.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | 3 | import 'package:flutter/rendering.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | typedef Future RequestHandler(dynamic args); 8 | 9 | class RequestReceiver { 10 | static const MethodChannel _channel = 11 | const MethodChannel('appspector_request_channel'); 12 | final Map handlers = Map(); 13 | 14 | RequestReceiver() { 15 | handlers["take_screenshot"] = _takeScreenshot; 16 | } 17 | 18 | void observeChannel() { 19 | _channel.setMethodCallHandler(_handler); 20 | } 21 | 22 | Future _handler(MethodCall call) async { 23 | final handler = handlers[call.method]; 24 | if (handler != null) { 25 | return handler(call.arguments); 26 | } 27 | //todo 28 | } 29 | } 30 | 31 | Future _takeScreenshot(dynamic args) async { 32 | int maxWidth = args["max_width"]; 33 | var renderViewElement = 34 | WidgetsFlutterBinding.ensureInitialized().rootElement; 35 | var renderObject = renderViewElement?.findRenderObject(); 36 | if (renderObject == null) { 37 | return null; 38 | } 39 | var ratio = maxWidth / renderObject.paintBounds.width; 40 | 41 | // ignore: invalid_use_of_protected_member 42 | var image = await (renderObject.layer as OffsetLayer) 43 | .toImage(renderObject.paintBounds, pixelRatio: ratio > 1.0 ? 1.0 : ratio); 44 | 45 | var byteData = await image.toByteData(format: ui.ImageByteFormat.png); 46 | return byteData != null ? byteData.buffer.asUint8List() : null; 47 | } 48 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | flutter upgrade 4 | 5 | cd example 6 | flutter clean 7 | cd .. 8 | 9 | flutter pub publish && echo "Project was successfully published" || echo "Project wasn't published" 10 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: appspector 2 | description: Flutter Plugin that integrate AppSpector to your mobile project. It provides remote access to application data to simplify developing process. 3 | version: 0.10.0 4 | homepage: https://github.com/appspector/flutter-plugin 5 | 6 | environment: 7 | sdk: ">=2.17.0 <4.0.0" 8 | flutter: ">=3.0.0" 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | flutter: 15 | plugin: 16 | platforms: 17 | android: 18 | package: com.appspector.flutter 19 | pluginClass: AppSpectorPlugin 20 | ios: 21 | pluginClass: AppSpectorPlugin 22 | -------------------------------------------------------------------------------- /static/appspector_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspector/flutter-plugin/dbbd326c5740ff27f670c3624bf03a912f261cbf/static/appspector_demo.gif -------------------------------------------------------------------------------- /test_ios.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | cd ./example/ios 6 | 7 | flutter pub get 8 | pod install 9 | 10 | # Build and test 11 | xcodebuild -workspace Runner.xcworkspace -scheme Runner -configuration Debug -sdk iphonesimulator -derivedDataPath /tmp/dd -destination "platform=iOS Simulator,name=iPhone 13 Pro Max,OS=16.2" build test 12 | --------------------------------------------------------------------------------