Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do those API work even if config.sessionReplay is disabled or both ways?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what we did for the other sdks I believe. So if local config is false, people can start a recording later on manually.

If remote config/flags is false, then no-op

Trying to validate these with some local testing

- `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 `beforeSend` callback to `PostHogConfig` for dropping or modifying events before they are sent to PostHog ([#255](https://github.com/PostHog/posthog-flutter/pull/255))
- **Limitation**:
- Does NOT intercept native-initiated events such as:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ class PosthogFlutterPlugin :
"isSessionReplayActive" -> {
result.success(isSessionReplayActive())
}
"startSessionRecording" -> {
startSessionRecording(call, result)
}
"stopSessionRecording" -> {
stopSessionRecording(result)
}
"getSessionId" -> {
getSessionId(result)
}
Expand All @@ -190,6 +196,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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ported from posthog-ios repo

--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 @@ -223,6 +223,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 @@ -455,6 +459,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 @@ -645,8 +671,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 @@ -281,4 +281,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;
}
}
18 changes: 18 additions & 0 deletions lib/src/posthog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -210,5 +210,23 @@ class Posthog {

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}) =>
_posthog.startSessionRecording(resumeCurrent: resumeCurrent);

/// Stops the current session recording if one is in progress.
///
/// This method will have no effect if PostHog is not enabled.
Future<void> stopSessionRecording() => _posthog.stopSessionRecording();

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

Posthog._internal();
}
41 changes: 41 additions & 0 deletions lib/src/posthog_flutter_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -658,4 +658,45 @@ class PosthogFlutterIO extends PosthogFlutterPlatformInterface {
printIfDebug('Exception on openUrl: $exception');
}
}

@override
Future<void> startSessionRecording({bool resumeCurrent = true}) async {
if (!isSupportedPlatform()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!isSupportedPlatform()) {
if (!isSupportedPlatform() || macOS) {

all those methods should bail if its a macOS app since replay isnt supported, theres no need to call method channels if we know its not gonna work
the code above is just pseudocode

return;
}

try {
await _methodChannel.invokeMethod('startSessionRecording', resumeCurrent);
} on PlatformException catch (exception) {
printIfDebug('Exception on startSessionRecording: $exception');
}
}

@override
Future<void> stopSessionRecording() async {
if (!isSupportedPlatform()) {
return;
}

try {
await _methodChannel.invokeMethod('stopSessionRecording');
} on PlatformException catch (exception) {
printIfDebug('Exception on stopSessionRecording: $exception');
}
}

@override
Future<bool> isSessionReplayActive() async {
if (!isSupportedPlatform()) {
return false;
}

try {
final result = await _methodChannel.invokeMethod('isSessionReplayActive');
return result as bool? ?? false;
} on PlatformException catch (exception) {
printIfDebug('Exception on isSessionReplayActive: $exception');
return false;
}
}
}
Loading
Loading