Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ void main() {
defaultCapturePrivacy: TreeCapturePrivacy(
textAndInputPrivacyLevel: TextAndInputPrivacyLevel.maskSensitiveInputs,
imagePrivacyLevel: ImagePrivacyLevel.maskNone,
),
touchPrivacyLevel: TouchPrivacyLevel.show,
touchPrivacyLevel: TouchPrivacyLevel.show,
),
);
platform = MockDatadogSessionReplayPlatform();
DatadogSessionReplayPlatform.instance = platform;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ void main() {
defaultCapturePrivacy: TreeCapturePrivacy(
textAndInputPrivacyLevel: TextAndInputPrivacyLevel.maskSensitiveInputs,
imagePrivacyLevel: ImagePrivacyLevel.maskNone,
),
touchPrivacyLevel: TouchPrivacyLevel.show,
touchPrivacyLevel: TouchPrivacyLevel.show,
),
);
platform = MockDatadogSessionReplayPlatform();
DatadogSessionReplayPlatform.instance = platform;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ void main() {
defaultCapturePrivacy: TreeCapturePrivacy(
textAndInputPrivacyLevel: TextAndInputPrivacyLevel.maskAll,
imagePrivacyLevel: ImagePrivacyLevel.maskNone,
),
touchPrivacyLevel: TouchPrivacyLevel.show,
touchPrivacyLevel: TouchPrivacyLevel.show,
),
);
platform = MockDatadogSessionReplayPlatform();
DatadogSessionReplayPlatform.instance = platform;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,18 @@ class DatadogSessionReplayConfiguration {

String? customEndpoint;

/// If `true` (default), recording begins immediately when Session Replay is
/// enabled. Set to `false` to defer recording until
/// [DatadogSessionReplay.startRecording] is called explicitly.
bool startRecordingImmediately;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025-Present Datadog, Inc.

import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';

Expand All @@ -23,6 +24,20 @@ const int _labelMinWidth = 125;
// This is essentially an 800x800 image, with a raw size of 2meg
const int maxImageSize = 640000;

/// Computes a fast non-cryptographic hash of [byteData] by sampling every
/// [stride]-th byte (capped at ~1 KB of input). Size is folded in to reduce
/// collisions between images that share the same pixel pattern but differ in
/// dimensions.
int hashImageBytes(ByteData byteData) {
final bytes = byteData.buffer.asUint8List();
final stride = max(1, bytes.length ~/ 1024);
var hash = 0;
for (var i = 0; i < bytes.length; i += stride) {
hash = (hash * 31 + bytes[i]) & 0x1FFFFFFFFFFFFFFF;
}
return hash ^ bytes.length;
}

class ImageRecorder implements ElementRecorder {
final KeyGenerator keyGenerator;

Expand Down Expand Up @@ -112,6 +127,7 @@ class ImageRecorder implements ElementRecorder {
attributes,
wireframeId: elementId,
resourceKey: resourceKey,
keyGenerator: keyGenerator,
),
],
);
Expand All @@ -137,18 +153,29 @@ class ImageRecorder implements ElementRecorder {
format: ImageByteFormat.rawRgba,
);
if (byteData != null) {
final resourceKey = keyGenerator.keyForImage(image);
await DatadogSessionReplayPlatform.instance.saveImageForProcessing(
resourceKey,
image.width,
image.height,
byteData,
);
final contentHash = hashImageBytes(byteData);
// Re-use an existing resourceKey if we've seen this content before,
// even if it arrived as a different ui.Image instance.
final existingKey = keyGenerator.resourceKeyForHash(contentHash);
final resourceKey = existingKey ?? keyGenerator.keyForImage(image);

if (existingKey == null) {
// First time seeing this content — send bytes to native and cache.
await DatadogSessionReplayPlatform.instance.saveImageForProcessing(
resourceKey,
image.width,
image.height,
byteData,
);
keyGenerator.cacheContentHash(contentHash, resourceKey);
}

nodes.add(
ResourceImageNode(
attributes,
wireframeId: elementId,
resourceKey: resourceKey,
keyGenerator: keyGenerator,
),
);
}
Expand Down Expand Up @@ -190,18 +217,27 @@ class ImageRecorder implements ElementRecorder {
class ResourceImageNode extends CaptureNode {
final int wireframeId;
final int resourceKey;
final KeyGenerator keyGenerator;

const ResourceImageNode(
super.attributes, {
required this.wireframeId,
required this.resourceKey,
required this.keyGenerator,
});

@override
List<SRWireframe> buildWireframes() {
final resourceId = DatadogSessionReplayPlatform.instance.resourceIdForKey(
resourceKey,
);
// Check Dart-side cache first to avoid a native call every capture cycle.
var resourceId = keyGenerator.cachedResourceId(resourceKey);
if (resourceId == null) {
resourceId = DatadogSessionReplayPlatform.instance.resourceIdForKey(
resourceKey,
);
if (resourceId != null) {
keyGenerator.cacheResourceId(resourceKey, resourceId);
}
}

return [
SRImageWireframe(
Expand Down
Loading