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
2 changes: 2 additions & 0 deletions sdk/flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:launchdarkly_flutter_observability/launchdarkly_flutter_observab
import 'click_scenarios.dart';
import 'getx_app.dart';
import 'my_app.dart';
import 'repro.dart';

class LDSingleton {
static LDClient? client;
Expand Down Expand Up @@ -61,6 +62,7 @@ void main() {
// Mirrors the two `LDObserve.Init(...)` variants in
// sdk/@launchdarkly/mobile-dotnet/sample/MauiProgram.cs.
_startObservability();
applyGlobalAttributes();

// Report any errors handled by flutter.
FlutterError.onError = (FlutterErrorDetails details) {
Expand Down
4 changes: 4 additions & 0 deletions sdk/flutter/example/lib/my_home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'getx_app.dart' show useGetX;
import 'getx_scenarios.dart';
import 'main.dart';
import 'nested_masking_propagation_page.dart';
import 'repro.dart';
import 'smoothie_list_page.dart';
import 'web_view_page.dart';

Expand Down Expand Up @@ -242,9 +243,11 @@ class _MyHomePageState extends State<MyHomePage> {
'test-integer': 42,
'test-double': 3.14,
'test-array': <double>[3.14, 6.28],
'utm_source': 'per_call',
},
);
debugPrint('Log triggered');
printLogExpectation();
}

// The MAUI sample captures the span context inside a span and passes it to
Expand Down Expand Up @@ -325,6 +328,7 @@ class _MyHomePageState extends State<MyHomePage> {
span.addEvent('download.completed');
span.end();
debugPrint('Custom span sent: $spanName');
printSpanExpectation();
}

void _onStartPolling() {
Expand Down
24 changes: 24 additions & 0 deletions sdk/flutter/example/lib/repro.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:flutter/foundation.dart';
import 'package:launchdarkly_flutter_observability/launchdarkly_flutter_observability.dart';

// Repro #1: global attributes (e.g. UTM params) on all telemetry.
//
// [applyGlobalAttributes] runs at startup right after `LDObserve.init` is
// called, before observability has finished booting, so these attributes are
// set before init. The "Send custom span" and "Trigger Log" buttons print what
// to look for on the resulting rows in LaunchDarkly.

const globalAttributes = <String, Object?>{
'utm_campaign': 'spring_promo',
'utm_source': 'email',
};

void applyGlobalAttributes() => LDObserve.setAttributes(globalAttributes);

void printSpanExpectation() =>
debugPrint('[#1] expect utm_campaign=spring_promo on this row');

void printLogExpectation() {
debugPrint('[#1] expect utm_campaign=spring_promo on this row');
debugPrint('[#1] expect utm_source=per_call (per-call wins over global)');
}
24 changes: 24 additions & 0 deletions sdk/flutter/packages/observability/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ Limitations worth knowing:
| `LDObserve.trackScreenView(name, {screenClass, screenId, category, properties})` | Record a screen view (navigation) as a `screen_view` span and a Session Replay `Navigate` event. Prefer `LDNavigatorObserver` for ordinary route changes. |
| `LDObserve.trackClick({id, tag, text, x, y, properties})` | Record a click as a `click` span and a Session Replay `Click` event, for interactions automatic capture cannot observe. `x`/`y` are `double` logical pixels. |
| `LDObserve.shutdown()` | Returns a `Future<void>`. Flush buffered spans, remove the Dart instrumentations, stop session replay, and turn every recording API into a no-op. Terminal: a later `init` does nothing. On Android and iOS the native SDK's automatic instrumentation (crashes, network, launch times) keeps running, because it has no teardown. |
| `LDObserve.setAttributes(map)` | Merge [global attributes](#global-attributes) attached to every span, log, and event recorded afterwards. |
| `LDObserve.removeAttributes(keys)` | Remove global attributes by key. |
Comment thread
abelonogov-ld marked this conversation as resolved.
Resolved
| `LDObserve.zoneSpecification()` | A zone spec that forwards `print`/`debugPrint` output as logs. |
| `LDNavigatorObserver({screenNameExtractor, category})` | A `NavigatorObserver` that reports each route change as a screen view. |
| `LDRoutePatterns.extractor(patterns, {skipUnmatched})` | A `screenNameExtractor` that reports the route pattern a navigation matched, so `/orders/42` becomes `/orders/:id`. |
Expand Down Expand Up @@ -487,6 +489,28 @@ span.setAttributes(<String, Object?>{

Methods that accept `properties` (`recordLog`, `recordException`, `startSpan`, `track`, `trackScreenView`, `trackClick`) take a `Map<String, Object?>` of these same plain values.

### Global attributes

`LDObserve.setAttributes` attaches attributes to everything recorded from then on: spans (manual and automatic, including flag evaluations, errors, and app lifecycle), logs, screen views, clicks, and track events. Calls merge by key, so a later call replaces only the keys it names. `LDObserve.removeAttributes` removes keys.

```dart
LDObserve.setAttributes(<String, Object?>{
'tenant.id': 'acme',
'app.flavor': 'beta',
});

// Later, for example on sign-out:
LDObserve.removeAttributes(['tenant.id']);
```

- **Per-call values win.** A key passed in `properties`, or set on a span with `setAttribute`, keeps its own value; a global attribute only fills keys that are not already set. Global attributes in turn take precedence over the `ObservabilityOptions.attributes` resource attributes.
- **A span gets the global attributes current when it starts.**
- **Safe before init.** Attributes set before `LDObserve.init` completes apply once observability is running.
- **In memory only.** They last for the life of the process, are kept across session changes, and are never persisted.
- **Session keys are reserved.** `session.id` and `highlight.session_id` are managed by the SDK and ignored here, as are values that cannot be attributes.

On iOS and Android, telemetry the native SDK records on its own (crash reports, launch times, native lifecycle spans, and metrics) does not carry global attributes. The Flutter SDK has no Dart metrics API, so there are no Dart metrics for them to apply to.

## Session replay

Session Replay captures screen recordings to help you understand how users interact with your application. Enable it by passing `SessionReplayOptions` to `LDObserve.init` / `LDObserve.initStandalone`, and wrap the part of your app you want recorded in a `SessionReplayCapture` widget:
Expand Down
28 changes: 28 additions & 0 deletions sdk/flutter/packages/observability/lib/src/ld_observe.dart
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,34 @@ final class LDObserve {
attributes: attributesFromProperties(properties),
);

/// Merge [attributes] into the global attributes attached to every span,
/// log and event recorded from now on, including automatic ones (flag
/// evaluations, errors, lifecycle, screen views, clicks, track events).
///
/// Keys merge with those already set; a later call replaces a key's value.
/// Attributes passed to an individual call, or set on a span, win over a
/// global attribute with the same key. A span takes the global attributes
/// current when it starts.
///
/// Values follow the same rules as `properties`: strings, numbers, booleans,
/// or homogeneous lists of those; anything else (including `null`) is
/// ignored. The SDK-managed session keys `session.id` and
/// `highlight.session_id` are ignored.
///
/// Safe to call before [init]: the attributes apply once observability is
/// running. They are kept in memory for the life of the process, survive
/// session changes, and are never persisted.
///
/// On iOS and Android, telemetry the native SDK records on its own (crashes,
/// launch times, native lifecycle, metrics) does not carry them.
static void setAttributes(Map<String, Object?> attributes) =>
ObserveOtel.setAttributes(attributes);

/// Remove [keys] from the global attributes set with [setAttributes].
/// Keys that are not set are ignored.
static void removeAttributes(List<String> keys) =>
ObserveOtel.removeAttributes(keys);

/// Get a zone specification which intercepts print statements.
static ZoneSpecification zoneSpecification() =>
ObserveOtel.zoneSpecification();
Expand Down
10 changes: 10 additions & 0 deletions sdk/flutter/packages/observability/lib/src/observe_otel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'api/span.dart';
import 'api/span_kind.dart';
import 'api/span_status_code.dart';
import 'otel/conversions.dart';
import 'otel/global_attributes.dart';
import 'otel/setup.dart';
import 'platform/ld_observe_platform.dart';
import 'plugin/ld_observe_plugin.dart';
Expand Down Expand Up @@ -268,6 +269,15 @@ final class ObserveOtel {
);
}

/// Merges [properties] into the global attributes applied to every span,
/// log and event recorded afterwards.
static void setAttributes(Map<String, Object?> properties) =>
GlobalAttributes.set(properties);

/// Removes [keys] from the global attributes.
static void removeAttributes(List<String> keys) =>
GlobalAttributes.remove(keys);

/// Whether [shutdown] has run. Terminal: a later boot does nothing.
static bool get isShutdown => _shutdown;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import '../../api/log_severity.dart';
import '../../plugin/observability_config.dart';
import '../../platform/io/messages.g.dart' as wire;
import '../conversions.dart';
import '../global_attributes.dart';
import 'exporter_factory.dart';

/// Creates the native (iOS/Android) implementation of [ObservabilityExporters].
Expand Down Expand Up @@ -89,7 +90,7 @@ class _NativeClickRecorder implements ClickRecorder {
x,
y,
timestampMillis,
properties,
GlobalAttributes.fillProperties(properties),
),
);
}
Expand Down Expand Up @@ -118,7 +119,13 @@ class _NativeScreenViewRecorder implements ScreenViewRecorder {
Map<String, Object?>? properties,
}) {
unawaited(
_api.trackScreenView(name, screenClass, screenId, category, properties),
_api.trackScreenView(
name,
screenClass,
screenId,
category,
GlobalAttributes.fillProperties(properties),
),
);
}
}
Expand Down Expand Up @@ -172,7 +179,9 @@ class _NativeTrackRecorder implements TrackRecorder {
unawaited(
_api.track(
eventName,
json is Map ? Map<String, Object?>.from(json) : null,
GlobalAttributes.fillProperties(
json is Map ? Map<String, Object?>.from(json) : null,
),
metricValue?.toDouble(),
contextKeys,
),
Expand Down Expand Up @@ -274,7 +283,7 @@ class _NativeLogRecorder implements LogRecorder {
Map<String, Attribute>? attributes,
}) {
final attrs = <String, Object?>{};
attributes?.forEach((key, attribute) {
GlobalAttributes.fillAttributes(attributes)?.forEach((key, attribute) {
final converted = convertAttribute(key, attribute);
if (converted != null) {
attrs[key] = converted.value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import '../../api/log_severity.dart';
import '../../plugin/observability_config.dart';
import '../click_convention.dart';
import '../conversions.dart';
import '../global_attributes.dart';
import '../log_convention.dart';
import '../screen_view_convention.dart';
import '../track_convention.dart';
Expand Down Expand Up @@ -204,7 +205,9 @@ class _SpanEventLogRecorder implements LogRecorder {
final span = tracer.startSpan(LogConvention.spanName);
span.addEvent(
LogConvention.eventName,
attributes: convertAttributes(combinedAttributes),
attributes: convertAttributes(
GlobalAttributes.fillAttributes(combinedAttributes)!,
),
);
span.end();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import '../../api/log_severity.dart';
import '../../plugin/observability_config.dart';
import '../click_convention.dart';
import '../conversions.dart';
import '../global_attributes.dart';
import '../log_convention.dart';
import '../screen_view_convention.dart';
import '../track_convention.dart';
Expand Down Expand Up @@ -208,7 +209,9 @@ class _SpanEventLogRecorder implements LogRecorder {
final span = tracer.startSpan(LogConvention.spanName);
span.addEvent(
LogConvention.eventName,
attributes: convertAttributes(combinedAttributes),
attributes: convertAttributes(
GlobalAttributes.fillAttributes(combinedAttributes)!,
),
);
span.end();
}
Expand Down
Loading
Loading