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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// Interfaces which instrumentations should implement.
interface class Instrumentation {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'dart:ui';
import '../../api/attribute.dart';

const _lifecycleSpanName = "device.app.lifecycle";
const _flutterAppState = "flutter.app.state";

enum LifecycleState {
detached('detached'),
resumed('resumed'),
inactive('inactive'),
hidden('hidden'),
paused('paused');

final String stringValue;

const LifecycleState(String value) : stringValue = value;

@override
String toString() {
return stringValue;
}

static LifecycleState fromAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.detached:
return LifecycleState.detached;
case AppLifecycleState.resumed:
return LifecycleState.resumed;
case AppLifecycleState.inactive:
return LifecycleState.inactive;
case AppLifecycleState.hidden:
return LifecycleState.hidden;
case AppLifecycleState.paused:
return LifecycleState.paused;
}
}
}

/// LaunchDarkly specific lifecycle convention inspired by the otel mobile
/// events semantic convention.
final class LifecycleConventions {
static Map<String, Attribute> getAttributes({
required AppLifecycleState state,
}) {
return {
_flutterAppState: StringAttribute(
LifecycleState.fromAppLifecycleState(state).toString(),
),
};
}

/// The name to use for the span.
static const spanName = _lifecycleSpanName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import 'package:flutter/scheduler.dart';
import 'package:launchdarkly_flutter_observability/launchdarkly_flutter_observability.dart';
import 'package:launchdarkly_flutter_observability/src/instrumentation/instrumentation.dart';
import 'package:launchdarkly_flutter_observability/src/instrumentation/lifecycle/lifecycle_conventions.dart';

import '../../observe.dart';
import 'platform/stub_lifecycle_listener.dart'
if (dart.library.io) 'platform/io_lifecycle_listener.dart'
if (dart.library.js_interop) 'platform/js_lifecycle_listener.dart';

final class LifecycleInstrumentation implements Instrumentation {
late final LDAppLifecycleListener _lifecycleListener;

LifecycleInstrumentation() {
final initialState = SchedulerBinding.instance.lifecycleState;
if (initialState != null) {
_handleApplicationLifecycle(initialState);
}

_lifecycleListener = LDAppLifecycleListener();
_lifecycleListener.stream.listen(_handleApplicationLifecycle);
}

/// The application lifecycle is as follows.
/// Diagram based on: https://api.flutter.dev/flutter/widgets/AppLifecycleListener-class.html
/// +-----------+ onStart +-----------+
/// | +---------------------------> |
/// | Detached | | Resumed |
/// | | | |
/// +--------^--+ +-^-------+-+
/// | | |
/// |onDetach onInactive| |onResume
/// | | |
/// | onPause | |
/// +--------+--+ +-----------+onHide +-+-------v-+
/// | <-------+ <-------+ |
/// | Paused | | Hidden | | Inactive |
/// | +-------> +-------> |
/// +-----------+ +-----------+onShow +-----------+
/// onRestart
///
/// On iOS/Android the hidden state is synthesized in the process of pausing,
/// so it will always hide before being paused. On desktop/web platforms
/// hidden may happen when the app is covered.
void _handleApplicationLifecycle(AppLifecycleState state) {
Observe.startSpan(
LifecycleConventions.spanName,
attributes: LifecycleConventions.getAttributes(state: state),
)
..setStatus(SpanStatusCode.ok)
..end();
}

void dispose() {
_lifecycleListener.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'dart:async';

import 'package:flutter/widgets.dart';

/// Lifecycle listener that uses the Flutter [AppLifecycleListener].
/// Unfortunately, the [AppLifecycleListener] does not support web very well at
/// the moment, so the [LDAppLifecycleListener] was created.
class LDAppLifecycleListener {
late final StreamController<AppLifecycleState> _streamController;
AppLifecycleListener? _underlyingListener;

LDAppLifecycleListener() {
_streamController = StreamController.broadcast(
onListen: () {
_underlyingListener = AppLifecycleListener(
onStateChange: (state) => _streamController.add(state),
);
},
onCancel: () {
_underlyingListener?.dispose();
_underlyingListener = null;
},
);
}

Stream<AppLifecycleState> get stream => _streamController.stream;

void close() {
_streamController.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'dart:async';
import 'dart:js_interop';
import 'package:web/web.dart' as web;

import 'package:flutter/widgets.dart';

/// Lifecycle listener that uses the underlying visibility of the html web
/// document to emit events.
class LDAppLifecycleListener {
late final StreamController<AppLifecycleState> _streamController;

LDAppLifecycleListener() {
_streamController = StreamController.broadcast();

void listenerFunc(web.Event event) => _streamController.add(
web.document.hidden == true
? AppLifecycleState.hidden
: AppLifecycleState.resumed,
);

/// Use a stable reference for the JS listener.
final listenerJS = listenerFunc.toJS;

_streamController.onListen = () {
web.document.addEventListener('visibilitychange', listenerJS);
};

_streamController.onCancel = () {
web.document.removeEventListener('visibilitychange', listenerJS);
};
}

Stream<AppLifecycleState> get stream => _streamController.stream;

void close() {
_streamController.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:flutter/widgets.dart';

class LDAppLifecycleListener {
Stream<AppLifecycleState> get stream =>
throw Exception('Stub implementation');

void close() {
throw Exception('Stub implementation');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'dart:collection';

import 'package:launchdarkly_flutter_client_sdk/launchdarkly_flutter_client_sdk.dart';
import 'package:launchdarkly_flutter_observability/src/api/span_status_code.dart';
import 'package:launchdarkly_flutter_observability/src/instrumentation/instrumentation.dart';
import 'package:launchdarkly_flutter_observability/src/instrumentation/lifecycle/lifecycle_instrumentation.dart';
import 'package:launchdarkly_flutter_observability/src/otel/feature_flag_convention.dart';
import 'package:launchdarkly_flutter_observability/src/otel/setup.dart';

Expand Down Expand Up @@ -66,10 +68,15 @@ final class _ObservabilityHook extends Hook {

/// LaunchDarkly Observability plugin.
final class ObservabilityPlugin extends Plugin {
final List<Instrumentation> _instrumentations = [];
final PluginMetadata _metadata = const PluginMetadata(
name: _launchDarklyObservabilityPluginName,
);

ObservabilityPlugin() {
_instrumentations.add(LifecycleInstrumentation());
}

@override
void register(
LDClient client,
Expand Down
Loading