Skip to content

Commit ac9cb12

Browse files
authored
feat: Add basic configuration. (#252)
## Summary Adds basic configuration for flutter. At this point most of the configuration is unused. This gets the configuration in place, so the instrumentation implementations can then build on that implementation. ## How did you test this change? Manual testing. Unit tests. ## Are there any deployment considerations? <!-- Backend - Do we need to consider migrations or backfilling data? --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds a configurable Observability plugin (app name/version, endpoints, context namer), updates OTEL setup to use config and service attributes, adjusts example, and adds tests. > > - **Observability Plugin** (`src/plugin/observability_plugin.dart`) > - Accepts configuration (applicationName, applicationVersion, otlpEndpoint, backendUrl, contextFriendlyName) via new `ObservabilityConfig`; passes config to `setup`. > - **OTEL Setup** (`src/otel/setup.dart`) > - `setup` now takes `ObservabilityConfig`, uses `config.otlpEndpoint` for traces, and adds service attributes via `ServiceConvention`. > - Adds conversions import and traces path suffix. > - **Service Attributes** (`src/otel/service_convention.dart`) > - New helper to generate `service.name` and `service.version` attributes. > - **Configuration** (`src/plugin/observability_config.dart`) > - New config class and `configWithDefaults` with defaults for OTLP and backend URLs and optional app metadata/context namer. > - **Example** (`example/lib/main.dart`) > - Demonstrates `ObservabilityPlugin` configuration with `applicationName` and `applicationVersion` from `GIT_SHA`. > - **Tests** (`test/observability_config_test.dart`) > - Unit tests for config defaults and overrides. > - **Dependencies** (`pubspec.yaml`) > - Adds `web` dependency. > - **Misc** > - Minor import refactors in `lifecycle_instrumentation.dart`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0f7e4fd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 1496df7 commit ac9cb12

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+
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)