diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index c6e39fd..68610be 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -51,9 +51,12 @@ jobs: - name: Run tests with coverage run: dart pub global run coverage:test_with_coverage - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: - file: coverage/lcov.info + # Tokenless uploads are rejected on protected branches + # ("Token required because branch is protected"). + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage/lcov.info fail_ci_if_error: false pana: diff --git a/CHANGELOG.md b/CHANGELOG.md index 3543248..4465ee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,41 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.0-beta.11-wip] +## [1.0.0-rc.1-wip] + +### Removed +- **Breaking: the deprecated vendor/RUM enums are removed** (deprecated + with notice in 1.0.0-beta.10): `AppLifecycleStates`, + `AppLifecycleSemantics`, `AppStartType`, `AppInfoSemantics`, + `DeviceSemantics`, `BatterySemantics`, `NavigationSemantics`, + `InteractionType`, `InteractionSemantics`, `PerformanceSemantics`, + `ErrorSemantics`, `NetworkSemantics`, `RumSessionView`, + `NavigationAction`, and `LifecycleState` (`semantics/rum.dart` is + gone). They are not OpenTelemetry semantic conventions and so do not + belong in this package; `flutterrific_opentelemetry` defines its own + Flutter conventions for what the registry does not yet cover. The API + package now contains only registry conventions. + +### Added +- **Semantic-conventions versioning policy** in VERSIONING.md: within a + major version, registry regenerations are additive and deprecating + only; identifier- or wire-affecting registry changes batch into the + next major; spec-fidelity string corrections are bug fixes with a + CHANGELOG wire-format table. + +### Changed +- **Breaking: `APIObservableResult` is now an abstract interface.** The + API-side implementation was unconstructible (private constructor, no + factory) so nothing could have used it; SDKs implement the interface, + as `dartastic_opentelemetry` already does. + +### Fixed +- `Attributes.of` no longer throws a `TypeError` when a map value is an + untyped list (`List` / `List`). Lists are now + element-checked like `Attributes.fromJson`: homogeneous + string/bool/int/double lists convert, mixed numeric lists promote to + `List`, and unsupported element types are warned and ignored + per the OTel specification. ## [1.0.0-beta.10] - 2026-07-18 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4de5388..ceaa1dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,8 @@ Thank you for your interest in contributing to the OpenTelemetry API for Dart! This document provides guidelines and instructions for contributing to this project. +Contributors should join the `#otel-dart` channel on the [CNCF Slack](https://slack.cncf.io/) — it's where Dart OpenTelemetry development is discussed and the fastest way to reach the maintainers. + ## Code of Conduct This project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers. @@ -189,6 +191,6 @@ By contributing to this project, you agree that your contributions will be licen ## Questions? -If you have any questions about contributing, please open an issue or contact the project maintainers directly. +If you have any questions about contributing, ask in the `#otel-dart` channel on the [CNCF Slack](https://slack.cncf.io/), open an issue, or contact the project maintainers directly. Thank you for contributing to the OpenTelemetry API for Dart! diff --git a/README.md b/README.md index ee5088e..6502ea4 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,8 @@ void main() { tracer.withSpan(childSpan, () { childSpan.setIntAttribute(ExampleAttribute.operationValue.key, 42); }); - childSpan.setStatus(SpanStatusCode.Ok); + // No setStatus(SpanStatusCode.Ok) here: Ok is the default, so + // only Error needs to be set explicitly. } catch (e, stackTrace) { // Per the OTel spec: recordException first, then setStatus(Error). childSpan.recordException(e, stackTrace: stackTrace); @@ -209,7 +210,6 @@ void main() { childSpan.end(); } }); - rootSpan.setStatus(SpanStatusCode.Ok); } catch (e, stackTrace) { rootSpan.recordException(e, stackTrace: stackTrace); rootSpan.setStatus(SpanStatusCode.Error, e.toString()); diff --git a/VERSIONING.md b/VERSIONING.md index d181441..176b789 100644 --- a/VERSIONING.md +++ b/VERSIONING.md @@ -53,6 +53,34 @@ This package aims to align with the OpenTelemetry specification: - We track the specification version we implement in our documentation - Critical specification changes may necessitate breaking changes +## Semantic Conventions Versioning + +The semantic-convention enums under `lib/src/api/semantics/semconv/` are +generated from a pinned OpenTelemetry semantic-conventions registry with +OTel Weaver (`tool/semconv/generate.sh`; verify freshness with +`--check`). The pinned registry version and commit are recorded in +`SemconvRegistry` and in every generated file header, and every registry +bump is documented in the CHANGELOG. + +Within a major version, registry regenerations are **additive and +deprecating only**: + +- New namespaces, attributes, enum members, metrics, events, and + entities may be added (a MINOR change). +- Attributes the registry deprecates gain `@Deprecated` but remain. +- Registry changes that would rename or remove generated Dart + identifiers, or change emitted key/value strings, are **held back and + batched into the next MAJOR release**. +- Exception: spec-fidelity corrections — where a generated member + emitted a string that did not match the registry — are bug fixes and + may land in a MINOR release with a prominent wire-format table in the + CHANGELOG (precedent: 1.0.0-beta.10). + +Only registry conventions (plus their spec-mandated deprecation +history) appear in this package. Conventions the registry does not yet +cover — such as Flutter RUM semantics — are defined by downstream +packages (e.g. `flutterrific_opentelemetry`). + ## Long-Term Support (LTS) - No formal LTS versions currently exist for this pre-1.0 package diff --git a/example/dartastic_opentelemetry_api_example.dart b/example/dartastic_opentelemetry_api_example.dart index c58812b..a57c002 100644 --- a/example/dartastic_opentelemetry_api_example.dart +++ b/example/dartastic_opentelemetry_api_example.dart @@ -112,8 +112,8 @@ void main() { [OTelAPI.attributeString(ExampleAttribute.eventFoo.key, 'bar')]), ); }); - // Capitalized Ok to match the OTel spec. - span.setStatus(SpanStatusCode.Ok); + // No setStatus(SpanStatusCode.Ok) here: Ok is the default, so only + // Error needs to be set explicitly. } catch (e, stackTrace) { span.recordException(e, stackTrace: stackTrace); span.setStatus(SpanStatusCode.Error, e.toString()); diff --git a/lib/dartastic_opentelemetry_api.dart b/lib/dartastic_opentelemetry_api.dart index 5642aca..0771ce5 100644 --- a/lib/dartastic_opentelemetry_api.dart +++ b/lib/dartastic_opentelemetry_api.dart @@ -55,7 +55,6 @@ export 'src/api/metrics/up_down_counter.dart' hide UpDownCounterCreate; export 'src/api/otel_api.dart'; // Semantics export 'src/api/semantics/http_header_attribute.dart'; -export 'src/api/semantics/rum.dart'; export 'src/api/semantics/semantics_base.dart'; export 'src/api/semantics/semconv/semconv.dart'; // Trace diff --git a/lib/src/api/common/attribute.dart b/lib/src/api/common/attribute.dart index fcabbf5..4a96fa1 100644 --- a/lib/src/api/common/attribute.dart +++ b/lib/src/api/common/attribute.dart @@ -31,9 +31,9 @@ class Attribute { if (valueList.isEmpty) { throw ArgumentError('Attribute _value list must not be empty'); } - if (valueList.contains(null)) { - throw ArgumentError('null is not allowed in List attribute values.'); - } + // No null-element guard: attributes are only created with + // non-nullable element types (List, List, List, + // List), so sound null safety already rules nulls out. } } diff --git a/lib/src/api/factory/otel_api_factory.dart b/lib/src/api/factory/otel_api_factory.dart index 5e830d6..2441ef7 100644 --- a/lib/src/api/factory/otel_api_factory.dart +++ b/lib/src/api/factory/otel_api_factory.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import '../../factory/otel_factory.dart'; +import '../../util/otel_log.dart'; import '../baggage/baggage.dart'; import '../baggage/baggage_entry.dart'; import '../common/attribute.dart'; @@ -185,19 +186,29 @@ class OTelAPIFactory extends OTelFactory { } else if (value is Attribute) { attributes.add(value); } else if (value is List) { + // Element-check rather than hard-cast: the static list type is + // often List or List (e.g. from map literals), + // which `as List` would reject at runtime. if (value.isNotEmpty) { - if (value.first is String) { + if (value.every((e) => e is String)) { attributes.add(AttributeCreate.create>( - key, value as List)); - } else if (value.first is bool) { + key, value.cast())); + } else if (value.every((e) => e is bool)) { attributes.add( - AttributeCreate.create>(key, value as List)); - } else if (value.first is int) { - attributes.add( - AttributeCreate.create>(key, value as List)); - } else if (value.first is double) { + AttributeCreate.create>(key, value.cast())); + } else if (value.every((e) => e is int)) { + attributes + .add(AttributeCreate.create>(key, value.cast())); + } else if (value.every((e) => e is double || e is int)) { + // Mixed numeric lists are promoted to double. attributes.add(AttributeCreate.create>( - key, value as List)); + key, + value + .map((e) => e is int ? e.toDouble() : e as double) + .toList())); + } else { + OTelLog.warn( + 'Ignoring attribute $key because the list contains unsupported types. Only String, bool, int, double lists are allowed by the OTel specification.'); } } } else { diff --git a/lib/src/api/metrics/meter_provider.dart b/lib/src/api/metrics/meter_provider.dart index 84a6bf4..f8df0ae 100644 --- a/lib/src/api/metrics/meter_provider.dart +++ b/lib/src/api/metrics/meter_provider.dart @@ -151,21 +151,16 @@ class APIMeterProvider { return true; // Already shut down } - try { - // Mark as shut down immediately to prevent new meters - _isShutdown = true; + // Mark as shut down immediately to prevent new meters + _isShutdown = true; - // Clear the meter cache - _meterCache.clear(); + // Clear the meter cache + _meterCache.clear(); - // Disable the provider - _enabled = false; + // Disable the provider + _enabled = false; - return true; - } catch (e) { - OTelLog.error('Error during MeterProvider shutdown: $e'); - return false; - } + return true; } /// Forces the MeterProvider to flush all pending metrics to exporters. @@ -177,12 +172,7 @@ class APIMeterProvider { return false; // Already shut down } - try { - // In the API implementation, this is a no-op - return true; - } catch (e) { - OTelLog.error('Error during MeterProvider forceFlush: $e'); - return false; - } + // In the API implementation, this is a no-op + return true; } } diff --git a/lib/src/api/metrics/observable_result.dart b/lib/src/api/metrics/observable_result.dart index 7c1625c..81565cc 100644 --- a/lib/src/api/metrics/observable_result.dart +++ b/lib/src/api/metrics/observable_result.dart @@ -1,29 +1,21 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -import '../../factory/otel_factory.dart'; import '../common/attributes.dart'; import 'measurement.dart'; /// Interface for recording observations from observable instruments. -class APIObservableResult { - List>? _measurements; - - APIObservableResult._(this._measurements); - +/// +/// Implementations are provided by SDKs (the previous API-side +/// implementation had a private constructor and no factory, so it was +/// unconstructible). +abstract class APIObservableResult { /// Records a measurement with the given value and attributes. - void observe(T value, [Attributes? attributes]) { - _measurements ??= []; - _measurements! - .add(OTelFactory.otelFactory!.createMeasurement(value, attributes)); - } + void observe(T value, [Attributes? attributes]); /// Records a measurement with the given value and attributes as a map. - void observeWithMap(T value, Map attributes) { - observe(value, attributes.toAttributes()); - } + void observeWithMap(T value, Map attributes); /// Get all recorded measurements - List> get measurements => - List.unmodifiable(_measurements ?? []); + List> get measurements; } diff --git a/lib/src/api/semantics/rum.dart b/lib/src/api/semantics/rum.dart deleted file mode 100644 index 778d509..0000000 --- a/lib/src/api/semantics/rum.dart +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -/// Vendor/RUM (Real User Monitoring) conventions that are NOT part of the -/// OpenTelemetry semantic-conventions registry. -/// -/// Everything in this file is deprecated and will be removed from -/// `dartastic_opentelemetry_api`; the future home for these conventions is -/// the Flutter RUM layer, `flutterrific_opentelemetry`. -library; - -import 'semantics_base.dart'; - -/// Defines the UI lifecycle, values for AppLifecycleSemantics.lifecycleState -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum AppLifecycleStates implements OTelSemantic { - active('device.app.lifecycle.active'), - resumed('device.app.lifecycle.resumed'), - detached('device.app.lifecycle.detached'), - inactive('device.app.lifecycle.inactive'), - hidden('device.app.lifecycle.hidden'), - paused('device.app.lifecycle.paused'); - - @override - final String key; - - @override - String toString() => key; - - const AppLifecycleStates(this.key); - - /// Converts a platform-specific lifecycle state string to the corresponding AppLifecycleStates enum value. - /// - /// [state] The platform-specific lifecycle state string (e.g., 'resumed', 'inactive') - /// Returns the matching AppLifecycleStates value, or AppLifecycleStates.active if no match is found. - static AppLifecycleStates appLifecycleStateFor(String state) { - if (state == 'detached') { - return AppLifecycleStates.detached; - } - if (state == 'resumed') { - return AppLifecycleStates.resumed; - } - if (state == 'inactive') { - return AppLifecycleStates.inactive; - } - if (state == 'hidden') { - return AppLifecycleStates.hidden; - } - if (state == 'paused') { - return AppLifecycleStates.paused; - } - return AppLifecycleStates.active; - } -} - -/// RUM Semantics related to application lifecycle events -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum AppLifecycleSemantics implements OTelSemantic { - appLaunchId('app.launch.id'), - appLifecycleId('app_lifecycle.id'), - appLifecycleChange('app_lifecycle.changed'), - // App lifecycle state key, value is one of the AppLifecycleState - appLifecycleState('app_lifecycle.state'), - appLifecycleStateId('app_lifecycle.state_id'), - appLifecyclePreviousState('app_lifecycle.previous_state'), - appLifecyclePreviousStateId('app_lifecycle.previous_state_id'), - appLifecyclePreviousLifecycleId('app_lifecycle.previous_lifecycle_id'), - appLifecycleTimestamp('app_lifecycle.timestamp'), - appLifecycleDuration('app_lifecycle.duration'), - appStartType('app.start.type'); //cold/hot - - @override - final String key; - - @override - String toString() => key; - - const AppLifecycleSemantics(this.key); -} - -/// RUM Semantic values for AppLifecycleSemantics.appStartType key -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum AppStartType implements OTelSemantic { - cold('cold'), - hot('hot'); - - @override - final String key; - - @override - String toString() => key; - - const AppStartType(this.key); -} - -/// RUM Semantics related to application information -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum AppInfoSemantics implements OTelSemantic { - appId('app.id'), - appName('app.name'), - appVersion('app.version'), - appBuildNumber('app.build_number'), - appPackageName('app.package_name'), - ; - - @override - final String key; - - @override - String toString() => key; - - const AppInfoSemantics(this.key); -} - -/// RUM Semantics related to device information -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum DeviceSemantics implements OTelSemantic { - deviceId('device.id'), - deviceModel('device.model'), - devicePlatform('device.platform'), - deviceOsVersion('device.os_version'), - isPhysicalDevice('device.physical'), - isiOSAppOnMac('device.ios_on_mac'), - ; - - @override - final String key; - - @override - String toString() => key; - - const DeviceSemantics(this.key); -} - -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum BatterySemantics implements OTelSemantic { - batteryLevel('battery.level'), - batteryState('battery.state'), - - /// Battery state values - batteryStateFull('battery.state.full'), - batteryStateCharging('battery.state.charging'), - batteryStateConnectedNotCharging('battery.state.connected_not_charging'), - batteryStateDischanrging('battery.state.discharging'), - batteryStateUnknown('battery.state.unknown'), - batterySaveMode('battery.save_mode'); - - @override - final String key; - - @override - String toString() => key; - - const BatterySemantics(this.key); -} - -/// RUM Semantics related to navigation and routing -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum NavigationSemantics implements OTelSemantic { - navigationAction('navigation.action'), - navigationTrigger('navigation.trigger'), - navigationTimestamp('navigation.timestamp'), - routeName('navigation.route.name'), - routeId('navigation.route.id'), - routeKey('navigation.route.key'), - routePath('navigation.route.path'), - routeArguments('navigation.route.arguments'), - routeTimestamp('navigation.route.timestamp'), - previousRouteName('navigation.previous_route_name'), - previousRouteId('navigation.previous_route_id'), - previousRoutePath('navigation.previous_route_path'), - previousRouteDuration('route.previous_route_duration'), - routeTransitionDuration('route.transition_duration'); - - @override - final String key; - - @override - String toString() => key; - - const NavigationSemantics(this.key); -} - -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum InteractionType implements OTelSemantic { - click('click'), - drag('drag'), - focusChange('focus_change'), - formSubmit('form_submit'), - keydown('keydown'), - keyup('keyup'), - listSelection('list_selection'), - listSelectionIndex('list_selected_index'), - longPress('long_press'), - menuSelect('menu_select'), - menuSelectedItem('menu_selected_item'), - scroll('scroll'), - swipe('swipe'), - tap('tap'), - textInput('text_input'), - - // Gesture details - gestureDeltaX('gesture.delta_x'), - gestureDeltaY('gesture.delta_y'), - gestureDirection('gesture.direction'); - - @override - final String key; - - @override - String toString() => key; - - const InteractionType(this.key); -} - -/// Semantics related to user interactions -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum InteractionSemantics implements OTelSemantic { - userInteraction('user_interaction'), - interactionType('interaction.type'), - interactionTarget('interaction.target'), - interactionResult('interaction.result'), - inputDelay('input.delay'); - - @override - final String key; - - @override - String toString() => key; - - const InteractionSemantics(this.key); -} - -/// RUM Semantics related to performance measurements -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum PerformanceSemantics implements OTelSemantic { - renderDuration('render.duration'), - firstPaint('first.paint'), - firstContentfulPaint('first.contentful.paint'), - timeToInteractive('time.to.interactive'), - frameTime('frame.time'), - frameRate('frame.rate'), - jankCount('jank.count'), - memoryUsage('memory.usage'), - rumFirstInputDelay('first_input_delay'); - - @override - final String key; - - @override - String toString() => key; - - const PerformanceSemantics(this.key); -} - -/// RUM Semantics related to errors and crashes -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum ErrorSemantics implements OTelSemantic { - errorType('error.type'), - errorSource('error.source'), - errorMessage('error.message'), - errorStacktrace('error.stacktrace'), - crashFreeSessionRate('crash.free.session.rate'); - - @override - final String key; - - @override - String toString() => key; - - const ErrorSemantics(this.key); -} - -/// RUM Semantics related to network -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum NetworkSemantics implements OTelSemantic { - networkConnectivity('network.connectivity'), - networkType('network.type'), - networkRequestUrl('network.request.url'), - networkRequestDuration('network.request.duration'); - - @override - final String key; - - @override - String toString() => key; - - const NetworkSemantics(this.key); -} - -/// RUM-style view + extended session attributes (not in OTel core -/// semconv). The OTel-spec session keys (`session.id`, -/// `session.previous_id`) live in the generated `Session` enum; the -/// OTel-spec user keys (`user.id`, `user.email`, `user.full_name`, -/// `user.hash`, `user.name`, `user.roles`) live in `User`. This -/// enum keeps the Datadog/Dynatrace-style underscore variants and the -/// view-duration metrics that aren't in the OTel registry. -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum RumSessionView implements OTelSemantic { - rumSessionId('session_id'), - sessionStart('session.start'), - sessionDuration('session.duration'), - viewName('view.name'), - rumViewName('view_name'), - rumViewId('view_id'), - viewStart('view.start'), - viewDuration('view.duration'), - rumViewLoadTime('view_load_time'), - rumActionCount('action.count'), - rumUserSatisfactionScore('user_satisfaction_score'); - - @override - final String key; - - @override - String toString() => key; - - const RumSessionView(this.key); -} - -/// Navigation actions in a UI context -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum NavigationAction { - /// Navigation to a new screen via push - push('push'), - - /// Navigation back via pop - pop('pop'), - - /// Replace current screen - replace('replace'), - - /// Remove a screen from navigation stack - remove('remove'), - - /// Return to a previous screen that's still in the stack - returnTo('return_to'), - - /// Initial route when app starts - initial('initial'), - - /// Deep link navigation - deepLink('deep_link'), - - /// Redirect to another route - redirect('redirect'); - - /// String representation of the navigation action, used for serialization. - final String value; - const NavigationAction(this.value); - - @override - String toString() => value; -} - -/// Application lifecycle states -@Deprecated( - 'Vendor/RUM convention, not part of the OTel semantic conventions; will be removed from dartastic_opentelemetry_api') -enum LifecycleState { - /// App is visible and responding to user input - resumed('resumed'), - - /// App is visible but not responding to user input - inactive('inactive'), - - /// App is not visible (in the background) - paused('paused'), - - /// App is being shut down or detached from Flutter engine - detached('detached'); - - /// String representation of the lifecycle state, used for serialization. - final String value; - const LifecycleState(this.value); - - @override - String toString() => value; -} diff --git a/lib/src/api/trace/span.dart b/lib/src/api/trace/span.dart index 5290aa7..0c93bf9 100644 --- a/lib/src/api/trace/span.dart +++ b/lib/src/api/trace/span.dart @@ -82,17 +82,9 @@ class APISpan { // Set initial status to unset per spec _spanStatusCode = SpanStatusCode.Unset; - // Validate parent-child relationship - if (parentSpan != null) { - // Must inherit trace ID from parent - if (spanContext.traceId != parentSpan.spanContext.traceId) { - throw ArgumentError('Child span must inherit trace ID from parent'); - } - // Parent's span ID must match our parent span ID - if (spanContext.parentSpanId != parentSpan.spanContext.spanId) { - throw ArgumentError('Parent span ID must match'); - } - } + // No parent-child validation here: APISpanCreate.create — the sole + // construction path — performs the trace-ID and parent-span-ID + // checks before invoking this constructor. // No `else` branch: when parentSpan is null, the span may legitimately // have any parentSpanId (null, the invalid all-zeros marker, OR a // valid remote span id propagated from an upstream context). The diff --git a/lib/src/api/trace/tracer_provider.dart b/lib/src/api/trace/tracer_provider.dart index b5f99a8..01fa186 100644 --- a/lib/src/api/trace/tracer_provider.dart +++ b/lib/src/api/trace/tracer_provider.dart @@ -183,24 +183,19 @@ class APITracerProvider { return true; // Already shut down } - try { - // Mark as shut down immediately to prevent new tracers/spans - _isShutdown = true; + // Mark as shut down immediately to prevent new tracers/spans + _isShutdown = true; - // Clear the tracer cache - _tracerCache.clear(); + // Clear the tracer cache + _tracerCache.clear(); - // Disable the provider - _enabled = false; + // Disable the provider + _enabled = false; - // Reset current context through context API - // Clear any active spans from context - Context.root.setCurrentSpan(null); + // Reset current context through context API + // Clear any active spans from context + Context.root.setCurrentSpan(null); - return true; - } catch (e) { - OTelLog.error('Error during TracerProvider shutdown: $e'); - return false; - } + return true; } } diff --git a/lib/src/util/web_time_provider_stub.dart b/lib/src/util/web_time_provider_stub.dart index 1def52a..b8b3d7a 100644 --- a/lib/src/util/web_time_provider_stub.dart +++ b/lib/src/util/web_time_provider_stub.dart @@ -19,8 +19,12 @@ class WebTimeProvider implements TimeProvider { ); } + // Unreachable: the constructor always throws, so no instance can exist + // to call this on. Kept only to satisfy the TimeProvider interface. + // coverage:ignore-start @override DateTime nowDateTime() { throw UnsupportedError('WebTimeProvider is only available on web targets.'); } + // coverage:ignore-end } diff --git a/pubspec.yaml b/pubspec.yaml index d2d567d..3073bce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dartastic_opentelemetry_api description: Dartastic.io's OpenTelemetry API for Dart following the OpenTelemetry specification. Supports all platforms including web. -version: 1.0.0-beta.11-wip +version: 1.0.0-rc.1-wip repository: https://github.com/MindfulSoftwareLLC/dartastic_opentelemetry_api.git homepage: https://dartastic.io diff --git a/test/unit/api/common/attribute_validation_test.dart b/test/unit/api/common/attribute_validation_test.dart new file mode 100644 index 0000000..c63f79a --- /dev/null +++ b/test/unit/api/common/attribute_validation_test.dart @@ -0,0 +1,80 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Coverage for Attribute value validation, Attribute.toString, and the +// dynamic-list conversion paths in Attributes.of. + +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; +import 'package:test/test.dart'; + +void main() { + group('Attribute validation', () { + setUp(() { + OTelAPI.reset(); + OTelAPI.initialize( + endpoint: 'http://localhost:4317', + serviceName: 'test-service', + serviceVersion: '1.0.0', + ); + }); + + test('empty string value throws', () { + expect(() => OTelAPI.attributeString('k', ''), throwsArgumentError); + }); + + test('empty list value throws', () { + expect(() => OTelAPI.attributeStringList('k', []), + throwsArgumentError); + }); + + test('toString includes the value', () { + expect(OTelAPI.attributeString('k', 'v').toString(), + equals('AttributeValue(v)')); + }); + + test('Attributes.of converts untyped bool lists', () { + final attrs = Attributes.of({ + 'flags': [true, false] + }); + expect(attrs.getBoolList('flags'), equals([true, false])); + }); + + test('Attributes.of converts untyped int lists', () { + final attrs = Attributes.of({ + 'counts': [1, 2, 3] + }); + expect(attrs.getIntList('counts'), equals([1, 2, 3])); + }); + + test('Attributes.of converts mixed numeric lists to double', () { + final attrs = Attributes.of({ + 'nums': [1, 2.5] + }); + expect(attrs.getDoubleList('nums'), equals([1.0, 2.5])); + }); + + test('Attributes.of ignores lists of unsupported types', () { + final attrs = Attributes.of({ + 'bad': [Duration.zero], + 'good': 'kept', + }); + expect(attrs.getString('good'), equals('kept')); + expect(attrs.getStringList('bad'), isNull); + }); + + test( + 'fromJson converts untyped string, bool, int, and mixed numeric' + ' lists', () { + final attrs = Attributes.fromJson({ + 'names': ['a', 'b'], + 'flags': [true, false], + 'counts': [1, 2], + 'nums': [1, 2.5], + }); + expect(attrs.getStringList('names'), equals(['a', 'b'])); + expect(attrs.getBoolList('flags'), equals([true, false])); + expect(attrs.getIntList('counts'), equals([1, 2])); + expect(attrs.getDoubleList('nums'), equals([1.0, 2.5])); + }); + }); +} diff --git a/test/unit/api/common/attributes_test.dart b/test/unit/api/common/attributes_test.dart index 32cf430..d803dba 100644 --- a/test/unit/api/common/attributes_test.dart +++ b/test/unit/api/common/attributes_test.dart @@ -199,10 +199,10 @@ void main() { 'should convert from Map with an Object to string value', () { final attrs = OTelAPI.attributesFromMap({ - 'menu.select': InteractionType - .menuSelect, //don't put a key as a value, this just tests toString + 'net.type': NetworkConnectionType + .wifi, //don't put a key as a value, this just tests toString }); - expect(attrs.getString('menu.select'), equals('menu_select')); + expect(attrs.getString('net.type'), equals('wifi')); }); test('remove returns new Attributes without given key', () { diff --git a/test/unit/api/context/context_api_coverage_test.dart b/test/unit/api/context/context_api_coverage_test.dart new file mode 100644 index 0000000..7bf4716 --- /dev/null +++ b/test/unit/api/context/context_api_coverage_test.dart @@ -0,0 +1,49 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Coverage for Context.withSpanContext trace-change rejection and the +// ContextKey uniqueId/toString surface. + +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; +import 'package:test/test.dart'; + +void main() { + group('Context API surface', () { + setUp(() { + OTelAPI.reset(); + OTelAPI.initialize( + endpoint: 'http://localhost:4317', + serviceName: 'test-service', + serviceVersion: '1.0.0', + ); + }); + + test('withSpanContext rejects changing the trace ID', () { + final sc1 = OTelAPI.spanContext( + traceId: OTelAPI.traceId(), + spanId: OTelAPI.spanId(), + ); + final sc2 = OTelAPI.spanContext( + traceId: OTelAPI.traceId(), // different trace + spanId: OTelAPI.spanId(), + ); + final span = OTelAPI.nonRecordingSpan(sc1); + final context = Context.root.withSpan(span); + + expect( + () => context.withSpanContext(sc2), + throwsA(isA().having( + (e) => e.message, + 'message', + contains('Cannot change trace ID'), + )), + ); + }); + + test('ContextKey exposes uniqueId and a descriptive toString', () { + final key = OTelAPI.contextKey('coverage-key'); + expect(key.uniqueId, isNotEmpty); + expect(key.toString(), contains('coverage-key')); + }); + }); +} diff --git a/test/unit/api/context/isolate_support_test.dart b/test/unit/api/context/isolate_support_test.dart new file mode 100644 index 0000000..171a137 --- /dev/null +++ b/test/unit/api/context/isolate_support_test.dart @@ -0,0 +1,21 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// The native isolate-support shim re-exports dart:isolate; its +// runIsolateComputation body is a guard that must never be called +// directly. This locks down that contract. + +@TestOn('vm') +library; + +import 'package:dartastic_opentelemetry_api/src/api/context/isolate_support.dart'; +import 'package:test/test.dart'; + +void main() { + test('runIsolateComputation must not be called directly', () { + expect( + runIsolateComputation(() async => 1, {}, {}, () {}), + throwsA(isA()), + ); + }); +} diff --git a/test/unit/api/metrics/metrics_api_coverage_test.dart b/test/unit/api/metrics/metrics_api_coverage_test.dart new file mode 100644 index 0000000..73696d8 --- /dev/null +++ b/test/unit/api/metrics/metrics_api_coverage_test.dart @@ -0,0 +1,108 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Coverage for the observable-instrument callback surface, the no-op +// histogram record, and meter equality. + +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; +import 'package:test/test.dart'; + +void main() { + group('Metrics API surface', () { + late APIMeter meter; + + setUp(() { + OTelAPI.reset(); + OTelAPI.initialize( + endpoint: 'http://localhost:4317', + serviceName: 'test-service', + serviceVersion: '1.0.0', + ); + meter = OTelAPI.meterProvider().getMeter(name: 'coverage-meter'); + }); + + void expectCallbackSurface({ + required List> Function() callbacks, + required APICallbackRegistration Function( + ObservableCallback) + addCallback, + required void Function(ObservableCallback) removeCallback, + required List Function() collect, + required int initialCallbacks, + }) { + void extra(APIObservableResult result) {} + + expect(callbacks().length, equals(initialCallbacks)); + addCallback(extra); + expect(callbacks(), contains(extra)); + removeCallback(extra); + expect(callbacks(), isNot(contains(extra))); + // The API implementation collects nothing; the SDK overrides this. + expect(collect(), isEmpty); + } + + test('observable counter callback registration surface', () { + final counter = meter.createObservableCounter( + name: 'obs-counter', + callback: (result) {}, + ); + expectCallbackSurface( + callbacks: () => counter.callbacks, + addCallback: counter.addCallback, + removeCallback: counter.removeCallback, + collect: counter.collect, + initialCallbacks: 1, + ); + }); + + test('observable up-down counter callback registration surface', () { + final counter = meter.createObservableUpDownCounter( + name: 'obs-updown', + callback: (result) {}, + ); + expectCallbackSurface( + callbacks: () => counter.callbacks, + addCallback: counter.addCallback, + removeCallback: counter.removeCallback, + collect: counter.collect, + initialCallbacks: 1, + ); + }); + + test('observable gauge callback registration surface', () { + final gauge = meter.createObservableGauge( + name: 'obs-gauge', + callback: (result) {}, + ); + expectCallbackSurface( + callbacks: () => gauge.callbacks, + addCallback: gauge.addCallback, + removeCallback: gauge.removeCallback, + collect: gauge.collect, + initialCallbacks: 1, + ); + }); + + test('histogram record is a no-op in the API', () { + final histogram = meter + .createHistogram(name: 'h', boundaries: [1.0, 5.0, 10.0]); + histogram.record(0.5); + histogram.record(1.5, Attributes.of({'k': 'v'})); + expect(histogram.name, equals('h')); + expect(histogram.boundaries, equals([1.0, 5.0, 10.0])); + }); + + test('meters with the same identity are equal', () { + final m1 = OTelAPI.meterProvider('mp-a') + .getMeter(name: 'm', version: '1.0', schemaUrl: 'https://s'); + final m2 = OTelAPI.meterProvider('mp-b') + .getMeter(name: 'm', version: '1.0', schemaUrl: 'https://s'); + final other = OTelAPI.meterProvider('mp-a').getMeter(name: 'different'); + + expect(identical(m1, m2), isFalse); + expect(m1, equals(m2)); + expect(m1.hashCode, equals(m2.hashCode)); + expect(m1, isNot(equals(other))); + }); + }); +} diff --git a/test/unit/api/otel_api_test.dart b/test/unit/api/otel_api_test.dart index aa6df5d..771829e 100644 --- a/test/unit/api/otel_api_test.dart +++ b/test/unit/api/otel_api_test.dart @@ -408,6 +408,86 @@ void main() { ); }, throwsA(isA())); }); + + test('loggerProvider returns named provider when name provided', () { + final provider = OTelAPI.loggerProvider('named-logs'); + expect(provider, isA()); + expect(provider, isNot(same(OTelAPI.loggerProvider()))); + expect(OTelAPI.loggerProvider('named-logs'), same(provider)); + }); + + test('createCounter delegates to the global meter provider', () { + final counter = OTelAPI.createCounter('api-counter', + description: 'counts things', unit: '{thing}'); + expect(counter, isA()); + expect(counter.name, equals('api-counter')); + }); + + test('createUpDownCounter delegates to the global meter provider', () { + final counter = OTelAPI.createUpDownCounter('api-updown', + description: 'queue depth', unit: '{item}'); + expect(counter, isA()); + expect(counter.name, equals('api-updown')); + }); + + test('createGauge delegates to the global meter provider', () { + final gauge = + OTelAPI.createGauge('api-gauge', description: 'level', unit: '%'); + expect(gauge, isA()); + expect(gauge.name, equals('api-gauge')); + }); + + test('createHistogram delegates to the global meter provider', () { + final histogram = OTelAPI.createHistogram('api-histogram', + description: 'latency', unit: 'ms', boundaries: [1, 5, 10]); + expect(histogram, isA()); + expect(histogram.name, equals('api-histogram')); + }); + + test('createObservableCounter delegates to the global meter provider', () { + final counter = OTelAPI.createObservableCounter('api-obs-counter', + description: 'observed count', unit: '{thing}'); + expect(counter, isA()); + expect(counter.name, equals('api-obs-counter')); + }); + + test('createObservableGauge delegates to the global meter provider', () { + final gauge = OTelAPI.createObservableGauge('api-obs-gauge', + description: 'observed level', unit: '%'); + expect(gauge, isA()); + expect(gauge.name, equals('api-obs-gauge')); + }); + + test('createObservableUpDownCounter delegates to the global meter provider', + () { + final counter = OTelAPI.createObservableUpDownCounter('api-obs-updown', + description: 'observed depth', unit: '{item}'); + expect(counter, isA()); + expect(counter.name, equals('api-obs-updown')); + }); + + test('initialize logs when replacing the uninitialized no-op factory', () { + OTelAPI.reset(); + final messages = []; + final prevLevel = OTelLog.currentLevel; + final prevFn = OTelLog.logFunction; + OTelLog.currentLevel = LogLevel.debug; + OTelLog.logFunction = messages.add; + try { + // Uninitialized use installs the default no-op API factory. + OTelAPI.tracerProvider(); + OTelAPI.initialize( + endpoint: 'http://localhost:4317', + serviceName: 'test-service', + serviceVersion: '1.0.0', + ); + } finally { + OTelLog.currentLevel = prevLevel; + OTelLog.logFunction = prevFn; + } + expect(messages.join('\n'), + contains('replacing the installed no-op API factory')); + }); }); } diff --git a/test/unit/api/remaining_coverage_test.dart b/test/unit/api/remaining_coverage_test.dart new file mode 100644 index 0000000..e15bc3e --- /dev/null +++ b/test/unit/api/remaining_coverage_test.dart @@ -0,0 +1,63 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// The last uncovered behaviors: baggage empty-name filtering, the +// OTelFactory.isAPIFactory default, and the no-SDK parent-span +// context pass-through. + +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; +import 'package:test/test.dart'; + +/// A factory that does not override [OTelFactory.isAPIFactory], to lock +/// down the base-class default: a factory is assumed to be a real (SDK) +/// implementation unless it declares otherwise. +class _DefaultingFactory extends OTelFactory { + _DefaultingFactory() + : super( + apiEndpoint: 'http://localhost:4317', + apiServiceName: 'svc', + apiServiceVersion: '1.0.0', + factoryFactory: otelApiFactoryFactoryFunction, + ); + + @override + dynamic noSuchMethod(Invocation invocation) => throw UnimplementedError(); +} + +void main() { + group('Remaining coverage', () { + setUp(() { + OTelAPI.reset(); + OTelAPI.initialize( + endpoint: 'http://localhost:4317', + serviceName: 'test-service', + serviceVersion: '1.0.0', + ); + }); + + test('baggage ignores entries with empty names', () { + final baggage = OTelAPI.baggageForMap({'': 'dropped', 'kept': 'v'}); + expect(baggage.getEntry(''), isNull); + expect(baggage.getEntry('kept')?.value, equals('v')); + }); + + test( + 'isAPIFactory defaults to false for factories that do not' + ' declare it', () { + expect(_DefaultingFactory().isAPIFactory, isFalse); + }); + + test('no-SDK createSpan carries an explicit parent span context through', + () { + final sc = OTelAPI.spanContext( + traceId: OTelAPI.traceId(), + spanId: OTelAPI.spanId(), + ); + final parent = OTelAPI.nonRecordingSpan(sc); + final child = OTelAPI.tracer('no-sdk') + .createSpan(name: 'child', parentSpan: parent); + expect(child, isA()); + expect(child.spanContext, equals(sc)); + }); + }); +} diff --git a/test/unit/api/semantics/app_lifecycle_semconv_test.dart b/test/unit/api/semantics/app_lifecycle_semconv_test.dart new file mode 100644 index 0000000..dff8ef4 --- /dev/null +++ b/test/unit/api/semantics/app_lifecycle_semconv_test.dart @@ -0,0 +1,50 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Registry replacement for the removed `LifecycleState` and +// `AppLifecycleStates` enums: app lifecycle is modeled by the +// `device.app.lifecycle` event carrying a platform state attribute +// (`android.app.state` or `ios.app.state`). +// flutterrific_opentelemetry maps Flutter's +// resumed/inactive/paused/detached states onto these platform values. + +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; +import 'package:test/test.dart'; + +void main() { + group('App lifecycle semconv', () { + test('device.app.lifecycle event', () { + expect( + DeviceEvent.deviceAppLifecycle.name, equals('device.app.lifecycle')); + expect(DeviceEvent.deviceAppLifecycle.toString(), + equals('device.app.lifecycle')); + }); + + test('android.app.state attribute and values', () { + expect(Android.androidAppState.key, equals('android.app.state')); + expect(AndroidAppState.created.value, equals('created')); + expect(AndroidAppState.background.value, equals('background')); + expect(AndroidAppState.foreground.value, equals('foreground')); + expect(AndroidAppState.foreground.toString(), equals('foreground')); + }); + + test('ios.app.state attribute and values', () { + expect(Ios.iosAppState.key, equals('ios.app.state')); + expect(IosAppState.active.value, equals('active')); + expect(IosAppState.inactive.value, equals('inactive')); + expect(IosAppState.background.value, equals('background')); + expect(IosAppState.foreground.value, equals('foreground')); + expect(IosAppState.terminate.value, equals('terminate')); + expect(IosAppState.terminate.toString(), equals('terminate')); + }); + + test('lifecycle state attributes round-trip through Attributes', () { + final attrs = OTelAPI.attributesFromSemanticMap({ + Android.androidAppState: AndroidAppState.foreground.value, + Ios.iosAppState: IosAppState.active.value, + }); + expect(attrs.getString('android.app.state'), equals('foreground')); + expect(attrs.getString('ios.app.state'), equals('active')); + }); + }); +} diff --git a/test/unit/api/semantics/app_screen_semconv_test.dart b/test/unit/api/semantics/app_screen_semconv_test.dart new file mode 100644 index 0000000..1340ea0 --- /dev/null +++ b/test/unit/api/semantics/app_screen_semconv_test.dart @@ -0,0 +1,45 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Nearest registry replacement for the removed `NavigationAction` enum. +// The registry has no navigation-action convention (push, pop, replace, +// deep link, ...); what it does define is the screen/widget surface: +// `app.screen.*` / `app.widget.*` attributes and the `app.screen.click` +// / `app.widget.click` events. flutterrific_opentelemetry defines +// navigation-action conventions, a candidate for a future upstream +// semantic convention. + +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; +import 'package:test/test.dart'; + +void main() { + group('App screen semconv', () { + test('app.screen attributes', () { + expect(App.appScreenId.key, equals('app.screen.id')); + expect(App.appScreenName.key, equals('app.screen.name')); + expect(App.appScreenCoordinateX.key, equals('app.screen.coordinate.x')); + expect(App.appScreenCoordinateY.key, equals('app.screen.coordinate.y')); + }); + + test('app.widget attributes', () { + expect(App.appWidgetId.key, equals('app.widget.id')); + expect(App.appWidgetName.key, equals('app.widget.name')); + }); + + test('screen and widget click events', () { + expect(AppEvent.appScreenClick.name, equals('app.screen.click')); + expect(AppEvent.appWidgetClick.name, equals('app.widget.click')); + }); + + test('screen attributes round-trip through Attributes', () { + final attrs = OTelAPI.attributesFromSemanticMap({ + App.appScreenName: 'HomeScreen', + App.appScreenId: 'home', + App.appWidgetName: 'CheckoutButton', + }); + expect(attrs.getString('app.screen.name'), equals('HomeScreen')); + expect(attrs.getString('app.screen.id'), equals('home')); + expect(attrs.getString('app.widget.name'), equals('CheckoutButton')); + }); + }); +} diff --git a/test/unit/api/semantics/lifecycle_state_test.dart b/test/unit/api/semantics/lifecycle_state_test.dart deleted file mode 100644 index 78036fe..0000000 --- a/test/unit/api/semantics/lifecycle_state_test.dart +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; -import 'package:test/test.dart'; - -void main() { - group('LifecycleState', () { - test('has correct values', () { - expect(LifecycleState.resumed.value, equals('resumed')); - expect(LifecycleState.inactive.value, equals('inactive')); - expect(LifecycleState.paused.value, equals('paused')); - expect(LifecycleState.detached.value, equals('detached')); - }); - - test('toString returns the value', () { - expect(LifecycleState.resumed.toString(), equals('resumed')); - expect(LifecycleState.inactive.toString(), equals('inactive')); - expect(LifecycleState.paused.toString(), equals('paused')); - expect(LifecycleState.detached.toString(), equals('detached')); - }); - }); -} diff --git a/test/unit/api/semantics/mobile_semconv_test.dart b/test/unit/api/semantics/mobile_semconv_test.dart new file mode 100644 index 0000000..1e6d3fb --- /dev/null +++ b/test/unit/api/semantics/mobile_semconv_test.dart @@ -0,0 +1,71 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Registry replacements for the removed RUM enums: the keys those +// enums carried are covered by registry conventions — device.*, +// session.*, network.connection.type, error.type, service.*, and the +// app.* crash/jank surface. + +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; +import 'package:test/test.dart'; + +void main() { + group('Mobile semconv', () { + test('device identity (was DeviceSemantics)', () { + expect(Device.deviceId.key, equals('device.id')); + expect(Device.deviceManufacturer.key, equals('device.manufacturer')); + expect( + Device.deviceModelIdentifier.key, equals('device.model.identifier')); + expect(Device.deviceModelName.key, equals('device.model.name')); + }); + + test('session attributes and events (was RumSessionView)', () { + expect(Session.sessionId.key, equals('session.id')); + expect(Session.sessionPreviousId.key, equals('session.previous_id')); + expect(SessionEvent.sessionStart.name, equals('session.start')); + expect(SessionEvent.sessionEnd.name, equals('session.end')); + }); + + test('network connection type (was NetworkSemantics)', () { + expect( + Network.networkConnectionType.key, equals('network.connection.type')); + expect(NetworkConnectionType.wifi.value, equals('wifi')); + expect(NetworkConnectionType.cell.value, equals('cell')); + expect(NetworkConnectionType.unavailable.value, equals('unavailable')); + }); + + test('error type (was ErrorSemantics)', () { + expect(ErrorAttributes.errorType.key, equals('error.type')); + }); + + test('app identity (was AppInfoSemantics)', () { + expect(Service.serviceName.key, equals('service.name')); + expect(Service.serviceVersion.key, equals('service.version')); + expect(App.appBuildId.key, equals('app.build_id')); + expect(App.appInstallationId.key, equals('app.installation.id')); + }); + + test('crash and jank (was PerformanceSemantics)', () { + expect(AppEvent.appCrash.name, equals('app.crash')); + expect(AppEvent.appJank.name, equals('app.jank')); + expect(App.appCrashId.key, equals('app.crash.id')); + expect(App.appJankFrameCount.key, equals('app.jank.frame_count')); + expect(App.appJankPeriod.key, equals('app.jank.period')); + expect(App.appJankThreshold.key, equals('app.jank.threshold')); + }); + + test('mobile attributes round-trip through Attributes', () { + final attrs = OTelAPI.attributesFromSemanticMap({ + Device.deviceId: 'a1b2c3', + Session.sessionId: 'sess-42', + Network.networkConnectionType: NetworkConnectionType.wifi.value, + ErrorAttributes.errorType: 'java.net.UnknownHostException', + }); + expect(attrs.getString('device.id'), equals('a1b2c3')); + expect(attrs.getString('session.id'), equals('sess-42')); + expect(attrs.getString('network.connection.type'), equals('wifi')); + expect(attrs.getString('error.type'), + equals('java.net.UnknownHostException')); + }); + }); +} diff --git a/test/unit/api/semantics/navigation_action_test.dart b/test/unit/api/semantics/navigation_action_test.dart deleted file mode 100644 index 66d32a9..0000000 --- a/test/unit/api/semantics/navigation_action_test.dart +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; -import 'package:test/test.dart'; - -void main() { - group('NavigationAction', () { - test('has correct values', () { - expect(NavigationAction.push.value, equals('push')); - expect(NavigationAction.pop.value, equals('pop')); - expect(NavigationAction.replace.value, equals('replace')); - expect(NavigationAction.remove.value, equals('remove')); - expect(NavigationAction.returnTo.value, equals('return_to')); - expect(NavigationAction.initial.value, equals('initial')); - expect(NavigationAction.deepLink.value, equals('deep_link')); - expect(NavigationAction.redirect.value, equals('redirect')); - }); - - test('toString returns the value', () { - expect(NavigationAction.push.toString(), equals('push')); - expect(NavigationAction.pop.toString(), equals('pop')); - expect(NavigationAction.replace.toString(), equals('replace')); - expect(NavigationAction.remove.toString(), equals('remove')); - expect(NavigationAction.returnTo.toString(), equals('return_to')); - expect(NavigationAction.initial.toString(), equals('initial')); - expect(NavigationAction.deepLink.toString(), equals('deep_link')); - expect(NavigationAction.redirect.toString(), equals('redirect')); - }); - }); -} diff --git a/test/unit/api/semantics/rum_semantics_test.dart b/test/unit/api/semantics/rum_semantics_test.dart deleted file mode 100644 index a6f311a..0000000 --- a/test/unit/api/semantics/rum_semantics_test.dart +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; -import 'package:test/test.dart'; - -void main() { - group('RUM/vendor semantics (deprecated)', () { - test('AppLifecycleStates toString returns key', () { - expect(AppLifecycleStates.active.toString(), - equals('device.app.lifecycle.active')); - expect(AppLifecycleStates.resumed.toString(), - equals('device.app.lifecycle.resumed')); - expect(AppLifecycleStates.detached.toString(), - equals('device.app.lifecycle.detached')); - expect(AppLifecycleStates.inactive.toString(), - equals('device.app.lifecycle.inactive')); - expect(AppLifecycleStates.hidden.toString(), - equals('device.app.lifecycle.hidden')); - expect(AppLifecycleStates.paused.toString(), - equals('device.app.lifecycle.paused')); - }); - - test('AppLifecycleStates.appLifecycleStateFor maps platform strings', () { - expect(AppLifecycleStates.appLifecycleStateFor('detached'), - equals(AppLifecycleStates.detached)); - expect(AppLifecycleStates.appLifecycleStateFor('resumed'), - equals(AppLifecycleStates.resumed)); - expect(AppLifecycleStates.appLifecycleStateFor('inactive'), - equals(AppLifecycleStates.inactive)); - expect(AppLifecycleStates.appLifecycleStateFor('hidden'), - equals(AppLifecycleStates.hidden)); - expect(AppLifecycleStates.appLifecycleStateFor('paused'), - equals(AppLifecycleStates.paused)); - // Unknown / unspecified strings fall back to active. - expect(AppLifecycleStates.appLifecycleStateFor('active'), - equals(AppLifecycleStates.active)); - expect(AppLifecycleStates.appLifecycleStateFor('garbage'), - equals(AppLifecycleStates.active)); - expect(AppLifecycleStates.appLifecycleStateFor(''), - equals(AppLifecycleStates.active)); - }); - - test('AppLifecycleSemantics toString returns key', () { - expect(AppLifecycleSemantics.appLaunchId.toString(), - equals('app.launch.id')); - expect(AppLifecycleSemantics.appLifecycleId.toString(), - equals('app_lifecycle.id')); - expect(AppLifecycleSemantics.appLifecycleChange.toString(), - equals('app_lifecycle.changed')); - expect(AppLifecycleSemantics.appLifecycleState.toString(), - equals('app_lifecycle.state')); - expect(AppLifecycleSemantics.appLifecycleStateId.toString(), - equals('app_lifecycle.state_id')); - expect(AppLifecycleSemantics.appLifecyclePreviousState.toString(), - equals('app_lifecycle.previous_state')); - expect(AppLifecycleSemantics.appLifecyclePreviousStateId.toString(), - equals('app_lifecycle.previous_state_id')); - expect(AppLifecycleSemantics.appLifecyclePreviousLifecycleId.toString(), - equals('app_lifecycle.previous_lifecycle_id')); - expect(AppLifecycleSemantics.appLifecycleTimestamp.toString(), - equals('app_lifecycle.timestamp')); - expect(AppLifecycleSemantics.appLifecycleDuration.toString(), - equals('app_lifecycle.duration')); - expect(AppLifecycleSemantics.appStartType.toString(), - equals('app.start.type')); - }); - - test('AppStartType toString returns key', () { - expect(AppStartType.cold.toString(), equals('cold')); - expect(AppStartType.hot.toString(), equals('hot')); - }); - - test('AppInfoSemantics toString returns key', () { - expect(AppInfoSemantics.appId.toString(), equals('app.id')); - expect(AppInfoSemantics.appName.toString(), equals('app.name')); - expect(AppInfoSemantics.appVersion.toString(), equals('app.version')); - expect(AppInfoSemantics.appBuildNumber.toString(), - equals('app.build_number')); - expect(AppInfoSemantics.appPackageName.toString(), - equals('app.package_name')); - }); - - test('DeviceSemantics toString returns key', () { - expect(DeviceSemantics.deviceId.toString(), equals('device.id')); - expect(DeviceSemantics.deviceModel.toString(), equals('device.model')); - expect( - DeviceSemantics.devicePlatform.toString(), equals('device.platform')); - expect(DeviceSemantics.deviceOsVersion.toString(), - equals('device.os_version')); - expect(DeviceSemantics.isPhysicalDevice.toString(), - equals('device.physical')); - expect(DeviceSemantics.isiOSAppOnMac.toString(), - equals('device.ios_on_mac')); - }); - - test('BatterySemantics toString returns key', () { - expect(BatterySemantics.batteryLevel.toString(), equals('battery.level')); - expect(BatterySemantics.batteryState.toString(), equals('battery.state')); - expect(BatterySemantics.batteryStateFull.toString(), - equals('battery.state.full')); - expect(BatterySemantics.batteryStateCharging.toString(), - equals('battery.state.charging')); - expect(BatterySemantics.batteryStateConnectedNotCharging.toString(), - equals('battery.state.connected_not_charging')); - expect(BatterySemantics.batteryStateDischanrging.toString(), - equals('battery.state.discharging')); - expect(BatterySemantics.batteryStateUnknown.toString(), - equals('battery.state.unknown')); - expect(BatterySemantics.batterySaveMode.toString(), - equals('battery.save_mode')); - }); - - test('NavigationSemantics toString returns key', () { - expect(NavigationSemantics.navigationAction.toString(), - equals('navigation.action')); - expect(NavigationSemantics.navigationTrigger.toString(), - equals('navigation.trigger')); - expect(NavigationSemantics.navigationTimestamp.toString(), - equals('navigation.timestamp')); - expect(NavigationSemantics.routeName.toString(), - equals('navigation.route.name')); - expect(NavigationSemantics.routeId.toString(), - equals('navigation.route.id')); - expect(NavigationSemantics.routeKey.toString(), - equals('navigation.route.key')); - expect(NavigationSemantics.routePath.toString(), - equals('navigation.route.path')); - expect(NavigationSemantics.routeArguments.toString(), - equals('navigation.route.arguments')); - expect(NavigationSemantics.routeTimestamp.toString(), - equals('navigation.route.timestamp')); - expect(NavigationSemantics.previousRouteName.toString(), - equals('navigation.previous_route_name')); - expect(NavigationSemantics.previousRouteId.toString(), - equals('navigation.previous_route_id')); - expect(NavigationSemantics.previousRoutePath.toString(), - equals('navigation.previous_route_path')); - expect(NavigationSemantics.previousRouteDuration.toString(), - equals('route.previous_route_duration')); - expect(NavigationSemantics.routeTransitionDuration.toString(), - equals('route.transition_duration')); - }); - - test('InteractionType toString returns key', () { - expect(InteractionType.click.toString(), equals('click')); - expect(InteractionType.drag.toString(), equals('drag')); - expect(InteractionType.focusChange.toString(), equals('focus_change')); - expect(InteractionType.formSubmit.toString(), equals('form_submit')); - expect(InteractionType.keydown.toString(), equals('keydown')); - expect(InteractionType.keyup.toString(), equals('keyup')); - expect( - InteractionType.listSelection.toString(), equals('list_selection')); - expect(InteractionType.listSelectionIndex.toString(), - equals('list_selected_index')); - expect(InteractionType.longPress.toString(), equals('long_press')); - expect(InteractionType.menuSelect.toString(), equals('menu_select')); - expect(InteractionType.menuSelectedItem.toString(), - equals('menu_selected_item')); - expect(InteractionType.scroll.toString(), equals('scroll')); - expect(InteractionType.swipe.toString(), equals('swipe')); - expect(InteractionType.tap.toString(), equals('tap')); - expect(InteractionType.textInput.toString(), equals('text_input')); - expect( - InteractionType.gestureDeltaX.toString(), equals('gesture.delta_x')); - expect( - InteractionType.gestureDeltaY.toString(), equals('gesture.delta_y')); - expect(InteractionType.gestureDirection.toString(), - equals('gesture.direction')); - }); - - test('InteractionSemantics toString returns key', () { - expect(InteractionSemantics.userInteraction.toString(), - equals('user_interaction')); - expect(InteractionSemantics.interactionType.toString(), - equals('interaction.type')); - expect(InteractionSemantics.interactionTarget.toString(), - equals('interaction.target')); - expect(InteractionSemantics.interactionResult.toString(), - equals('interaction.result')); - expect(InteractionSemantics.inputDelay.toString(), equals('input.delay')); - }); - - test('PerformanceSemantics toString returns key', () { - expect(PerformanceSemantics.renderDuration.toString(), - equals('render.duration')); - expect(PerformanceSemantics.firstPaint.toString(), equals('first.paint')); - expect(PerformanceSemantics.firstContentfulPaint.toString(), - equals('first.contentful.paint')); - expect(PerformanceSemantics.timeToInteractive.toString(), - equals('time.to.interactive')); - expect(PerformanceSemantics.frameTime.toString(), equals('frame.time')); - expect(PerformanceSemantics.frameRate.toString(), equals('frame.rate')); - expect(PerformanceSemantics.jankCount.toString(), equals('jank.count')); - expect( - PerformanceSemantics.memoryUsage.toString(), equals('memory.usage')); - expect(PerformanceSemantics.rumFirstInputDelay.toString(), - equals('first_input_delay')); - }); - - test('ErrorSemantics toString returns key', () { - expect(ErrorSemantics.errorType.toString(), equals('error.type')); - expect(ErrorSemantics.errorSource.toString(), equals('error.source')); - expect(ErrorSemantics.errorMessage.toString(), equals('error.message')); - expect(ErrorSemantics.errorStacktrace.toString(), - equals('error.stacktrace')); - expect(ErrorSemantics.crashFreeSessionRate.toString(), - equals('crash.free.session.rate')); - }); - - test('NetworkSemantics toString returns key', () { - expect(NetworkSemantics.networkConnectivity.toString(), - equals('network.connectivity')); - expect(NetworkSemantics.networkType.toString(), equals('network.type')); - expect(NetworkSemantics.networkRequestUrl.toString(), - equals('network.request.url')); - expect(NetworkSemantics.networkRequestDuration.toString(), - equals('network.request.duration')); - }); - - test('Session (OTel spec) toString returns key', () { - expect(Session.sessionId.toString(), equals('session.id')); - expect( - Session.sessionPreviousId.toString(), equals('session.previous_id')); - }); - - test('RumSessionView (non-spec) toString returns key', () { - expect(RumSessionView.rumSessionId.toString(), equals('session_id')); - expect(RumSessionView.sessionStart.toString(), equals('session.start')); - expect(RumSessionView.sessionDuration.toString(), - equals('session.duration')); - expect(RumSessionView.viewName.toString(), equals('view.name')); - expect(RumSessionView.rumViewName.toString(), equals('view_name')); - expect(RumSessionView.rumViewId.toString(), equals('view_id')); - expect(RumSessionView.viewStart.toString(), equals('view.start')); - expect(RumSessionView.viewDuration.toString(), equals('view.duration')); - expect( - RumSessionView.rumViewLoadTime.toString(), equals('view_load_time')); - expect(RumSessionView.rumActionCount.toString(), equals('action.count')); - expect(RumSessionView.rumUserSatisfactionScore.toString(), - equals('user_satisfaction_score')); - }); - - test('User (OTel spec) toString returns key', () { - expect(User.userId.toString(), equals('user.id')); - expect(User.userRoles.toString(), equals('user.roles')); - }); - }); -} diff --git a/test/unit/api/semantics/semconv_invariants_test.dart b/test/unit/api/semantics/semconv_invariants_test.dart index 9a422f2..069e8f2 100644 --- a/test/unit/api/semantics/semconv_invariants_test.dart +++ b/test/unit/api/semantics/semconv_invariants_test.dart @@ -4,32 +4,6 @@ import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; import 'package:test/test.dart'; -/// The RUM/vendor keys that intentionally shadow a registry attribute key. -/// Anything outside this set is an accidental collision and a bug. -const Set allowedRumRegistryOverlap = { - 'device.id', - 'error.message', - 'error.type', - 'network.type', -}; - -/// Every deprecated vendor/RUM enum that implements [OTelSemantic]. -List> rumSemanticEnums() => [ - AppLifecycleStates.values, - AppLifecycleSemantics.values, - AppStartType.values, - AppInfoSemantics.values, - InteractionType.values, - DeviceSemantics.values, - BatterySemantics.values, - NavigationSemantics.values, - InteractionSemantics.values, - PerformanceSemantics.values, - ErrorSemantics.values, - NetworkSemantics.values, - RumSessionView.values, - ]; - void main() { group('Semconv invariants', () { final keyFormat = RegExp(r'^[a-z0-9_.]+$'); @@ -98,6 +72,7 @@ void main() { for (final values in SemconvRegistry.allEntityEnums) { for (final entity in values) { expect(entity.name, matches(keyFormat)); + expect(entity.toString(), equals(entity.name)); for (final attr in [...entity.identifying, ...entity.descriptive]) { expect(allKeys, contains(attr.key), reason: @@ -107,18 +82,6 @@ void main() { } }); - test('RUM/vendor keys only shadow the known registry keys', () { - final semconvKeys = { - for (final values in SemconvRegistry.allAttributeEnums) - for (final member in values) member.key, - }; - final rumKeys = { - for (final values in rumSemanticEnums()) - for (final member in values) member.key, - }; - expect(semconvKeys.intersection(rumKeys), allowedRumRegistryOverlap); - }); - test('registry records its source version', () { expect(SemconvRegistry.schemaVersion, isNotEmpty); expect(SemconvRegistry.registryCommit, isNot('unknown')); diff --git a/test/unit/api/trace/span_create_guards_test.dart b/test/unit/api/trace/span_create_guards_test.dart new file mode 100644 index 0000000..79022db --- /dev/null +++ b/test/unit/api/trace/span_create_guards_test.dart @@ -0,0 +1,103 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// APISpanCreate / SpanLinkCreate guard coverage. The public tracer path +// pre-validates (throwing its own trace-mismatch error) and +// auto-corrects parent span IDs, so these internal guards can only fire +// for direct SDK-style calls — which is exactly how they are exercised +// here. + +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; +import 'package:dartastic_opentelemetry_api/src/api/trace/span.dart'; +import 'package:dartastic_opentelemetry_api/src/api/trace/span_link.dart'; +import 'package:test/test.dart'; + +import '../../../test_util.dart'; + +void main() { + group('APISpanCreate guards', () { + late OTelFactory originalFactory; + late APITracer tracer; + + setUp(() { + OTelAPI.reset(); + OTelAPI.initialize( + endpoint: 'http://localhost:4317', + serviceName: 'test-service', + serviceVersion: '1.0.0', + ); + installSdkLikeFactory(); + originalFactory = OTelFactory.otelFactory!; + tracer = OTelAPI.tracer('guards-tracer'); + }); + + tearDown(() { + OTelFactory.otelFactory = originalFactory; + }); + + test('child span must inherit the parent trace ID', () { + final parent = tracer.createSpan(name: 'parent'); + final foreignTrace = OTelAPI.spanContext( + traceId: OTelAPI.traceId(), // different trace + spanId: OTelAPI.spanId(), + parentSpanId: parent.spanContext.spanId, + ); + expect( + () => APISpanCreate.create( + name: 'child', + spanContext: foreignTrace, + parentSpan: parent, + instrumentationScope: parent.instrumentationScope, + ), + throwsA(isA().having( + (e) => e.message, + 'message', + contains('inherit trace ID'), + )), + ); + }); + + test('child span must reference the parent span ID', () { + final parent = tracer.createSpan(name: 'parent'); + final wrongParentId = OTelAPI.spanContext( + traceId: parent.spanContext.traceId, + spanId: OTelAPI.spanId(), + parentSpanId: OTelAPI.spanId(), // not the parent's span ID + ); + expect( + () => APISpanCreate.create( + name: 'child', + spanContext: wrongParentId, + parentSpan: parent, + instrumentationScope: parent.instrumentationScope, + ), + throwsA(isA().having( + (e) => e.message, + 'message', + contains('reference parent span ID'), + )), + ); + }); + + test('create throws StateError once the factory is gone', () { + final parent = tracer.createSpan(name: 'parent'); + final scope = parent.instrumentationScope; + final sc = parent.spanContext; + OTelAPI.reset(); + + expect( + () => APISpanCreate.create( + name: 'late', + spanContext: sc, + parentSpan: null, + instrumentationScope: scope, + ), + throwsStateError, + ); + expect( + () => SpanLinkCreate.create(spanContext: sc), + throwsStateError, + ); + }); + }); +} diff --git a/test/unit/api/trace/trace_api_coverage_test.dart b/test/unit/api/trace/trace_api_coverage_test.dart new file mode 100644 index 0000000..4f97987 --- /dev/null +++ b/test/unit/api/trace/trace_api_coverage_test.dart @@ -0,0 +1,177 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// Coverage for the APISpan mutation/getter surface, factory-gone +// StateErrors, ID byte validation, TraceState eviction, and tracer +// equality. + +import 'package:dartastic_opentelemetry_api/dartastic_opentelemetry_api.dart'; +import 'package:test/test.dart'; + +import '../../../test_util.dart'; + +/// An exception whose toString throws, to exercise recordException's +/// defensive message fallback. +class _ThrowsOnToString { + @override + String toString() => throw StateError('toString exploded'); +} + +void main() { + group('APISpan surface (SDK-like factory)', () { + late OTelFactory originalFactory; + late APITracer tracer; + + setUp(() { + OTelAPI.reset(); + OTelAPI.initialize( + endpoint: 'http://localhost:4317', + serviceName: 'test-service', + serviceVersion: '1.0.0', + ); + installSdkLikeFactory(); + originalFactory = OTelFactory.otelFactory!; + tracer = OTelAPI.tracer('coverage-tracer'); + }); + + tearDown(() { + OTelFactory.otelFactory = originalFactory; + }); + + test( + 'span getters: spanId, instrumentationScope, isValid,' + ' parentSpanContext', () { + final parent = tracer.createSpan(name: 'parent'); + final child = tracer.createSpan(name: 'child', parentSpan: parent); + + expect(child.spanId, equals(child.spanContext.spanId)); + expect(child.instrumentationScope.name, equals('coverage-tracer')); + expect(child.isValid, isTrue); + expect(child.parentSpanContext, equals(parent.spanContext)); + expect(parent.parentSpanContext, isNull); + }); + + test('setDateTimeAsStringAttribute stores the formatted time', () { + final span = tracer.createSpan(name: 'timed'); + final when = DateTime.utc(2026, 7, 18, 12, 30); + span.setDateTimeAsStringAttribute('happened.at', when); + expect(span.attributes.getString('happened.at'), isNotNull); + }); + + test('addEvents adds one event per map entry', () { + final span = tracer.createSpan(name: 'eventful'); + span.addEvents({ + 'first': null, + 'second': Attributes.of({'k': 'v'}), + }); + expect(span.spanEvents, hasLength(2)); + expect(span.spanEvents!.map((e) => e.name), + containsAll(['first', 'second'])); + }); + + test('addLink and addSpanLink append links', () { + final span = tracer.createSpan(name: 'linked'); + final other = OTelAPI.spanContext( + traceId: OTelAPI.traceId(), + spanId: OTelAPI.spanId(), + ); + span.addLink(other, Attributes.of({'why': 'related'})); + span.addSpanLink(OTelAPI.spanLink(other, Attributes.of({'why': 'also'}))); + expect(span.spanLinks, hasLength(2)); + }); + + test('recordException survives a throwing toString', () { + final span = tracer.createSpan(name: 'exceptional'); + span.recordException(_ThrowsOnToString()); + expect(span.spanEvents!.any((e) => e.name == 'exception'), isTrue); + }); + + test('span mutators throw StateError after reset', () { + final span = tracer.createSpan(name: 'orphaned'); + final linkTarget = OTelAPI.spanContext( + traceId: OTelAPI.traceId(), + spanId: OTelAPI.spanId(), + ); + OTelAPI.reset(); + expect(() => span.addEventNow('late'), throwsStateError); + expect(() => span.addEvents({'late': null}), throwsStateError); + expect(() => span.addLink(linkTarget), throwsStateError); + }); + }); + + group('Trace API surface (API factory)', () { + setUp(() { + OTelAPI.reset(); + OTelAPI.initialize( + endpoint: 'http://localhost:4317', + serviceName: 'test-service', + serviceVersion: '1.0.0', + ); + }); + + test('NonRecordingSpan requires an installed factory', () { + final sc = OTelAPI.spanContext( + traceId: OTelAPI.traceId(), + spanId: OTelAPI.spanId(), + ); + OTelAPI.reset(); + expect(() => NonRecordingSpan(sc), throwsStateError); + }); + + test('spanIdFromBytes and traceIdFromBytes validate length', () { + expect(() => OTelAPI.spanIdFromBytes([1, 2, 3]), throwsArgumentError); + expect(() => OTelAPI.traceIdFromBytes([1, 2, 3]), throwsArgumentError); + }); + + test('SpanId.invalidSpanId is all zeros', () { + expect(SpanId.invalidSpanId.toString(), + equals(OTelAPI.spanIdInvalid().toString())); + }); + + test('TraceState.put evicts the oldest entry at the 32-entry limit', () { + final full = { + for (var i = 0; i < 32; i++) 'key$i': 'value$i', + }; + final traceState = OTelAPI.traceState(full); + final evicted = traceState.put('overflow', 'v'); + + expect(evicted.get('overflow'), equals('v')); + expect(evicted.get('key0'), isNull, reason: 'oldest entry is evicted'); + expect(evicted.entries, hasLength(32)); + }); + + test('tracers with the same identity are equal', () { + final t1 = OTelAPI.tracerProvider('tp-a') + .getTracer('t', version: '1.0', schemaUrl: 'https://s'); + final t2 = OTelAPI.tracerProvider('tp-b') + .getTracer('t', version: '1.0', schemaUrl: 'https://s'); + final other = OTelAPI.tracerProvider('tp-a').getTracer('different'); + + expect(identical(t1, t2), isFalse); + expect(t1, equals(t2)); + expect(t1.hashCode, equals(t2.hashCode)); + expect(t1, isNot(equals(other))); + }); + + test( + 'createSpan with an empty root context returns an invalid' + ' non-recording span', () { + final span = OTelAPI.tracer('no-sdk') + .createSpan(name: 'rootless', context: Context.root); + expect(span, isA()); + expect(span.spanContext.isValid, isFalse); + }); + + test('createSpan carries a context-only SpanContext through unchanged', () { + final sc = OTelAPI.spanContext( + traceId: OTelAPI.traceId(), + spanId: OTelAPI.spanId(), + ); + final ctx = Context.root.withSpanContext(sc); + final span = + OTelAPI.tracer('no-sdk').createSpan(name: 'remote', context: ctx); + expect(span, isA()); + expect(span.spanContext, equals(sc)); + }); + }); +} diff --git a/test/unit/util/web_time_provider_stub_test.dart b/test/unit/util/web_time_provider_stub_test.dart new file mode 100644 index 0000000..c6476bd --- /dev/null +++ b/test/unit/util/web_time_provider_stub_test.dart @@ -0,0 +1,17 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +// On native targets the conditional facade exports this stub, whose +// whole contract is to throw: WebTimeProvider requires a web target. + +@TestOn('vm') +library; + +import 'package:dartastic_opentelemetry_api/src/util/web_time_provider_stub.dart'; +import 'package:test/test.dart'; + +void main() { + test('WebTimeProvider cannot be constructed on native targets', () { + expect(WebTimeProvider.new, throwsUnsupportedError); + }); +} diff --git a/tool/coverage.sh b/tool/coverage.sh index 523138c..2d62f9e 100755 --- a/tool/coverage.sh +++ b/tool/coverage.sh @@ -1,6 +1,11 @@ #!/bin/bash -# Ensure the coverage directory exists +# Always operate from the repo root, wherever the script is invoked from. +cd "$(dirname "$0")/.." || exit 1 + +# Start from a clean slate: stale coverage JSON from earlier runs gets +# merged by format_coverage and corrupts line data and totals. +rm -rf coverage mkdir -p coverage # Run tests with coverage