Skip to content

Commit 773a7fd

Browse files
committed
feat: Add basic configuration.
1 parent 1496df7 commit 773a7fd

File tree

8 files changed

+240
-12
lines changed

8 files changed

+240
-12
lines changed

sdk/@launchdarkly/launchdarkly_flutter_observability/example/lib/main.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import 'dart:async';
2-
import 'dart:ui';
32

43
import 'package:flutter/material.dart';
54
import 'package:launchdarkly_flutter_client_sdk/launchdarkly_flutter_client_sdk.dart';
@@ -27,7 +26,18 @@ void main() {
2726
// If using android studio the `additional run args` option can include the correct --dart-define.
2827
CredentialSource.fromEnvironment(),
2928
AutoEnvAttributes.enabled,
30-
plugins: [ObservabilityPlugin()],
29+
plugins: [
30+
ObservabilityPlugin(
31+
applicationName: 'test-application',
32+
// This could be a semantic version or a git commit hash.
33+
// This demonstrates how to use an environment variable to set the hash.
34+
// flutter build --dart-define GIT_SHA=$(git rev-parse HEAD) --dart-define LAUNCHDARKLY_MOBILE_KEY=<my-mobile-key>
35+
applicationVersion: const String.fromEnvironment(
36+
'GIT_SHA',
37+
defaultValue: 'no-version',
38+
),
39+
),
40+
],
3141
),
3242
LDContextBuilder().kind('user', 'bob').build(),
3343
);

sdk/@launchdarkly/launchdarkly_flutter_observability/lib/src/instrumentation/lifecycle/lifecycle_instrumentation.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import 'package:flutter/scheduler.dart';
2-
import 'package:launchdarkly_flutter_observability/launchdarkly_flutter_observability.dart';
3-
import 'package:launchdarkly_flutter_observability/src/instrumentation/instrumentation.dart';
4-
import 'package:launchdarkly_flutter_observability/src/instrumentation/lifecycle/lifecycle_conventions.dart';
52

3+
import '../../api/span_status_code.dart';
64
import '../../observe.dart';
5+
import '../instrumentation.dart';
6+
import './lifecycle_conventions.dart';
7+
78
import 'platform/stub_lifecycle_listener.dart'
89
if (dart.library.io) 'platform/io_lifecycle_listener.dart'
910
if (dart.library.js_interop) 'platform/js_lifecycle_listener.dart';
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import 'package:launchdarkly_flutter_observability/src/api/attribute.dart';
2+
3+
const _attributeServiceName = 'service.name';
4+
const _attributeServiceVersion = 'service.version';
5+
6+
class ServiceConvention {
7+
static Map<String, Attribute> getAttributes({
8+
String? serviceName,
9+
String? serviceVersion,
10+
}) {
11+
final attributes = <String, Attribute>{};
12+
if (serviceName != null) {
13+
attributes[_attributeServiceName] = StringAttribute(serviceName);
14+
}
15+
if (serviceVersion != null) {
16+
attributes[_attributeServiceVersion] = StringAttribute(serviceVersion);
17+
}
18+
return attributes;
19+
}
20+
}

sdk/@launchdarkly/launchdarkly_flutter_observability/lib/src/otel/setup.dart

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
1+
import 'package:launchdarkly_flutter_observability/src/otel/conversions.dart';
2+
import 'package:launchdarkly_flutter_observability/src/otel/service_convention.dart';
3+
import 'package:launchdarkly_flutter_observability/src/plugin/observability_config.dart';
14
import 'package:opentelemetry/api.dart'
25
show registerGlobalTracerProvider, Attribute;
36
import 'package:opentelemetry/sdk.dart'
47
show BatchSpanProcessor, CollectorExporter, TracerProviderBase, Resource;
58

69
const _highlightProjectIdAttr = 'highlight.project_id';
7-
const _defaultOtlpEndpoint =
8-
'https://otel.observability.app.launchdarkly.com:4318';
9-
const _defaultOtlpTracesEndpoint = '$_defaultOtlpEndpoint/v1/traces';
10+
const _tracesSuffix = '/v1/traces';
1011

11-
void setup(String sdkKey) {
12+
void setup(String sdkKey, ObservabilityConfig config) {
1213
final resourceAttributes = <Attribute>[
1314
Attribute.fromString(_highlightProjectIdAttr, sdkKey),
1415
];
16+
resourceAttributes.addAll(
17+
convertAttributes(
18+
ServiceConvention.getAttributes(
19+
serviceName: config.applicationName,
20+
serviceVersion: config.applicationVersion,
21+
),
22+
),
23+
);
1524
final tracerProvider = TracerProviderBase(
1625
processors: [
1726
BatchSpanProcessor(
18-
CollectorExporter(Uri.parse(_defaultOtlpTracesEndpoint)),
27+
CollectorExporter(Uri.parse('${config.otlpEndpoint}$_tracesSuffix')),
1928
),
2029
],
2130
resource: Resource(resourceAttributes),
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'package:launchdarkly_flutter_client_sdk/launchdarkly_flutter_client_sdk.dart';
2+
3+
const _defaultOtlpEndpoint =
4+
'https://otel.observability.app.launchdarkly.com:4318';
5+
6+
const _defaultBackendUrl = 'https://pub.observability.app.launchdarkly.com';
7+
8+
// Implementation note: The final values with defaults should be included
9+
// in the configuration. This centralizes the assignment of defaults versus
10+
// having them in each location that requires them.
11+
12+
final class ObservabilityConfig {
13+
/// The configured OTLP endpoint.
14+
final String? otlpEndpoint;
15+
16+
/// The configured back-end URL.
17+
final String? backendUrl;
18+
19+
/// The name of the application.
20+
final String? applicationName;
21+
22+
/// The version of the application.
23+
///
24+
/// This is commonly a Git hash or a semantic version.
25+
final String? applicationVersion;
26+
27+
/// Function for mapping context to a friendly name for use in the
28+
/// observability UI.
29+
final String? Function(LDContext context)? contextFriendlyName;
30+
31+
ObservabilityConfig({
32+
this.applicationName,
33+
this.applicationVersion,
34+
required this.otlpEndpoint,
35+
required this.backendUrl,
36+
required this.contextFriendlyName,
37+
});
38+
}
39+
40+
ObservabilityConfig configWithDefaults({
41+
String? applicationName,
42+
String? applicationVersion,
43+
String? otlpEndpoint,
44+
String? backendUrl,
45+
String? Function(LDContext context)? contextFriendlyName,
46+
}) {
47+
return ObservabilityConfig(
48+
applicationName: applicationName,
49+
applicationVersion: applicationVersion,
50+
otlpEndpoint: otlpEndpoint ?? _defaultOtlpEndpoint,
51+
backendUrl: backendUrl ?? _defaultBackendUrl,
52+
contextFriendlyName: contextFriendlyName,
53+
);
54+
}

sdk/@launchdarkly/launchdarkly_flutter_observability/lib/src/plugin/observability_plugin.dart

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:launchdarkly_flutter_observability/src/instrumentation/instrumen
66
import 'package:launchdarkly_flutter_observability/src/instrumentation/lifecycle/lifecycle_instrumentation.dart';
77
import 'package:launchdarkly_flutter_observability/src/otel/feature_flag_convention.dart';
88
import 'package:launchdarkly_flutter_observability/src/otel/setup.dart';
9+
import 'package:launchdarkly_flutter_observability/src/plugin/observability_config.dart';
910

1011
import '../api/span.dart';
1112
import '../observe.dart';
@@ -73,7 +74,44 @@ final class ObservabilityPlugin extends Plugin {
7374
name: _launchDarklyObservabilityPluginName,
7475
);
7576

76-
ObservabilityPlugin() {
77+
final ObservabilityConfig _config;
78+
79+
/// Construct an observability plugin with the given configuration.
80+
///
81+
/// [applicationName] The name of the application.
82+
/// [applicationVersion] The version of the application. This is commonly a
83+
/// git SHA or semantic version.
84+
/// [otlpEndpoint] The OTLP endpoint for reporting OpenTelemetry data. This
85+
/// setting does not need to be used in most configurations.
86+
/// [backendUrl] The back-end URL. This setting does not need to be used in
87+
/// most configurations.
88+
/// [contextFriendlyName] A function that returns a friendly name for a given
89+
/// context. This name will be used to identify the session in the
90+
/// observability UI.
91+
/// ```dart
92+
/// ObservabilityPlugin(contextFriendlyName: (LDContext context) {
93+
/// // If there is a user context with an email, then use that email.
94+
/// final email = context.get('user', AttributeReference('email'));
95+
/// if(email.stringValue().isNotEmpty) {
96+
/// return email.stringValue();
97+
/// }
98+
/// // If there is no email, then use the default name.
99+
/// return null;
100+
/// })
101+
/// ```
102+
ObservabilityPlugin({
103+
String? applicationName,
104+
String? applicationVersion,
105+
String? otlpEndpoint,
106+
String? backendUrl,
107+
String? Function(LDContext context)? contextFriendlyName,
108+
}) : _config = configWithDefaults(
109+
applicationName: applicationName,
110+
applicationVersion: applicationVersion,
111+
otlpEndpoint: otlpEndpoint,
112+
backendUrl: backendUrl,
113+
contextFriendlyName: contextFriendlyName,
114+
) {
77115
_instrumentations.add(LifecycleInstrumentation());
78116
}
79117

@@ -82,7 +120,7 @@ final class ObservabilityPlugin extends Plugin {
82120
LDClient client,
83121
PluginEnvironmentMetadata environmentMetadata,
84122
) {
85-
setup(environmentMetadata.credential.value);
123+
setup(environmentMetadata.credential.value, _config);
86124
super.register(client, environmentMetadata);
87125
}
88126

sdk/@launchdarkly/launchdarkly_flutter_observability/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ dependencies:
1515
opentelemetry: 0.18.10
1616

1717
launchdarkly_flutter_client_sdk: ^4.12.0
18+
web: ^1.1.1
1819

1920
dev_dependencies:
2021
flutter_test:
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:launchdarkly_flutter_client_sdk/launchdarkly_flutter_client_sdk.dart';
3+
import 'package:launchdarkly_flutter_observability/src/plugin/observability_config.dart';
4+
5+
void main() {
6+
group('configWithDefaults', () {
7+
test('uses default OTLP endpoint when not provided', () {
8+
final config = configWithDefaults();
9+
10+
expect(
11+
config.otlpEndpoint,
12+
'https://otel.observability.app.launchdarkly.com:4318',
13+
);
14+
});
15+
16+
test('uses default backend URL when not provided', () {
17+
final config = configWithDefaults();
18+
19+
expect(
20+
config.backendUrl,
21+
'https://pub.observability.app.launchdarkly.com',
22+
);
23+
});
24+
25+
test('uses custom OTLP endpoint when provided', () {
26+
final config = configWithDefaults(
27+
otlpEndpoint: 'https://custom-otel.example.com:4318',
28+
);
29+
30+
expect(config.otlpEndpoint, 'https://custom-otel.example.com:4318');
31+
});
32+
33+
test('uses custom backend URL when provided', () {
34+
final config = configWithDefaults(
35+
backendUrl: 'https://custom-backend.example.com',
36+
);
37+
38+
expect(config.backendUrl, 'https://custom-backend.example.com');
39+
});
40+
41+
test('sets application name when provided', () {
42+
final config = configWithDefaults(applicationName: 'MyApp');
43+
44+
expect(config.applicationName, 'MyApp');
45+
});
46+
47+
test('sets application version when provided', () {
48+
final config = configWithDefaults(applicationVersion: '1.2.3');
49+
50+
expect(config.applicationVersion, '1.2.3');
51+
});
52+
53+
test('sets context friendly name function when provided', () {
54+
String? friendlyNameFunc(LDContext context) => 'TestUser';
55+
56+
final config = configWithDefaults(contextFriendlyName: friendlyNameFunc);
57+
58+
expect(config.contextFriendlyName, friendlyNameFunc);
59+
});
60+
61+
test('leaves application name null when not provided', () {
62+
final config = configWithDefaults();
63+
64+
expect(config.applicationName, isNull);
65+
});
66+
67+
test('leaves application version null when not provided', () {
68+
final config = configWithDefaults();
69+
70+
expect(config.applicationVersion, isNull);
71+
});
72+
73+
test('leaves context friendly name null when not provided', () {
74+
final config = configWithDefaults();
75+
76+
expect(config.contextFriendlyName, isNull);
77+
});
78+
79+
test('combines custom and default values', () {
80+
final config = configWithDefaults(
81+
applicationName: 'MyApp',
82+
applicationVersion: '1.0.0',
83+
backendUrl: 'https://custom.example.com',
84+
);
85+
86+
expect(config.applicationName, 'MyApp');
87+
expect(config.applicationVersion, '1.0.0');
88+
expect(config.backendUrl, 'https://custom.example.com');
89+
expect(
90+
config.otlpEndpoint,
91+
'https://otel.observability.app.launchdarkly.com:4318',
92+
);
93+
});
94+
});
95+
}

0 commit comments

Comments
 (0)