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
29 changes: 29 additions & 0 deletions .github/workflows/publish-pub-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ name: Publish to pub.dev
# flashcat_flutter_plugin -> flashcat_flutter_plugin-v{{version}}
# flashcat_webview_tracking -> flashcat_webview_tracking-v{{version}}
# flashcat_tracking_http_client -> flashcat_tracking_http_client-v{{version}}
#
# Release order matters. flashcat_webview_tracking and
# flashcat_tracking_http_client depend on flashcat_flutter_plugin from pub.dev
# (there are no dependency_overrides), so whenever a release bumps that
# constraint, push flashcat_flutter_plugin-v{{version}} first and wait for it to
# publish before tagging the other two. The "Ensure the flashcat_flutter_plugin
# dependency is published" step below enforces this.
on:
push:
tags:
Expand Down Expand Up @@ -78,6 +85,28 @@ jobs:
exit 1
fi

# `flutter pub get` below resolves flashcat_flutter_plugin from pub.dev, so
# a dependent package tagged before it fails with an opaque version-solving
# error. Check the constraint's lower bound up front instead.
- name: Ensure the flashcat_flutter_plugin dependency is published
if: steps.package.outputs.name != 'flashcat_flutter_plugin'
working-directory: ${{ steps.package.outputs.dir }}
env:
PACKAGE_NAME: ${{ steps.package.outputs.name }}
run: |
required=$(grep -E "^[[:space:]]+flashcat_flutter_plugin:[[:space:]]*\^?[0-9]" pubspec.yaml \
| head -1 | sed -E 's/.*[^0-9]([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
if [ -z "$required" ]; then
echo "No pinned flashcat_flutter_plugin dependency found; nothing to check."
exit 0
fi
if ! curl -sfo /dev/null "https://pub.dev/api/packages/flashcat_flutter_plugin/versions/${required}"; then
echo "flashcat_flutter_plugin ${required} is not on pub.dev yet, so ${PACKAGE_NAME} cannot resolve it." >&2
echo "Push the flashcat_flutter_plugin-v${required} tag and let it publish first, then re-run this release." >&2
exit 1
fi
echo "flashcat_flutter_plugin ${required} is published."

# setup-dart performs the OIDC temporary-token exchange with pub.dev
- uses: dart-lang/setup-dart@v1

Expand Down
15 changes: 15 additions & 0 deletions packages/datadog_flutter_plugin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## 0.2.0

* Add opt-in RUM remote configuration through
`remoteConfigurationEnabled`, which defaults to `false`.
* Add `beforeSampling` so applications can synchronously override the sample
rate for each new session. The callback also runs when remote configuration
is disabled; in that case it receives the local sample rate and no custom
values.
* Add `DatadogRum.setForcedSession()` and
`DatadogRum.getRemoteConfig()`. Forced collection lasts for the current
process and does not imply Session Replay support in Flutter.
* Upgrade the native FlashCat SDKs to Android 0.7.0 and iOS 0.6.0. The Android
upgrade includes the default NTP endpoint change introduced in Android
0.6.0.

## 0.1.3

* Ship the OkHttp TLS-provider `-dontwarn` rules in the plugin's consumer
Expand Down
1 change: 1 addition & 0 deletions packages/datadog_flutter_plugin/NATIVE_SDK_VERSIONS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
| Flutter | iOS SDK | Android SDK |
|---------|---------|-------------|
| 0.2.0 | 0.6.0 | 0.7.0 |
| 3.0.1 | 3.4.0 | 3.5.0 |
| 3.0.0 | 3.4.0 | 3.5.0 |
| 2.16.1 | 2.30.1 | 2.26.1 |
Expand Down
55 changes: 53 additions & 2 deletions packages/datadog_flutter_plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ This SDK is forked from the [Datadog Flutter SDK](https://github.com/DataDog/dd-
- **Package name**: Published as `flashcat_flutter_plugin` and `flashcat_webview_tracking`; imports use `package:flashcat_flutter_plugin/…`. Only the published package name changes — internal Dart/Kotlin/Swift namespaces remain `datadog*`.
- **Native dependencies**: Uses the FlashCat forks — iOS `Flashcat*` pods / `fc-sdk-ios` (SPM), Android `cloud.flashcat:*`.
- **v1 scope**: iOS and Android only; the Flutter Web target is dropped.
- **Not yet available**: `Logs` (the API is a no-op — FlashCat ingest does not accept Logs yet), Session Replay, automatic HTTP/resource tracking (`datadog_tracking_http_client`), the dio/gql/grpc integrations, and Feature Flags.
- **Not yet available**: `Logs` (the API is a no-op — FlashCat ingest does not accept Logs yet), Session Replay, the dio/gql/grpc integrations, and Feature Flags. HTTP/resource tracking is available through the companion `flashcat_tracking_http_client` package.

---

Expand All @@ -27,7 +27,7 @@ This release requires Flutter 3.27+ and supports iOS and Android only.

| iOS SDK | Android SDK |
| :-----: | :---------: |
| 0.5.0 | 0.4.1 |
| 0.6.0 | 0.7.0 |

### iOS

Expand Down Expand Up @@ -62,6 +62,57 @@ final configuration = DatadogConfiguration(

For more information on available configuration options, see the [DatadogConfiguration object][8] documentation.

### Remote RUM configuration

Remote configuration is opt-in and is disabled by default. Enable it when you
want the native Android or iOS SDK to retrieve the RUM session sampling rate
and custom values published for the application:

```dart
rumConfiguration: DatadogRumConfiguration(
applicationId: '<RUM_APPLICATION_ID>',
remoteConfigurationEnabled: true,
beforeSampling: (context) {
final debugUsers = context.custom?['debugUsers'];
if (debugUsers is List && debugUsers.contains(currentUserId)) {
return 100;
}
return null; // Keep the native SDK's sampling rate.
},
)
```

`beforeSampling` runs for every new session even when remote configuration is
disabled. In that case, `sessionSampleRate` is the local value and `custom` is
`null`. Returning `null`, throwing, timing out, or returning a value outside
`0...100` keeps the native SDK's sampling decision.

The current process can be switched permanently to forced collection, and the
latest custom values can be read at runtime:

```dart
DatadogSdk.instance.rum?.setForcedSession();
final custom = await DatadogSdk.instance.rum?.getRemoteConfig();
```

Forced collection cannot be reverted until the process restarts. It reports a
session sample rate of 100 without a remote-configuration version and does not
enable Session Replay in Flutter. `getRemoteConfig()` returns only the
console's `custom` object. Treat it as public information and never store
secrets in it.

The `custom` values from `getRemoteConfig()` and
`RumBeforeSamplingContext.custom` cross the platform channel with the type
fidelity each native SDK provides, so a number published in the console can
arrive as either an `int` or a `double` depending on the platform. Read numbers
as `num` (`(context.custom?['threshold'] as num?)?.toDouble()`) rather than
testing for `int` or `double`.

When attaching Flutter to an already initialized native SDK,
`remoteConfigurationEnabled` and `beforeSampling` must have been configured by
the native application during RUM initialization. The two runtime methods can
still operate on the existing RUM monitor.

### Initialize the library

You can initialize RUM using one of two methods in the `main.dart` file.
Expand Down
7 changes: 7 additions & 0 deletions packages/datadog_flutter_plugin/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ analyzer:
exclude:
- "**/*.mocks.dart"
- "**/*.g.dart"
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
language:
strict-raw-types: true

Expand Down
2 changes: 1 addition & 1 deletion packages/datadog_flutter_plugin/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ version "1.0-SNAPSHOT"

buildscript {
ext.kotlin_version = "2.1.0"
ext.datadog_version = "0.5.0"
ext.datadog_version = "0.7.0"

repositories {
google()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.datadoghq.flutter

import android.util.Log
import com.datadog.android.rum.BeforeSamplingContext
import com.datadog.android.rum.ExperimentalRumApi
import com.datadog.android.rum.RumConfiguration
import com.datadog.android.rum.model.ActionEvent
Expand All @@ -8,6 +10,7 @@ import com.datadog.android.rum.model.LongTaskEvent
import com.datadog.android.rum.model.ResourceEvent
import com.datadog.android.rum.model.RumVitalOperationStepEvent
import com.datadog.android.rum.model.ViewEvent
import com.google.gson.Gson
import com.google.gson.JsonParser

/**
Expand All @@ -29,6 +32,7 @@ class DatadogRumEventMapper {
fun mapErrorEvent(encodedEvent: String): String?
fun mapLongTaskEvent(encodedEvent: String): String?
fun mapVitalOperationStepEvent(encodedEvent: String): String?
fun beforeSampling(encodedContext: String): String?
}

var eventMapper: EventMapper? = null
Expand Down Expand Up @@ -64,10 +68,30 @@ class DatadogRumEventMapper {
}
)
}
if (optionIsSet("attachBeforeSampling")) {
configBuilder.setBeforeSampling { context -> beforeSampling(context) }
}

return configBuilder
}

internal fun beforeSampling(context: BeforeSamplingContext): Float? {
val encodedContext = try {
Gson().toJson(
mapOf(
"sessionSampleRate" to context.sessionSampleRate,
"custom" to context.custom?.sanitizeForFlutter()
)
)
} catch (error: RuntimeException) {
Log.w(DATADOG_FLUTTER_TAG, "Unable to encode beforeSampling context.", error)
return null
}
val encodedResult = eventMapper?.beforeSampling(encodedContext) ?: return null
val result = encodedResult.toFloatOrNull() ?: return null
return result.takeIf { it.isFinite() && it in 0f..100f }
}

internal fun mapViewEvent(event: ViewEvent): ViewEvent {
val result: ViewEvent = event

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ class DatadogRumPlugin : MethodChannel.MethodCallHandler {
when (call.method) {
"deinitialize" -> deinitialize(call, result)
"getCurrentSessionId" -> getCurrentSessionId(call, result)
"setForcedSession" -> setForcedSession(call, result)
"getRemoteConfig" -> getRemoteConfig(call, result)
"startView" -> startView(call, result)
"stopView" -> stopView(call, result)
"addTiming" -> addTiming(call, result)
Expand Down Expand Up @@ -245,6 +247,15 @@ class DatadogRumPlugin : MethodChannel.MethodCallHandler {
}
}

private fun setForcedSession(call: MethodCall, result: Result) {
rum?.setForcedSession()
result.success(null)
}

private fun getRemoteConfig(call: MethodCall, result: Result) {
result.success(rum?.getRemoteConfig()?.sanitizeForFlutter())
}

private fun mapTelemetryConfiguration(
event: TelemetryConfigurationEvent
): TelemetryConfigurationEvent {
Expand Down Expand Up @@ -628,6 +639,9 @@ fun RumConfiguration.Builder.withEncoded(encoded: Map<String, Any?>): RumConfigu
(encoded["sessionSampleRate"] as? Number)?.let {
builder = builder.setSessionSampleRate(it.toFloat())
}
(encoded["remoteConfigurationEnabled"] as? Boolean)?.let {
builder = builder.setRemoteConfigurationEnabled(it)
}
(encoded["longTaskThreshold"] as? Number)?.let {
builder = builder.trackLongTasks((it.toFloat() * 1000).toLong())
}
Expand Down Expand Up @@ -666,6 +680,60 @@ fun RumConfiguration.Builder.withEncoded(encoded: Map<String, Any?>): RumConfigu
return builder
}

internal fun Map<String, Any?>.sanitizeForFlutter(): Map<String, Any?> {
return buildMap {
this@sanitizeForFlutter.forEach { (key, value) ->
val sanitized = sanitizeValueForFlutter(value)
if (sanitized !== UnsupportedFlutterValue) {
put(key, sanitized)
}
}
}
}

private object UnsupportedFlutterValue

private fun sanitizeValueForFlutter(value: Any?): Any? {
return when (value) {
null,
is Boolean,
is String,
is Byte,
is Short,
is Int,
is Long,
is Float,
is Double -> value
is Map<*, *> -> buildMap<String, Any?> {
value.forEach { (key, item) ->
if (key is String) {
val sanitized = sanitizeValueForFlutter(item)
if (sanitized !== UnsupportedFlutterValue) {
put(key, sanitized)
}
} else {
Log.w(DATADOG_FLUTTER_TAG, "Dropping a remote config entry with a non-string key.")
}
}
}
is Iterable<*> -> buildList<Any?> {
value.forEach { item ->
val sanitized = sanitizeValueForFlutter(item)
if (sanitized !== UnsupportedFlutterValue) {
add(sanitized)
}
}
}
else -> {
Log.w(
DATADOG_FLUTTER_TAG,
"Dropping unsupported remote config value of type ${value.javaClass.name}."
)
UnsupportedFlutterValue
}
}
}

fun parseRumHttpMethod(value: String): RumResourceMethod {
return when (value) {
"RumHttpMethod.get" -> RumResourceMethod.GET
Expand Down
Loading