Skip to content

Commit 62c8c1b

Browse files
authored
Merge pull request #1 from unix14/features/funnels_manager
Features/funnels manager
2 parents c391a0a + 72b8014 commit 62c8c1b

File tree

11 files changed

+314
-5
lines changed

11 files changed

+314
-5
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ A Flutter library for custom analytics management, designed to streamline integr
66

77
- **Custom Event Tracking**: Easily log and manage events in your application.
88
- **Screen View Logging**: Track screen views to gain insights into user navigation.
9+
- ** **Funnels Manager**: Tracks the time it takes the users go through a journey in our app.
910
- **Robust Reporting**: Access detailed reports on user interactions and events.
1011

1112
## Getting Started
@@ -90,6 +91,34 @@ class _SimpleEventExampleScreenState extends State<SimpleEventExampleScreen> {
9091
}
9192
```
9293

94+
## Funnels Manager
95+
96+
### Purpose
97+
The `FunnelsManager` is a tool designed to track the sequence of user interactions or events in a specific flow, commonly referred to as a "funnel." A funnel is a defined series of steps that a user follows, and by tracking how users interact with these steps, you can gather insights on engagement, abandonment points, and overall user behavior.
98+
99+
### Implementation
100+
101+
1. **Create and Start a Funnel**
102+
To start tracking a funnel, you need to define a funnel using the `AnalytixFunnel` class and register it with the `FunnelsManager`. Here’s an example of how to start a funnel:
103+
104+
```dart
105+
FunnelsManager().start(AnalytixFunnel(Funnels.funnel_2, shouldCountTime: true));
106+
```
107+
• Funnels.funnel_2 is the name of the funnel you’re tracking.
108+
• The shouldCountTime flag tracks the time a user spends on each step of the funnel.
109+
2. **Tracking Events in the Funnel**
110+
After starting the funnel, you can track specific events or steps within the funnel. For example:
111+
```dart
112+
FunnelsManager().track(Funnels.funnel_3, "step_1");
113+
```
114+
This code logs the event "step_1" within the funnel_3.
115+
3. **Finishing a Funnel**
116+
Once the funnel is complete (i.e., when the user completes all the steps), make sure to finish the funnel to capture the final event and any time-related data:
117+
```dart
118+
FunnelsManager().finish(Funnels.funnel_2, "finish");
119+
```
120+
This marks the funnel as finished and logs the final event.
121+
93122
For help getting started with Flutter development, view the online
94123
[documentation](https://flutter.dev/).
95124

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
extension MapExtensions on Map<String, dynamic> {
3+
4+
String getString({bool isInner = false}) {
5+
String result = "";
6+
if(entries.isEmpty) {
7+
return "{}";
8+
}
9+
for(MapEntry<String, dynamic> keyValuePair in entries) {
10+
if(keyValuePair.value is Map<String, dynamic>) {
11+
result += "\n -> '${keyValuePair.key}' : ";
12+
// print inner map
13+
var innerMapString = (keyValuePair.value as Map<String, dynamic>).getString(isInner: true);
14+
result += innerMapString;
15+
} else {
16+
result += "\n ${isInner ? " " : ""}-> '${keyValuePair.key}': '${keyValuePair.value}'";
17+
}
18+
}
19+
return result;
20+
}
21+
}

lib/analitix/models/analytix_event.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11

2+
import 'package:analitix/analitix/extensions/map_extenions.dart';
3+
24
class AnalytixEvent {
35
final String eventName;
46
final String? subEventName;
@@ -8,6 +10,10 @@ class AnalytixEvent {
810

911
@override
1012
String toString() {
11-
return 'AnalytixEvent: $eventName, subEventName: $subEventName, parameters: $parameters';
13+
String result = 'AnalytixEvent: $eventName\nsubEventName: $subEventName';
14+
if(parameters?.isNotEmpty == true) {
15+
result += "\nparameters: ${parameters!.getString()}";
16+
}
17+
return result;
1218
}
1319
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
import 'package:analitix/analitix/abstract/analytix_manager.dart';
3+
import 'package:flutter/foundation.dart';
4+
5+
import 'funnel.dart';
6+
7+
class AnalytixFunnel extends Funnel {
8+
9+
// Constructor
10+
AnalytixFunnel(String funnelName, {bool shouldCountTime = false})
11+
: super(funnelName, shouldCountTime: shouldCountTime);
12+
13+
@override
14+
void track(String eventName) {
15+
super.track(eventName);
16+
var shouldReport = !kDebugMode || true;
17+
if(shouldReport) {
18+
var shouldReportFunnelDuration = shouldCountTime && endTime != null && startTime != null;
19+
var durationTime = shouldReportFunnelDuration ? endTime!.difference(startTime!).inMilliseconds : 0;
20+
var duration = shouldReportFunnelDuration ? (durationTime.toString()) : "";
21+
AnalytixManager().logEvent(funnelName, eventName,
22+
params: shouldReportFunnelDuration ? {
23+
'duration': duration
24+
} : {}
25+
);
26+
}
27+
}
28+
}

lib/funnels_manager/funnel.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
3+
import 'package:flutter/cupertino.dart';
4+
5+
/// Funnel class to track user journey
6+
/// it defines the structure of a funnel and its methods
7+
abstract class Funnel {
8+
9+
String funnelName;
10+
11+
// Time management
12+
bool shouldCountTime;
13+
DateTime? startTime;
14+
DateTime? endTime;
15+
16+
Funnel(this.funnelName, {this.shouldCountTime = false});
17+
18+
void start() {
19+
if(shouldCountTime) {
20+
startTime = DateTime.now();
21+
}
22+
print("Funnel Tracking:: Start: $funnelName");
23+
}
24+
25+
@mustCallSuper
26+
void track(String eventName) {
27+
print("Funnel Tracking:: $funnelName: $eventName");
28+
}
29+
30+
void finish() {
31+
if (shouldCountTime) {
32+
endTime = DateTime.now();
33+
}
34+
print("Funnel Tracking:: End: $funnelName ${shouldCountTime ? ", Duration: ${endTime!.difference(startTime!)}" : ""}");
35+
}
36+
}

lib/funnels_manager/funnels.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
/// Funnels are used to track user journey within the app
3+
/// and measure the time it takes to complete a certain journey
4+
class Funnels {
5+
6+
/// Funnel 1 is used to track the time it takes the users
7+
/// who just entered the app to click on the Funnels button
8+
static String funnel_1 = 'f#1';
9+
10+
/// Funnel 2 is used to track the time it takes the users
11+
/// who are already in the funnels screen and then they click on the exit button
12+
static String funnel_2 = 'f#2';
13+
14+
/// Funnel 3 is used to track the time it takes the users
15+
/// who are already in the funnels screen and then they move between the steps
16+
/// each step starts with start and ends with finish events
17+
/// in between we will report the step number and in the end we will report
18+
/// the time it takes to move between the steps
19+
/// it includes the following events:
20+
/// - start
21+
/// - step_1
22+
/// - step_2
23+
/// - step_3
24+
/// - finish
25+
static String funnel_3 = 'f#3';
26+
27+
/// Funnel Internet is used for users who are already registered and logged in to the app
28+
/// we want to measure the events related to internet connection
29+
/// it includes the following events:
30+
/// - Internet_down
31+
/// - Internet_up (will be sent only if internet_down was sent)
32+
static String funnel_Internet = 'Internet';
33+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
2+
import 'funnel.dart';
3+
4+
class FunnelsManager {
5+
6+
static final FunnelsManager _instance = FunnelsManager._internal();
7+
8+
factory FunnelsManager() => _instance;
9+
FunnelsManager._internal();
10+
11+
final Map<String, Funnel> _funnels = {};
12+
13+
void start(Funnel funnel) {
14+
var funnelName = funnel.funnelName;
15+
if (_funnels[funnelName] == null) {
16+
print("FunnelsManager:: Adding funnel with name: $funnelName");
17+
_funnels[funnelName] = funnel;
18+
funnel.start();
19+
} else {
20+
print("FunnelsManager:: Funnel with name: $funnelName already exists");
21+
}
22+
}
23+
24+
void track(String funnelName, String data) {
25+
if (_funnels[funnelName] == null) {
26+
print("FunnelsManager:: No funnel found with name: $funnelName");
27+
return;
28+
}
29+
_funnels[funnelName]?.track(data);
30+
}
31+
32+
void finish(String funnelName, String data) {
33+
print("FunnelsManager:: Removing funnel with name: $funnelName");
34+
_funnels[funnelName]?.finish();
35+
// we are tracking the funnel only if it should count time
36+
if(_funnels[funnelName]?.shouldCountTime == true) {
37+
track(funnelName, data);
38+
}
39+
_funnels.remove(funnelName);
40+
}
41+
42+
void clear() {
43+
print("FunnelsManager:: Clearing all funnels");
44+
_funnels.clear();
45+
}
46+
}

lib/main.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import 'package:analitix/analitix/abstract/analytix_manager.dart';
2+
import 'package:analitix/funnels_manager/analytix_funnel.dart';
3+
import 'package:analitix/funnels_manager/funnels.dart';
4+
import 'package:analitix/funnels_manager/funnels_manager.dart';
25
import 'package:flutter/material.dart';
36
import 'custom_reporters/screen_logger_reporter.dart';
47
import 'ui/analytix_example_screen.dart';
@@ -26,4 +29,7 @@ _initAnalytics() {
2629
AnalytixManager().setUserProperty("amountOfConnectedSessions", 3);
2730
AnalytixManager().setUserProperty("lastLoginTime", DateTime.now());
2831
AnalytixManager().setUserProperty("refreshToken", "FNvof-Qq3llcnu8nvknerk87@#aaAxz-zZq3");
32+
33+
// Start f1 funnel
34+
FunnelsManager().start(AnalytixFunnel(Funnels.funnel_1, shouldCountTime: true));
2935
}

lib/ui/simple_event_example_screen.dart

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22
import 'package:analitix/analitix/abstract/analytix_manager.dart';
33
import 'package:analitix/custom_reporters/screen_logger_reporter.dart';
44
import 'package:analitix/ui/screen_view_example_screen.dart';
5+
import 'package:analitix/ui/simple_funnels_example_screen.dart';
56
import 'package:flutter/material.dart';
67

8+
import '../funnels_manager/funnels.dart';
9+
import '../funnels_manager/funnels_manager.dart';
10+
711
class SimpleEventExampleScreen extends StatefulWidget {
812
@override
913
State<SimpleEventExampleScreen> createState() => _SimpleEventExampleScreenState();
@@ -50,13 +54,22 @@ class _SimpleEventExampleScreenState extends State<SimpleEventExampleScreen> {
5054
});
5155
} : null,
5256
),
57+
IconButton(
58+
tooltip: "Funnels Manager",
59+
icon: const Icon(Icons.filter_alt_outlined),
60+
onPressed: () async {
61+
_showSnackBar("Open Funnels Manager");
62+
FunnelsManager().finish(Funnels.funnel_1, "finish");
63+
await _goToScreen(const SimpleFunnelExampleScreen());
64+
FunnelsManager().finish(Funnels.funnel_2, "finish");
65+
},
66+
),
5367
IconButton(
5468
tooltip: "Navigate to new Screen",
5569
icon: const Icon(Icons.screen_rotation_outlined),
5670
onPressed: () async {
5771
_showSnackBar("Open new Screen");
58-
await Navigator.push(context, MaterialPageRoute(builder: (context) => const ScreenViewExampleScreen()));
59-
setState(() {});
72+
await _goToScreen(const ScreenViewExampleScreen());
6073
},
6174
),
6275
// disable / enable button for data collection
@@ -130,8 +143,13 @@ class _SimpleEventExampleScreenState extends State<SimpleEventExampleScreen> {
130143
_showSnackBar(String message) {
131144
SnackBar snackBar = SnackBar(
132145
content: Text(message),
133-
duration: const Duration(seconds: 2),
146+
duration: const Duration(seconds: 1),
134147
);
135148
ScaffoldMessenger.of(context).showSnackBar(snackBar);
136149
}
150+
151+
_goToScreen(StatefulWidget newScreen) async {
152+
await Navigator.push(context, MaterialPageRoute(builder: (context) => newScreen));
153+
setState(() {});
154+
}
137155
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
2+
import 'package:flutter/material.dart';
3+
4+
import '../funnels_manager/analytix_funnel.dart';
5+
import '../funnels_manager/funnels.dart';
6+
import '../funnels_manager/funnels_manager.dart';
7+
8+
class SimpleFunnelExampleScreen extends StatefulWidget {
9+
const SimpleFunnelExampleScreen({super.key});
10+
11+
@override
12+
State<SimpleFunnelExampleScreen> createState() => _SimpleFunnelExampleScreenState();
13+
}
14+
15+
class _SimpleFunnelExampleScreenState extends State<SimpleFunnelExampleScreen> {
16+
17+
final PageController _pageController = PageController();
18+
19+
@override
20+
void initState() {
21+
// Start tracking Funnel 2
22+
FunnelsManager().start(AnalytixFunnel(Funnels.funnel_2, shouldCountTime: true));
23+
24+
// Start track Funnel 3
25+
FunnelsManager().start(AnalytixFunnel(Funnels.funnel_3, shouldCountTime: true));
26+
FunnelsManager().track(Funnels.funnel_3, "start");
27+
super.initState();
28+
}
29+
30+
@override
31+
void dispose() {
32+
// finish the funnel if a user decides to leave the screen
33+
// before finishing the whole steps. This won't affect if the user finishes all steps
34+
FunnelsManager().finish(Funnels.funnel_2, "finish");
35+
FunnelsManager().finish(Funnels.funnel_3, "finish");
36+
37+
super.dispose();
38+
}
39+
40+
@override
41+
Widget build(BuildContext context) {
42+
return Scaffold(
43+
appBar: AppBar(
44+
title: const Text('Simple Funnel Example'),
45+
),
46+
body: Center(
47+
child: PageView.builder(
48+
itemCount: 4,
49+
controller: _pageController,
50+
physics: const NeverScrollableScrollPhysics(),
51+
itemBuilder: (context, index) {
52+
var indexUserFriendly = index+1;
53+
return Column(
54+
children: [
55+
Center(
56+
child: ListTile(
57+
title: Text('Page #$indexUserFriendly'),
58+
onTap: () {
59+
// start a new funnel if possible - to track how much time we stay at each screen
60+
FunnelsManager().start(AnalytixFunnel(Funnels.funnel_3, shouldCountTime: true));
61+
// Track the event
62+
FunnelsManager().track(Funnels.funnel_3, "step_$indexUserFriendly");
63+
FunnelsManager().finish(Funnels.funnel_3, "finish");
64+
65+
66+
if(indexUserFriendly <4) {
67+
// start a new funnel for the next screen
68+
FunnelsManager().start(AnalytixFunnel(Funnels.funnel_3, shouldCountTime: true));
69+
_pageController.animateToPage(indexUserFriendly, duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
70+
} else {
71+
Navigator.pop(context);
72+
}
73+
},
74+
),
75+
),
76+
],
77+
);
78+
},
79+
)
80+
),
81+
);
82+
}
83+
}

0 commit comments

Comments
 (0)