Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Next

- feat: add manual session recording control APIs ([#256](https://github.com/PostHog/posthog-flutter/pull/256))
- `startSessionRecording({bool resumeCurrent = true})` Start session recording, optionally starting a new session
- `stopSessionRecording()` Stop the current session recording
- `isSessionReplayActive()` Check if session replay is currently active
- feat: add `getFeatureFlagResult` API ([#279](https://github.com/PostHog/posthog-flutter/pull/279))

# 5.13.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ class PosthogFlutterPlugin :
"isSessionReplayActive" -> {
result.success(isSessionReplayActive())
}
"startSessionRecording" -> {
startSessionRecording(call, result)
}
"stopSessionRecording" -> {
stopSessionRecording(result)
}
"getSessionId" -> {
getSessionId(result)
}
Expand All @@ -194,6 +200,20 @@ class PosthogFlutterPlugin :

private fun isSessionReplayActive(): Boolean = PostHog.isSessionReplayActive()

private fun startSessionRecording(
call: MethodCall,
result: Result,
) {
val resumeCurrent = call.arguments as? Boolean ?: true
PostHog.startSessionReplay(resumeCurrent)
result.success(null)
}

private fun stopSessionRecording(result: Result) {
PostHog.stopSessionReplay()
result.success(null)
}

private fun handleMetaEvent(
call: MethodCall,
result: Result,
Expand Down
90 changes: 90 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,96 @@ class InitialScreenState extends State<InitialScreen> {
child: Text("distinctId"),
)),
const Divider(),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
"Session Recording",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
onPressed: () async {
await _posthogFlutterPlugin.startSessionRecording();
if (mounted && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Session recording started (resume current)'),
duration: Duration(seconds: 2),
),
);
}
},
child: const Text("Start Recording"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
),
onPressed: () async {
await _posthogFlutterPlugin.startSessionRecording(
resumeCurrent: false);
if (mounted && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Session recording started (new session)'),
duration: Duration(seconds: 2),
),
);
}
},
child: const Text("Start New Session"),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: () async {
await _posthogFlutterPlugin.stopSessionRecording();
if (mounted && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Session recording stopped'),
duration: Duration(seconds: 2),
),
);
}
},
child: const Text("Stop Recording"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
),
onPressed: () async {
final isActive =
await _posthogFlutterPlugin.isSessionReplayActive();
if (mounted && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Session replay active: $isActive'),
duration: const Duration(seconds: 2),
),
);
}
},
child: const Text("Check Active"),
),
],
),
const Divider(),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text(
Expand Down
109 changes: 109 additions & 0 deletions ios/Classes/.swiftformat
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Disabled rules
--disable docComments
--disable noForceUnwrapInTests
--disable hoistTry
--disable redundantAsync
--disable redundantThrows
--disable redundantProperty
--disable redundantReturn
--disable redundantClosure
--disable redundantType
--disable wrapPropertyBodies
--disable blankLinesBetweenScopes

--acronyms ID,URL,UUID
--allman false
--anonymousforeach convert
--assetliterals visual-width
--asynccapturing
--beforemarks
--binarygrouping none
--callsiteparen default
--categorymark "MARK: %c"
--classthreshold 0
--closingparen balanced
--closurevoid remove
--commas always
--complexattrs preserve
--computedvarattrs preserve
--condassignment after-property
--conflictmarkers reject
--dateformat system
--decimalgrouping ignore
--doccomments before-declarations
--elseposition same-line
--emptybraces no-space
--enumnamespaces always
--enumthreshold 0
--exponentcase lowercase
--exponentgrouping disabled
--extensionacl on-extension
--extensionlength 0
--extensionmark "MARK: - %t + %c"
--fractiongrouping disabled
--fragment false
--funcattributes preserve
--generictypes
--groupedextension "MARK: %c"
--guardelse auto
--header ignore
--hexgrouping 4,8
--hexliteralcase uppercase
--ifdef indent
--importgrouping alpha
--indent 4
--indentcase false
--indentstrings false
--initcodernil false
--lifecycle
--lineaftermarks true
--linebreaks lf
--markcategories true
--markextensions always
--marktypes always
--maxwidth none
--modifierorder
--nevertrailing
--nilinit remove
--noncomplexattrs
--nospaceoperators
--nowrapoperators
--octalgrouping none
--onelineforeach ignore
--operatorfunc spaced
--organizationmode visibility
--organizetypes actor,class,enum,struct
--patternlet hoist
--ranges spaced
--redundanttype infer-locals-only
--self remove
--selfrequired
--semicolons never
--shortoptionals except-properties
--smarttabs enabled
--someany true
--storedvarattrs preserve
--stripunusedargs always
--structthreshold 0
--tabwidth unspecified
--throwcapturing
--timezone system
--trailingclosures
--trimwhitespace always
--typeattributes preserve
--typeblanklines remove
--typedelimiter space-after
--typemark "MARK: - %t"
--voidtype void
--wraparguments preserve
--wrapcollections preserve
--wrapconditions preserve
--wrapeffects preserve
--wrapenumcases always
--wrapparameters default
--wrapreturntype preserve
--wrapternary default
--wraptypealiases preserve
--xcodeindentation disabled
--yodaswap always
--hexgrouping ignore
29 changes: 27 additions & 2 deletions ios/Classes/PosthogFlutterPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ public class PosthogFlutterPlugin: NSObject, FlutterPlugin {
sendFullSnapshot(call, result: result)
case "isSessionReplayActive":
isSessionReplayActive(result: result)
case "startSessionRecording":
startSessionRecording(call, result: result)
case "stopSessionRecording":
stopSessionRecording(result: result)
case "getSessionId":
getSessionId(result: result)
case "openUrl":
Expand Down Expand Up @@ -457,6 +461,28 @@ extension PosthogFlutterPlugin {
#endif
}

private func startSessionRecording(
_ call: FlutterMethodCall,
result: @escaping FlutterResult
) {
#if os(iOS)
let resumeCurrent = call.arguments as? Bool ?? true
PostHogSDK.shared.startSessionRecording(resumeCurrent: resumeCurrent)
result(nil)
#else
result(nil)
#endif
}

private func stopSessionRecording(result: @escaping FlutterResult) {
#if os(iOS)
PostHogSDK.shared.stopSessionRecording()
result(nil)
#else
result(nil)
#endif
}

private func openUrl(
_ call: FlutterMethodCall,
result: @escaping FlutterResult
Expand Down Expand Up @@ -672,8 +698,7 @@ extension PosthogFlutterPlugin {
}
}

private func reloadFeatureFlags(_ result: @escaping FlutterResult
) {
private func reloadFeatureFlags(_ result: @escaping FlutterResult) {
PostHogSDK.shared.reloadFeatureFlags()
result(nil)
}
Expand Down
19 changes: 19 additions & 0 deletions lib/posthog_flutter_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -294,4 +294,23 @@ class PosthogFlutterWeb extends PosthogFlutterPlatformInterface {
printIfDebug('Exception in captureException: $exception');
}
}

@override
Future<void> startSessionRecording({bool resumeCurrent = true}) async {
return handleWebMethodCall(MethodCall('startSessionRecording', {
'resumeCurrent': resumeCurrent,
}));
}

@override
Future<void> stopSessionRecording() async {
return handleWebMethodCall(const MethodCall('stopSessionRecording'));
}

@override
Future<bool> isSessionReplayActive() async {
final result =
await handleWebMethodCall(const MethodCall('isSessionReplayActive'));
return result as bool? ?? false;
}
}
31 changes: 30 additions & 1 deletion lib/src/posthog.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'package:meta/meta.dart';
import 'package:flutter/foundation.dart';

import 'package:posthog_flutter/src/error_tracking/posthog_error_tracking_autocapture_integration.dart';
import 'package:posthog_flutter/src/error_tracking/posthog_exception.dart';
import 'feature_flag_result.dart';
import 'posthog_config.dart';
import 'posthog_flutter_platform_interface.dart';
import 'posthog_internal_events.dart';
import 'posthog_observer.dart';

class Posthog {
Expand Down Expand Up @@ -43,6 +44,10 @@ class Posthog {
Future<void> setup(PostHogConfig config) {
_config = config; // Store the config

if (config.sessionReplay) {
PostHogInternalEvents.sessionRecordingActive.value = true;
}

_installFlutterIntegrations(config);

return _posthog.setup(config);
Expand Down Expand Up @@ -314,6 +319,7 @@ class Posthog {
Future<void> close() {
_config = null;
_currentScreen = null;
PostHogInternalEvents.sessionRecordingActive.value = false;
PosthogObserver.clearCurrentContext();

// Uninstall Flutter integrations
Expand All @@ -325,5 +331,28 @@ class Posthog {
/// Returns the session Id if a session is active
Future<String?> getSessionId() => _posthog.getSessionId();

/// Starts session recording.
///
/// This method will have no effect if PostHog is not enabled, or if session
/// replay is disabled in your project settings.
///
/// [resumeCurrent] - If true (default), resumes recording of the current session.
/// If false, starts a new session and begins recording.
Future<void> startSessionRecording({bool resumeCurrent = true}) async {
await _posthog.startSessionRecording(resumeCurrent: resumeCurrent);
PostHogInternalEvents.sessionRecordingActive.value = true;
}

/// Stops the current session recording if one is in progress.
///
/// This method will have no effect if PostHog is not enabled.
Future<void> stopSessionRecording() async {
await _posthog.stopSessionRecording();
PostHogInternalEvents.sessionRecordingActive.value = false;
}

/// Returns whether session replay is currently active.
Future<bool> isSessionReplayActive() => _posthog.isSessionReplayActive();

Posthog._internal();
}
Loading
Loading