b2metric_sdk 0.2.0
b2metric_sdk: ^0.2.0 copied to clipboard
B2Metric analytics SDK for Flutter. Collects events, manages sessions, and supports push notifications on Android and iOS.
example/lib/main.dart
import 'dart:convert';
import 'package:b2metric_sdk/b2metric_sdk.dart';
import 'package:flutter/material.dart';
void main() => runApp(const TestbedApp());
// ─── Types ────────────────────────────────────────────────────────────────────
enum TerminalLevel { debug, info, warn, error, app }
class LogEntry {
final String time;
final TerminalLevel level;
final String message;
LogEntry(this.time, this.level, this.message);
}
class Preset {
final String label;
final String eventName;
final String? properties;
final String? itemProperties;
const Preset({
required this.label,
required this.eventName,
this.properties,
this.itemProperties,
});
}
// ─── Constants ────────────────────────────────────────────────────────────────
const _sdkLogLevels = LogLevel.values;
String _pretty(Object obj) =>
const JsonEncoder.withIndent(' ').convert(obj);
final _presets = <Preset>[
Preset(
label: 'page_view',
eventName: 'page_view',
properties: _pretty({'screen': 'home', 'referrer': 'push'}),
),
Preset(
label: 'button_click',
eventName: 'button_click',
properties: _pretty({'button': 'checkout', 'section': 'cart'}),
),
Preset(
label: 'add_to_cart',
eventName: 'add_to_cart',
properties: _pretty({'total': 5700, 'currency': 'TRY'}),
itemProperties: _pretty([
{'id': 'SKU-001', 'name': 'Koşu Ayakkabısı', 'price': 2850, 'quantity': 1},
{'id': 'SKU-002', 'name': 'Spor Çorap', 'price': 150, 'quantity': 3},
]),
),
Preset(
label: 'purchase',
eventName: 'purchase',
properties: _pretty({
'order_id': 'ORD-4521',
'total': 5700,
'currency': 'TRY',
'payment': 'credit_card',
}),
itemProperties: _pretty([
{'id': 'SKU-001', 'name': 'Koşu Ayakkabısı', 'price': 2850, 'quantity': 1},
]),
),
Preset(
label: 'search',
eventName: 'search',
properties: _pretty({'query': 'koşu ayakkabısı', 'results': 42}),
),
Preset(
label: 'login',
eventName: 'login',
properties: _pretty({'method': 'email', 'success': true}),
),
];
const _levelColors = {
TerminalLevel.debug: Color(0xFF6B7280),
TerminalLevel.info: Color(0xFF38BDF8),
TerminalLevel.warn: Color(0xFFFBBF24),
TerminalLevel.error: Color(0xFFF87171),
TerminalLevel.app: Color(0xFF4ADE80),
};
// ─── App root ─────────────────────────────────────────────────────────────────
class TestbedApp extends StatelessWidget {
const TestbedApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'B2Metric Testbed',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: const Color(0xFF0F172A),
fontFamily: 'monospace',
),
home: const TestbedScreen(),
);
}
}
// ─── Screen ───────────────────────────────────────────────────────────────────
class TestbedScreen extends StatefulWidget {
const TestbedScreen({super.key});
@override
State<TestbedScreen> createState() => _TestbedScreenState();
}
class _TestbedScreenState extends State<TestbedScreen> {
final _apiKey = TextEditingController();
final _appIdentifier = TextEditingController();
final _batchSize = TextEditingController(text: '20');
final _flushInterval = TextEditingController(text: '30');
final _sessionTimeout = TextEditingController(text: '30');
LogLevel _logLevel = LogLevel.debug;
bool _sdkReady = false;
final _eventName = TextEditingController();
final _propsJson = TextEditingController();
final _itemPropsJson = TextEditingController();
final List<LogEntry> _logs = [];
final ScrollController _terminalScroll = ScrollController();
@override
void dispose() {
_apiKey.dispose();
_appIdentifier.dispose();
_batchSize.dispose();
_flushInterval.dispose();
_sessionTimeout.dispose();
_eventName.dispose();
_propsJson.dispose();
_itemPropsJson.dispose();
_terminalScroll.dispose();
super.dispose();
}
void _addLog(TerminalLevel level, String message) {
final ts = DateTime.now().toUtc().toIso8601String().substring(11, 23);
setState(() {
_logs.insert(0, LogEntry(ts, level, message));
});
if (_terminalScroll.hasClients) {
_terminalScroll.jumpTo(0);
}
}
TerminalLevel _mapLevel(LogLevel l) => switch (l) {
LogLevel.debug => TerminalLevel.debug,
LogLevel.info => TerminalLevel.info,
LogLevel.warning => TerminalLevel.warn,
LogLevel.error => TerminalLevel.error,
LogLevel.off => TerminalLevel.debug,
};
// ─── Handlers ──────────────────────────────────────────────────────────────
Future<void> _handleInit() async {
final key = _apiKey.text.trim();
if (key.isEmpty) {
_addLog(TerminalLevel.warn, 'API key is required');
return;
}
final appId = _appIdentifier.text.trim();
if (appId.isEmpty) {
_addLog(TerminalLevel.warn, 'App identifier is required');
return;
}
try {
await B2Metric.instance.init(B2MetricConfig(
apiKey: key,
appIdentifier: appId,
batchSize: int.tryParse(_batchSize.text) ?? 20,
flushIntervalSeconds: int.tryParse(_flushInterval.text) ?? 30,
sessionTimeoutMinutes: int.tryParse(_sessionTimeout.text) ?? 30,
logLevel: _logLevel,
onLog: (level, message) {
final clean = message.replaceFirst(RegExp(r'\[B2Metric\]\s?'), '');
_addLog(_mapLevel(level), clean);
},
));
setState(() => _sdkReady = true);
_addLog(TerminalLevel.app, 'SDK initialized successfully');
} catch (e) {
_addLog(TerminalLevel.error, 'Init failed: $e');
}
}
Future<void> _handleDestroy() async {
try {
await B2Metric.instance.destroy();
setState(() => _sdkReady = false);
_addLog(TerminalLevel.app, 'SDK destroyed');
} catch (e) {
_addLog(TerminalLevel.error, 'Destroy failed: $e');
}
}
Future<void> _handleFlush() async {
try {
await B2Metric.instance.flush();
_addLog(TerminalLevel.app, 'Manual flush triggered');
} catch (e) {
_addLog(TerminalLevel.error, 'Flush failed: $e');
}
}
void _fillPreset(Preset p) {
_eventName.text = p.eventName;
_propsJson.text = p.properties ?? '';
_itemPropsJson.text = p.itemProperties ?? '';
}
void _handleSendEvent() {
final name = _eventName.text.trim();
if (name.isEmpty) {
_addLog(TerminalLevel.warn, 'Event name is required');
return;
}
try {
Map<String, dynamic>? properties;
List<Map<String, dynamic>>? itemProperties;
if (_propsJson.text.trim().isNotEmpty) {
properties =
(jsonDecode(_propsJson.text) as Map).cast<String, dynamic>();
}
if (_itemPropsJson.text.trim().isNotEmpty) {
itemProperties = (jsonDecode(_itemPropsJson.text) as List)
.map((e) => (e as Map).cast<String, dynamic>())
.toList();
}
B2Metric.instance.logEvent(
name,
properties: properties,
itemProperties: itemProperties,
);
_addLog(TerminalLevel.app, 'logEvent("$name")');
} catch (e) {
_addLog(TerminalLevel.error, 'Send failed: $e');
}
}
// ─── Render ────────────────────────────────────────────────────────────────
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0F172A),
body: SafeArea(
child: Column(
children: [
Expanded(
child: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(top: 8, bottom: 8),
child: Text(
'B2Metric Testbed',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Color(0xFFF1F5F9),
letterSpacing: 0.3,
),
),
),
_StatusBadge(ready: _sdkReady),
const SizedBox(height: 16),
_ConfigSection(
apiKey: _apiKey,
appIdentifier: _appIdentifier,
batchSize: _batchSize,
flushInterval: _flushInterval,
sessionTimeout: _sessionTimeout,
logLevel: _logLevel,
onLogLevel: (l) => setState(() => _logLevel = l),
sdkReady: _sdkReady,
onInit: _handleInit,
onFlush: _handleFlush,
onDestroy: _handleDestroy,
),
const SizedBox(height: 12),
_PresetsSection(onPick: _fillPreset),
const SizedBox(height: 12),
_SendEventSection(
eventName: _eventName,
propsJson: _propsJson,
itemPropsJson: _itemPropsJson,
sdkReady: _sdkReady,
onSend: _handleSendEvent,
onClear: () {
_eventName.clear();
_propsJson.clear();
_itemPropsJson.clear();
},
),
],
),
),
),
),
_Terminal(
logs: _logs,
scrollController: _terminalScroll,
onClear: () => setState(_logs.clear),
),
],
),
),
);
}
}
// ─── Sections ─────────────────────────────────────────────────────────────────
class _StatusBadge extends StatelessWidget {
final bool ready;
const _StatusBadge({required this.ready});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: ready ? const Color(0xFF14532D) : const Color(0xFF1F2937),
borderRadius: BorderRadius.circular(20),
),
child: Text(
ready ? '● SDK READY' : '○ NOT INITIALIZED',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
fontFamily: 'monospace',
color: Color(0xFFD1FAE5),
),
),
);
}
}
class _SectionCard extends StatelessWidget {
final String title;
final Widget child;
const _SectionCard({required this.title, required this.child});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: const Color(0xFF1E293B),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
title.toUpperCase(),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Color(0xFF475569),
letterSpacing: 1.5,
),
),
),
child,
],
),
);
}
}
class _FieldLabel extends StatelessWidget {
final String text;
const _FieldLabel(this.text);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 8, bottom: 4),
child: Text(
text,
style: const TextStyle(fontSize: 11, color: Color(0xFF64748B)),
),
);
}
}
class _Input extends StatelessWidget {
final TextEditingController controller;
final String? hint;
final bool multiline;
final TextInputType? keyboardType;
const _Input({
required this.controller,
this.hint,
this.multiline = false,
this.keyboardType,
});
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
keyboardType: keyboardType ??
(multiline ? TextInputType.multiline : TextInputType.text),
maxLines: multiline ? null : 1,
minLines: multiline ? 3 : 1,
autocorrect: false,
enableSuggestions: false,
style: const TextStyle(
fontSize: 13,
color: Color(0xFFE2E8F0),
fontFamily: 'monospace',
),
decoration: InputDecoration(
isDense: true,
filled: true,
fillColor: const Color(0xFF0F172A),
contentPadding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
hintText: hint,
hintStyle: const TextStyle(
color: Color(0xFF4B5563),
fontFamily: 'monospace',
fontSize: 13,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF334155)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF334155)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF3B82F6)),
),
),
);
}
}
class _ActionBtn extends StatelessWidget {
final String label;
final Color color;
final VoidCallback onPressed;
final bool disabled;
final double flex;
const _ActionBtn({
required this.label,
required this.color,
required this.onPressed,
this.disabled = false,
this.flex = 1,
});
@override
Widget build(BuildContext context) {
return Expanded(
flex: (flex * 100).round(),
child: Padding(
padding: const EdgeInsets.only(top: 10),
child: Material(
color: disabled ? const Color(0xFF374151) : color,
borderRadius: BorderRadius.circular(8),
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: disabled ? null : onPressed,
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
child: Center(
child: Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color:
disabled ? const Color(0xFF6B7280) : Colors.white,
),
),
),
),
),
),
),
);
}
}
class _ConfigSection extends StatelessWidget {
final TextEditingController apiKey;
final TextEditingController appIdentifier;
final TextEditingController batchSize;
final TextEditingController flushInterval;
final TextEditingController sessionTimeout;
final LogLevel logLevel;
final ValueChanged<LogLevel> onLogLevel;
final bool sdkReady;
final VoidCallback onInit;
final VoidCallback onFlush;
final VoidCallback onDestroy;
const _ConfigSection({
required this.apiKey,
required this.appIdentifier,
required this.batchSize,
required this.flushInterval,
required this.sessionTimeout,
required this.logLevel,
required this.onLogLevel,
required this.sdkReady,
required this.onInit,
required this.onFlush,
required this.onDestroy,
});
@override
Widget build(BuildContext context) {
return _SectionCard(
title: 'Configuration',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _FieldLabel('API Key'),
_Input(controller: apiKey, hint: 'your-api-key'),
const _FieldLabel('App Identifier'),
_Input(controller: appIdentifier, hint: 'your-app-identifier'),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _FieldLabel('Batch Size'),
_Input(
controller: batchSize,
keyboardType: TextInputType.number,
),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _FieldLabel('Flush Interval (s)'),
_Input(
controller: flushInterval,
keyboardType: TextInputType.number,
),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _FieldLabel('Session Timeout (min)'),
_Input(
controller: sessionTimeout,
keyboardType: TextInputType.number,
),
],
),
),
],
),
const _FieldLabel('Log Level'),
Wrap(
spacing: 6,
runSpacing: 6,
children: _sdkLogLevels.map((l) {
final active = l == logLevel;
return InkWell(
onTap: () => onLogLevel(l),
borderRadius: BorderRadius.circular(6),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: active
? const Color(0xFF1D4ED8)
: const Color(0xFF0F172A),
border: Border.all(
color: active
? const Color(0xFF3B82F6)
: const Color(0xFF334155),
),
borderRadius: BorderRadius.circular(6),
),
child: Text(
l.name,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
fontFamily: 'monospace',
color: active
? const Color(0xFFDBEAFE)
: const Color(0xFF64748B),
),
),
),
);
}).toList(),
),
Row(
children: sdkReady
? [
_ActionBtn(
label: 'Flush',
color: const Color(0xFF059669),
onPressed: onFlush,
),
const SizedBox(width: 8),
_ActionBtn(
label: 'Destroy',
color: const Color(0xFFDC2626),
onPressed: onDestroy,
),
]
: [
_ActionBtn(
label: 'Init SDK',
color: const Color(0xFF2563EB),
onPressed: onInit,
),
],
),
],
),
);
}
}
class _PresetsSection extends StatelessWidget {
final ValueChanged<Preset> onPick;
const _PresetsSection({required this.onPick});
@override
Widget build(BuildContext context) {
return _SectionCard(
title: 'Presets',
child: Wrap(
spacing: 6,
runSpacing: 6,
children: _presets.map((p) {
return InkWell(
onTap: () => onPick(p),
borderRadius: BorderRadius.circular(6),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
border: Border.all(color: const Color(0xFF334155)),
borderRadius: BorderRadius.circular(6),
),
child: Text(
p.label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Color(0xFF94A3B8),
fontFamily: 'monospace',
),
),
),
);
}).toList(),
),
);
}
}
class _SendEventSection extends StatelessWidget {
final TextEditingController eventName;
final TextEditingController propsJson;
final TextEditingController itemPropsJson;
final bool sdkReady;
final VoidCallback onSend;
final VoidCallback onClear;
const _SendEventSection({
required this.eventName,
required this.propsJson,
required this.itemPropsJson,
required this.sdkReady,
required this.onSend,
required this.onClear,
});
@override
Widget build(BuildContext context) {
return _SectionCard(
title: 'Send Event',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const _FieldLabel('Event Name'),
_Input(controller: eventName, hint: 'e.g. add_to_cart'),
const _FieldLabel('properties (JSON object, optional)'),
_Input(
controller: propsJson,
hint: '{ "price": 99, "currency": "USD" }',
multiline: true,
),
const _FieldLabel('itemProperties (JSON array, optional)'),
_Input(
controller: itemPropsJson,
hint: '[{ "id": "SKU-1", "name": "Item", "price": 99 }]',
multiline: true,
),
Row(
children: [
_ActionBtn(
label: 'Send Event',
color: const Color(0xFF2563EB),
onPressed: onSend,
disabled: !sdkReady,
),
const SizedBox(width: 8),
_ActionBtn(
label: 'Clear Form',
color: const Color(0xFF475569),
onPressed: onClear,
flex: 0.55,
),
],
),
],
),
);
}
}
// ─── Terminal ─────────────────────────────────────────────────────────────────
class _Terminal extends StatelessWidget {
final List<LogEntry> logs;
final ScrollController scrollController;
final VoidCallback onClear;
const _Terminal({
required this.logs,
required this.scrollController,
required this.onClear,
});
@override
Widget build(BuildContext context) {
return Container(
height: 260,
decoration: const BoxDecoration(
color: Color(0xFF020617),
border: Border(top: BorderSide(color: Color(0xFF1E293B))),
),
child: Column(
children: [
Container(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: const BoxDecoration(
border:
Border(bottom: BorderSide(color: Color(0xFF1E293B))),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'TERMINAL',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
color: Color(0xFF475569),
letterSpacing: 2,
fontFamily: 'monospace',
),
),
InkWell(
onTap: onClear,
child: const Padding(
padding:
EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text(
'CLEAR',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: Color(0xFF3B82F6),
letterSpacing: 1,
),
),
),
),
],
),
),
Expanded(
child: logs.isEmpty
? const Center(
child: Text(
'Waiting for logs…',
style: TextStyle(
color: Color(0xFF1E293B),
fontSize: 12,
fontFamily: 'monospace',
fontStyle: FontStyle.italic,
),
),
)
: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
itemCount: logs.length,
itemBuilder: (ctx, i) {
final log = logs[i];
final color = _levelColors[log.level]!;
final levelLabel = log.level.name
.toUpperCase()
.padRight(5)
.substring(0, 5);
return Padding(
padding: const EdgeInsets.only(bottom: 1),
child: SelectableText.rich(
TextSpan(
style: const TextStyle(
fontSize: 11,
fontFamily: 'monospace',
height: 1.6,
),
children: [
TextSpan(
text: '${log.time} ',
style: const TextStyle(
color: Color(0xFF334155)),
),
TextSpan(
text: '[$levelLabel] ',
style: TextStyle(
color: color,
fontWeight: FontWeight.w700,
),
),
TextSpan(
text: log.message,
style: TextStyle(color: color),
),
],
),
),
);
},
),
),
],
),
);
}
}