Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/datadog_session_replay/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ analyzer:

linter:
rules:
directives_ordering: true
prefer_single_quotes: true
prefer_relative_imports: true
unawaited_futures: true
69 changes: 69 additions & 0 deletions packages/datadog_session_replay/lib/datadog_session_replay.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,67 @@ enum TouchPrivacyLevel {
hide,
}

/// Controls how captured `TextStyle.fontFamily` values are rewritten before
/// they are sent as `SRTextStyle.family` on text wireframes.
///
/// Custom callbacks are intentionally not supported: Session Replay builds
/// wireframes in a background isolate, which cannot serialize Dart closures
/// — use [FontFamilyTransformConfig.rules] instead.
enum FontFamilyStrategy {
/// Preserves reasonable CSS-compatible family names while
/// cleaning up Flutter-specific artifacts: strips `packages/<pkg>/`
/// asset-prefix, drops Flutter / platform sentinels that are not
/// valid on the web (e.g. `CupertinoSystemText`, `.SF UI Text`),
/// splits EditableText comma-joined fallback lists, quotes names
/// with spaces, and always appends a generic CSS fallback
/// (`sans-serif`) when none is present so the replay player has a
/// guaranteed fallback. Yields the default replay font stack when
/// the captured family is empty or fully sentinel.
smart,

/// Always emits the single hardcoded CSS stack
/// `-apple-system, BlinkMacSystemFont, Roboto, sans-serif`,
/// regardless of the captured family. Matches the native SDK
/// behavior and is the safest choice if you do not want any
/// Flutter font names leaving the device.
fallback,
Comment on lines +66 to +71
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does it make sense to check the current Platform and supply an Android fallback as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

updated the comment, we already use a android compatible fallback


/// No transform is applied—the raw `TextStyle.fontFamily` (or
/// comma-joined fallback list from EditableText) captured by the
/// recorders is emitted verbatim on the wire. Intended for
/// debugging and backwards compatibility with the previous
/// behavior; not recommended for production because values like
/// `packages/google_fonts/Roboto` or `""` may not render correctly
/// in the replay player.
none,
}

/// Serialized font-family rewriting rules passed to the processor isolate.
///
/// Use [rules] for exact-match overrides only (no callbacks).
class FontFamilyTransformConfig {
/// Default is [FontFamilyStrategy.none] for backwards compatibility; set
/// [FontFamilyStrategy.smart] for web-friendly font stacks.
final FontFamilyStrategy strategy;

/// Exact-match overrides, applied per comma-separated token before built-in
/// normalization when [strategy] is [FontFamilyStrategy.smart]. Keys are
/// case-sensitive—match either the captured token as recorded (trimmed /
/// outer quotes removed) or the same token after stripping a
/// `packages/<pkg>/` asset prefix. Values may be comma-separated stacks.
///
/// Use an empty string key (`''`) in [rules] to supply a custom CSS stack
/// when the captured family is empty or becomes empty after dropping
/// sentinels; if absent, [FontFamilyStrategy.smart] uses the default iOS-parity
/// stack in those cases.
final Map<String, String> rules;

const FontFamilyTransformConfig({
this.strategy = FontFamilyStrategy.none,
this.rules = const {},
});
}

/// Configuration options for Session Replay, including
/// default privacy levels.
class DatadogSessionReplayConfiguration {
Expand Down Expand Up @@ -78,12 +139,20 @@ class DatadogSessionReplayConfiguration {

String? customEndpoint;

/// Rewrites captured font family strings into web-compatible CSS stacks in
/// the processor isolate before snapshots are serialized.
///
/// Defaults to [FontFamilyStrategy.none] so existing behavior is unchanged;
/// use [FontFamilyStrategy.smart] for web-friendly normalization.
FontFamilyTransformConfig fontFamilyTransform;

DatadogSessionReplayConfiguration({
required this.replaySampleRate,
this.textAndInputPrivacyLevel = TextAndInputPrivacyLevel.maskAll,
this.imagePrivacyLevel = ImagePrivacyLevel.maskAll,
this.touchPrivacyLevel = TouchPrivacyLevel.hide,
this.customEndpoint,
this.fontFamilyTransform = const FontFamilyTransformConfig(),
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

import 'package:flutter/cupertino.dart';

import '../material_widgets/checkbox_recorder.dart';
import '../../capture_node.dart';
import '../../recorder.dart';
import '../../view_tree_snapshot.dart';
import '../material_widgets/checkbox_recorder.dart';
import '../recording_extensions.dart';
import 'cupertino_recording_extensions.dart';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

import 'package:flutter/cupertino.dart';

import '../material_widgets/radio_recorder.dart';
import '../../capture_node.dart';
import '../../recorder.dart';
import '../../view_tree_snapshot.dart';
import '../material_widgets/radio_recorder.dart';
import '../recording_extensions.dart';
import 'cupertino_recording_extensions.dart';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

import 'package:flutter/cupertino.dart';

import '../material_widgets/radio_recorder.dart';
import '../material_widgets/switch_recorder.dart';
import '../../capture_node.dart';
import '../../recorder.dart';
import '../../view_tree_snapshot.dart';
import '../material_widgets/radio_recorder.dart';
import '../material_widgets/switch_recorder.dart';
import '../recording_extensions.dart';
import 'cupertino_recording_extensions.dart';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

import '../cupertino_widgets/cupertino_recording_extensions.dart';
import 'radio_recorder.dart';
import '../../../extensions.dart';
import '../../../sr_data_models.dart';
import '../../capture_node.dart';
import '../../recorder.dart';
import '../../view_tree_snapshot.dart';
import '../cupertino_widgets/cupertino_recording_extensions.dart';
import '../recording_extensions.dart';
import 'radio_recorder.dart';

/// Detects 'Switch' widgets and places a Switch icon
/// on SessionReplay.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

import 'package:flutter/widgets.dart';

import '../../../datadog_session_replay.dart';
import '../../sr_data_models.dart';
import '../recorder.dart';
import '../../../datadog_session_replay.dart';

extension SRTextAlignment on TextAlign {
SRHorizontalAlignment getSrHorizontalAlignment(TextDirection? textDirection) {
Expand Down
10 changes: 5 additions & 5 deletions packages/datadog_session_replay/lib/src/capture/recorder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ import '../rum_context.dart';
import '../widgets.dart';
import 'capture_node.dart';
import 'element_recorders/container_recorder.dart';
import 'element_recorders/cupertino_widgets/cupertino_checkbox_recorder.dart';
import 'element_recorders/cupertino_widgets/cupertino_radio_recorder.dart';
import 'element_recorders/cupertino_widgets/cupertino_switch_recorder.dart';
import 'element_recorders/custom_paint_recorder.dart';
import 'element_recorders/editable_text_recorder.dart';
import 'element_recorders/image_recorder.dart';
import 'element_recorders/privacy_recorder.dart';
import 'element_recorders/text_recorder.dart';
import 'element_recorders/material_widgets/checkbox_recorder.dart';
import 'element_recorders/cupertino_widgets/cupertino_checkbox_recorder.dart';
import 'element_recorders/material_widgets/radio_recorder.dart';
import 'element_recorders/cupertino_widgets/cupertino_radio_recorder.dart';
import 'element_recorders/material_widgets/switch_recorder.dart';
import 'element_recorders/cupertino_widgets/cupertino_switch_recorder.dart';
import 'element_recorders/privacy_recorder.dart';
import 'element_recorders/text_recorder.dart';
import 'pointer_capture.dart';
import 'view_tree_snapshot.dart';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ class DatadogSessionReplay {
});

if (success) {
await _processor.start();
await _processor.start(
fontFamilyTransform: _configuration.fontFamilyTransform,
);

_startPeriodicCapture();
WidgetsBinding.instance.addPostFrameCallback((_) {
Expand Down
Loading