sensorswave_flutter_sdk 0.7.0
sensorswave_flutter_sdk: ^0.7.0 copied to clipboard
Sensors Wave Flutter SDK for event tracking and AB testing.
Sensors Wave Flutter SDK #
Sensors Wave Sensors Wave Flutter SDK is a mobile analytics tracking and AB testing library for Flutter applications. If you're new to Sensor Wave, check out our product and create an account at sensorswave.com.
SDK Usage #
1. Install via pub.flutter-io.cn #
Add the following to your pubspec.yaml:
dependencies:
sensorswave_flutter_sdk: ^0.7.0
Then run:
flutter pub get
2. Initialize the SDK #
import 'package:sensorswave_flutter_sdk/sensorswave_flutter_sdk.dart';
// In your main.dart or app initialization
final sdk = SensorsWaveSDK();
sdk.init('your-source-token', config: SensorsConfig.create(
debug: false,
apiHost: 'https://your-api-host.com',
autoCapture: true,
));
// SDK init is synchronous - you can use it immediately
sdk.trackEvent('APP_READY');
3. Integrate Route Observer #
To enable automatic page tracking, add the route observer to your MaterialApp:
MaterialApp(
navigatorObservers: [
if (SensorsWaveSDK().routeObserver != null)
SensorsWaveSDK().routeObserver!,
],
// ... other properties
);
4. Send Custom Events #
SensorsWaveSDK().trackEvent('ButtonClick', {
'button_name': 'submit',
'page': 'home',
});
Configuration Options #
| Option | Type | Default | Description |
|---|---|---|---|
| debug | bool | false | Whether to enable debug mode for logging |
| apiHost | String | '' | API host address for sending events |
| autoCapture | bool | true | Whether to automatically capture app events (app start, app end, page view, page leave) |
| enableClickTrack | bool | false | Whether to enable automatic click tracking on supported widgets |
| enableErrorTrack | bool | false | Whether to enable error-level exception auto capture (FlutterError / PlatformDispatcher / Isolate errors; web: FlutterError only). Independent of autoCapture. |
| enableCrashTrack | bool | false | Whether to enable fatal native crash capture (Android UncaughtExceptionHandler / iOS NSException + fatal signals, including SIGTRAP for Swift traps such as fatalError / force unwraps / array out-of-bounds). Crashes are persisted on disk and reported as $Exception on next launch. App (iOS/Android) only; independent of autoCapture. |
| enableAB | bool | false | Whether to enable A/B testing feature |
| abRefreshInterval | int | 600000 (10 minutes) | The interval in milliseconds for refreshing A/B test configuration (minimum 30000ms) |
| batchSend | bool | false | Whether to use batch sending mode. When true, events are sent in batches (up to 10 events every 5 seconds). When false, events are sent immediately one by one. |
| optOutCapturing | bool | false | Compliance: whether to disable data collection immediately on init. Set to true so no data is collected before the user grants authorization. |
| persistOptOut | bool | false | Compliance: whether to persist the opt-out/opt-in state to local storage. When true, the state survives app restarts; when false (default), it is in-memory only and resets to the optOutCapturing default on restart. |
Exception Tracking #
The SDK reports errors and crashes as $Exception events:
| Property | Description |
|---|---|
$exception_level |
fatal (uncaptured native crash that terminated the process) or error (caught / non-fatal errors) |
$exception_type |
Fully-qualified exception class name, e.g. java.lang.NullPointerException, StateError, TypeError |
$exception_message |
Message only, without the stack trace |
$exception_frames |
Structured stack frames (array, max 30; outermost/entry frame first, innermost/crash point last). Each frame: platform (dart / web:javascript / java / cocoa), filename (cleaned path), function ("?" if anonymous), numeric lineno / colno (colno only on dart/web), abs_path (pre-cleaned URL, web only, ≤ 1000 chars), module (Java fully-qualified class name / iOS binary image name), instruction_addr / image_addr / debug_id (iOS symbolication: runtime instruction address / owning image base / image dSYM UUID, only on iOS native crash frames, for server-side offline symbolication); absent fields are omitted |
Capture matrix:
| Source | Switch | Level |
|---|---|---|
Native uncaught crash (Android UncaughtExceptionHandler / iOS NSException + fatal signals), persisted and reported on next launch |
enableCrashTrack (app only) |
fatal |
Uncaught NSException termination on iOS (handler persists the record, then the runtime abort()s) |
enableCrashTrack (app only) |
fatal — deduplicated: the follow-up SIGABRT is recognized as the same crash and does not produce a second record |
FlutterError / PlatformDispatcher / Isolate errors (web: FlutterError only) |
enableErrorTrack |
error |
Manual trackException(throwable) |
none (always available) | error |
Notes:
- Both switches are independent of
autoCaptureand default tofalse. - Compliance: when opted out, native crash persistence is stopped in real time (
optOutCapturing()/optInCapturing()sync the native capture flag — crashes during the opted-out window are never collected or reported), pending fatal crashes on disk are cleared without being reported, and all other exception events are suppressed. - Known boundary: crashes occurring before the SDK initializes natively (extremely early crashes) and iOS signal-handler stack capture are best-effort.
- Known boundary: the iOS fatal-signal path installs its own signal handlers and does not chain to signal handlers installed earlier by other crash SDKs (order of installation decides which one captures; the
NSExceptionpath does chain).
API Methods #
Event Tracking #
trackEvent
Manually track a custom event with properties.
Parameters:
eventName(String, required): The name of the event to trackproperties(Map<String, dynamic>, optional): Additional properties to include with the event
Example:
SensorsWaveSDK().trackEvent('ButtonClick', {
'button_name': 'submit',
'page': 'home',
'category': 'user_action',
});
trackException
Manually report a caught error as an error-level $Exception event. Available after init() regardless of enableErrorTrack/enableCrashTrack; gated by optOutCapturing only.
Parameters:
throwable(Object, required): The error to report (Error/Exception/Stringare all accepted)stackTrace(StackTrace, optional): Stack trace; when omitted,$exception_framesis an empty arrayproperties(Map<String, dynamic>, optional): Custom properties merged into the event (cannot override the$exception_*preset properties)
The reported event contains:
| Property | Description |
|---|---|
$exception_level |
Always error for manual reports (fatal is reserved for native crashes captured via enableCrashTrack) |
$exception_type |
Fully-qualified exception class name, e.g. StateError, TypeError, java.lang.NullPointerException |
$exception_message |
Message only, without the stack trace |
$exception_frames |
Structured stack frames (array, max 30; outermost/entry frame first, innermost/crash point last). Each frame: platform (dart / web:javascript / java / cocoa), filename (cleaned path), function ("?" if anonymous), numeric lineno / colno (colno only on dart/web), abs_path (pre-cleaned URL, web only, ≤ 1000 chars), module (Java fully-qualified class name / iOS binary image name), instruction_addr / image_addr / debug_id (iOS symbolication: runtime instruction address / owning image base / image dSYM UUID, only on iOS native crash frames, for server-side offline symbolication); absent fields are omitted |
Example:
try {
throw FormatException('invalid input');
} on Object catch (e, stack) {
SensorsWaveSDK().trackException(e, stackTrace: stack, properties: {
'source': 'checkout',
});
}
track
Track an event with full control over the event structure. This is an advanced method that allows you to specify all event fields manually.
Parameters:
event(SensorsSendEvent, required): Complete event object
Example:
final event = SensorsSendEvent(
event: 'PurchaseCompleted',
properties: {
'product_id': '12345',
'amount': 99.99,
'currency': 'USD',
},
time: DateTime.now().millisecondsSinceEpoch,
traceId: UUID(),
anonId: SensorsWaveSDK().getAnonId(),
loginId: SensorsWaveSDK().getLoginId(),
userProperties: {
'plan': 'premium',
'signup_date': '2024-01-01',
},
);
SensorsWaveSDK().track(event);
User Profile #
profileSet
Set user properties. If a property already exists, it will be overwritten.
Parameters:
properties(Map<String, dynamic>, required): User properties to set
Example:
await SensorsWaveSDK().profileSet({
'name': 'John Doe',
'age': 30,
'plan': 'premium',
});
profileSetOnce
Set user properties only if they don't already exist. Existing properties will not be overwritten.
Parameters:
properties(Map<String, dynamic>, required): User properties to set once
Example:
await SensorsWaveSDK().profileSetOnce({
'signup_date': '2024-01-15',
'initial_referrer': 'google',
'initial_campaign': 'spring_sale',
});
profileIncrement
Increment numeric user properties by a specified amount. Only supports numeric properties.
Parameters:
properties(Map<String, num>, required): Properties to increment with numeric values
Example:
// Increment single property
await SensorsWaveSDK().profileIncrement({
'login_count': 1,
});
// Increment multiple properties
await SensorsWaveSDK().profileIncrement({
'login_count': 1,
'points_earned': 100,
'purchases_count': 1,
});
profileAppend
Append new values to list-type user properties without deduplication.
Parameters:
properties(Map<String, dynamic>, required): Properties with list values to append
Example:
await SensorsWaveSDK().profileAppend({
'categories_viewed': ['electronics', 'mobile_phones'],
'tags': ['new_customer', 'q1_2024'],
});
profileUnion
Append new values to list-type user properties with deduplication (avoids duplicate values).
Parameters:
properties(Map<String, List>, required): Properties with list values to append with deduplication
Example:
await SensorsWaveSDK().profileUnion({
'interests': ['technology', 'gaming'],
'newsletter_subscriptions': ['tech_news'],
});
profileUnset
Set specific user properties to null (effectively removing them).
Parameters:
keys(String or List
Example:
// Unset single property
await SensorsWaveSDK().profileUnset('temporary_campaign');
// Unset multiple properties
await SensorsWaveSDK().profileUnset(['old_plan', 'expired_flag', 'temp_id']);
profileDelete
Delete all user profile data for the current user. This operation cannot be undone.
Example:
await SensorsWaveSDK().profileDelete();
User Identification #
identify
Set the login ID for the current user and send a $Identify binding event to the backend.
Parameters:
loginId(String or num, required): Unique identifier for the user (e.g., email, user ID, username)
Example:
SensorsWaveSDK().identify('user_12345');
setLoginId
Set the login ID for the current user without sending a binding event.
Difference from identify:
identify: Sets login ID AND sends a$Identifyevent to backendsetLoginId: Only sets login ID locally, no event sent
Parameters:
loginId(String or num, required): Unique identifier for the user
Example:
// Set login ID without triggering event
SensorsWaveSDK().setLoginId('user_12345');
// Also supports numeric types (auto-converted to String)
SensorsWaveSDK().setLoginId(12345);
getLoginId
Get the current login ID.
Returns: String?
Example:
final loginId = SensorsWaveSDK().getLoginId();
getAnonId
Get the anonymous ID for the current user.
Returns: String?
Example:
final anonId = SensorsWaveSDK().getAnonId();
reset
Call when the user logs out to unbind the login ID from the device.
Parameters:
resetAnonId(bool, optional, defaultfalse): Whether to also reset the anonymous ID
Behavior:
reset()— Logout only: clears the login ID and keeps the anonymous ID (default behavior). Events after logout are still attributed to the same anonymous ID.reset(true)— Logout and reset: also generates a new anonymous ID. Use this for shared/public devices where identities should not carry over.
Logout is a data-clearing operation, so it remains available even while capturing is disabled via
optOutCapturing().
Example:
// When the user logs out
SensorsWaveSDK().reset(); // Keeps the anonymous ID (default)
// Also reset the anonymous ID (e.g., shared/public devices)
SensorsWaveSDK().reset(true);
Common Properties #
registerCommonProperties
Register static common properties that will be included with all events. This is useful for including global context like app version, environment, or user-specific data.
Parameters:
properties(Map<String, dynamic>, required): Properties to register
Example:
SensorsWaveSDK().registerCommonProperties({
'app_version': '1.0.0',
'environment': 'production',
'user_tier': getUserTier(),
});
clearCommonProperties
Remove specific registered common properties.
Parameters:
keys(List
Example:
SensorsWaveSDK().clearCommonProperties(['app_version', 'user_session_id']);
A/B Testing #
checkFeatureGate
Check if a feature gate (feature flag) is enabled for the current user.
Parameters:
key(String, required): The feature gate key to check
Returns: Future
Example:
// Check if a feature is enabled
final isEnabled = await SensorsWaveSDK().checkFeatureGate('new_checkout_flow');
if (isEnabled) {
// Show new feature
showNewCheckout();
} else {
// Show old feature
showOldCheckout();
}
getExperiment
Get experiment variant data for the current user.
Parameters:
key(String, required): The experiment key to retrieve
Returns: Future<Map<String, dynamic>>
Example:
// Get experiment configuration
final experiment = await SensorsWaveSDK().getExperiment('homepage_layout');
if (experiment.isNotEmpty) {
// Apply experiment configuration
applyLayout(experiment['layout_type']);
}
getFeatureConfig
Get feature configuration data for the current user.
Parameters:
key(String, required): The feature config key to retrieve
Returns: Future<Map<String, dynamic>>
Example:
// Get feature configuration
final config = await SensorsWaveSDK().getFeatureConfig('theme_config');
if (config.isNotEmpty) {
// Apply feature configuration
applyTheme(config['theme']);
}
Compliance / Data Capture Control #
For privacy compliance scenarios, you can disable all data collection until the user grants authorization, then re-enable it. This is controlled by optOutCapturing / persistOptOut at init time and the runtime APIs below.
When opt-out is active, the SDK follows a no-write, no-read, no-send policy: business events, auto-captured events, user profiles, and $Identify are all blocked; getAnonId() / getLoginId() return null. UTM parameters are still captured to memory (so attribution works after opt-in), and common properties can still be registered/cleared (local-only, never sent).
optOutCapturing
Disable data collection. Also stops the batch flush timer.
Example:
SensorsWaveSDK().optOutCapturing();
optInCapturing
Re-enable data collection. Drains the backlog of events that were queued (but failed to send) before opt-out, and restarts the flush timer. Events blocked during the opt-out period are not resent — they were never queued.
Example:
SensorsWaveSDK().optInCapturing();
hasOptedOutCapturing
Returns whether data collection is currently disabled.
Example:
final optedOut = SensorsWaveSDK().hasOptedOutCapturing();
Typical compliance flow
// 1. Initialize with collection disabled until the user consents
sdk.init(
'your-source-token',
config: SensorsConfig.create(
apiHost: 'https://your-api-host.com',
optOutCapturing: true, // collect nothing before authorization
persistOptOut: true, // remember the choice across restarts
),
);
// 2. After the user grants authorization, resume collection
SensorsWaveSDK().optInCapturing();
// 3. If the user revokes authorization, stop collecting
SensorsWaveSDK().optOutCapturing();
Other Methods #
destroy
Clean up and release all SDK resources. Call this when the app is terminating.
Example:
await SensorsWaveSDK().destroy();
Best Practices #
1. Initialization Timing #
Initialize the SDK as early as possible in your app's lifecycle:
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Initialize SDK
final sdk = SensorsWaveSDK();
sdk.init('your-source-token');
runApp(MyApp());
}
2. User Identity Management #
Use identify() when a user logs in to track their identity across sessions:
// When user logs in
sdk.identify('user_12345');
// When you need to update login ID without sending event
sdk.setLoginId('new.user_12345');
// When user logs out (keeps anonymous ID; pass true to also reset it)
sdk.reset();
3. Event Naming Convention #
- Use descriptive event names:
ButtonClick,PurchaseCompleted - Avoid using
$prefix for custom events (it's reserved for preset events)
4. Property Design #
- Use snake case for property keys:
user_id,product_name,is_premium - Include context in properties:
{ 'button_name': 'submit', 'page': 'checkout' } - Use consistent data types for the same property across events
5. Common Properties #
Register app-level properties once instead of including them in every event:
// Set once at app startup
sdk.registerCommonProperties({
'app_version': '1.0.0',
'user_tier': getUserTier(),
'environment': 'production',
});
// These will be automatically included in all events
sdk.trackEvent('any_event'); // Properties above are included
6. Error Handling #
The SDK handles errors internally and logs them when debug mode is enabled:
sdk.init('your-source-token', config: SensorsConfig.create(
debug: true, // Enable debug logs during development
));
7. Memory Management #
The SDK manages resources automatically. Call destroy() only when:
- User logs out and you want to clear all data
- App is shutting down permanently
- You need to reinitialize the SDK with different configuration
await sdk.destroy();
FAQ #
Q: What's the difference between identify() and setLoginId()? #
A:
identify(): Sets the login ID AND sends a$Identifybinding event to the backend. Use this when a user logs in.setLoginId(): Only sets the login ID locally without sending an event. Use this when you need to update the ID but don't want to trigger the binding event.
Q: Is init() synchronous or asynchronous? #
A: The init() method is synchronous and returns immediately. Actual initialization (loading data from storage, collecting device info, initializing plugins) happens in the background. You can call other SDK methods right after init() returns.
Q: When should I disable autoCapture? #
A: Disable autoCapture if:
- You want full control over what events are tracked
- You only want to track specific custom events
- You need to implement custom page view tracking logic
- You want to minimize performance impact
Q: What's the difference between batchSend: true and batchSend: false? #
A:
batchSend: true: Events are queued and sent in batches (up to 10 events every 5 seconds). Better for performance and network efficiency.batchSend: false(default): Each event is sent immediately. Better for real-time analytics but uses more network requests.
Q: How do I track events that need to be sent immediately? #
A: The SDK automatically handles critical events (like $AppEnd) with immediate sending. For custom events, use batchSend: false or call flush() after tracking.
Q: Can I use the SDK in the background? #
A: Yes, but note that some features like automatic page tracking won't work in the background since there are no route changes. Event tracking and user profile methods work normally.
Q: How do I test the SDK during development? #
A:
- Enable debug mode:
sdk.init('your-token', config: SensorsConfig.create(debug: true)) - Check console logs for event tracking details
- Verify events are sent to your analytics dashboard
- Use a test source token separate from production
Q: What happens if I call init() multiple times? #
A: The SDK ignores subsequent init() calls after the first initialization. The first call's configuration is used throughout the SDK lifecycle.
Q: How do I clear user data? #
A: Use the appropriate method based on your needs:
- Clear user properties:
sdk.profileDelete() - User logout (unbind login ID, keep anonymous ID):
sdk.reset() - User logout with anonymous ID reset (shared devices):
sdk.reset(true) - Clear login ID only:
sdk.setLoginId(null)orsdk.setLoginId('') - Clear all data:
await sdk.destroy()then reinitialize
Q: Does the SDK work offline? #
A: Yes! Events are queued in local storage using SharedPreferences. When the device comes back online, queued events are automatically sent.
Supported Event Types #
The SDK automatically captures the following event types when autoCapture is enabled:
- $AppInstall: Triggered on first app launch
- $AppStart: Triggered when the app enters the foreground
- $AppEnd: Triggered when the app enters the background
- $AppPageView: Triggered when a user navigates to a new page/route
- $AppPageLeave: Triggered when a user leaves a page/route
- $AppClick: Triggered on element interactions (only when
enableClickTrackis true)
AppClick - Supported Elements #
When enableClickTrack is enabled, the SDK automatically tracks clicks on the following Flutter widget types:
| Widget Type | Description |
|---|---|
| ElevatedButton | Material Design elevated button |
| TextButton | Material Design text button (flat button) |
| OutlinedButton | Material Design outlined button |
| FilledButton | Material Design filled button |
| IconButton | Icon button with an icon |
| ListTile | Single row in a list with optional leading/trailing widgets |
| GestureDetector | Custom gesture detector widget |
| InkWell | Material ink splash widget (also covers widgets that use InkWell internally) |
How It Works
- Enable click tracking in the SDK config:
sdk.init('your-source-token', config: SensorsConfig.create(
enableClickTrack: true,
));
- Use any of the supported widgets normally - no wrapper components needed:
// These will be tracked automatically
ElevatedButton(
onPressed: () => doSomething(),
child: Text('Click Me'),
)
TextButton(
onPressed: () => doSomething(),
child: Text('Text Button'),
)
IconButton(
icon: Icon(Icons.favorite),
onPressed: () => doSomething(),
)
ListTile(
title: Text('Title'),
onTap: () => doSomething(),
)
Event Properties
Each $AppClick event includes the following properties:
| Property | Description |
|---|---|
$element_type |
Type of the clicked element (e.g., "ElevatedButton", "TextButton") |
$element_content |
Text content of the clicked element (if available) |
$screen_name |
Current screen/page name when the click occurred |
$event_duration |
Time in seconds since the page was viewed |
$element_selector |
CSS-style selector for the element (e.g., "[ElevatedButton]") |
$element_path |
Full path to the element (e.g., "/HomePage/ElevatedButton") |
Manual Click Tracking
Custom events can be tracked using the trackEvent() or track() methods.
Complete Example #
Here's a complete example showing how to integrate the SDK into a Flutter app:
import 'package:flutter/material.dart';
import 'package:sensorswave_flutter_sdk/sensorswave_flutter_sdk.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Initialize SDK (synchronous, returns immediately)
final sdk = SensorsWaveSDK();
sdk.init('your-source-token', config: SensorsConfig.create(
debug: false,
autoCapture: true,
enableAB: true,
enableClickTrack: true,
batchSend: false,
));
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final sdk = SensorsWaveSDK();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
// Add route observer for automatic page tracking
navigatorObservers: sdk.routeObserver != null ? [sdk.routeObserver!] : [],
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
final sdk = SensorsWaveSDK();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Column(
children: [
ElevatedButton(
onPressed: () {
// Track custom event
sdk.trackEvent('ButtonClick', {
'button_name': 'get_started',
'page': 'home',
});
},
child: Text('Get Started'),
),
ElevatedButton(
onPressed: () async {
// Check feature gate
final showNewFeature = await sdk.checkFeatureGate('new_dashboard');
if (showNewFeature) {
Navigator.push(context, MaterialPageRoute(
builder: (_) => NewDashboardPage(),
));
} else {
Navigator.push(context, MaterialPageRoute(
builder: (_) => OldDashboardPage(),
));
}
},
child: Text('Go to Dashboard'),
),
],
),
);
}
}
class LoginPage extends StatefulWidget {
@override
_LoginPageState createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final sdk = SensorsWaveSDK();
void handleLogin(String email, String password) {
// Your login logic here
// ...
// After successful login, identify the user
sdk.identify(email);
}
@override
Widget build(BuildContext context) {
// Your login UI here
return Container();
}
}
class ProductPage extends StatelessWidget {
final sdk = SensorsWaveSDK();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Product')),
body: ListView(
children: [
ElevatedButton(
onPressed: () {
// Increment purchase count
sdk.profileIncrement({'purchase_count': 1});
// Track purchase event
sdk.trackEvent('product_purchase', {
'product_id': '12345',
'product_name': 'Premium Plan',
'price': 99.99,
});
},
child: Text('Buy Now'),
),
],
),
);
}
}
class SettingsPage extends StatefulWidget {
@override
_SettingsPageState createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
final sdk = SensorsWaveSDK();
void handleLogout() {
// Track logout event
sdk.trackEvent('UserLogout');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Settings')),
body: ListView(
children: [
ListTile(
title: Text('Logout'),
onTap: handleLogout,
),
],
),
);
}
}
License #
Apache-2.0