begin static method

EdotRequestTrace? begin({
  1. required String method,
  2. required String url,
  3. required String client,
  4. int? requestSize,
})

Starts tracing a request, or returns null when it must not be traced.

Null means the transport should carry the request out untouched: the Plugin has not started, the request is to the Collector Host (ADR-0006), or it matches EdotConfig.excludedUrls. There is no span, so nothing to end and no Trace Context to send — an untraced request has no span for a header to name.

client names the transport and reaches http.client, which is what tells two integrations apart in a dashboard.

requestSize is the request body's size when the transport knows it. Absent rather than zero when it does not: a stream of unknown length has no size to report, and zero would read as an empty body.

Implementation

static EdotRequestTrace? begin({
  required String method,
  required String url,
  required String client,
  int? requestSize,
}) {
  final rules = tracingRules;

  if (rules == null) {
    // Deliberately not buffered like other pre-start telemetry: without rules this
    // cannot tell the Collector Host from any other host, and tracing the Agent's own
    // export traffic is what ADR-0006 exists to prevent. Carrying on beats throwing —
    // a transport is usually wired up during startup, and failing the app's first
    // request over telemetry would be a poor trade.
    //
    // Sanitised even here, and even though no custom hook is configured yet: a
    // debug log is somewhere the URL is recorded, and the query string it would
    // otherwise carry is the reason the sanitiser exists.
    edotLog('request before Edot.start; not traced: ${sanitizeUrl(url)}');
    return null;
  }

  // Both exclusions match the URL as given, before sanitising — a pattern written
  // against a query parameter has to be able to see it, and an excluded request
  // produces no span, so nothing is recorded either way.
  if (isCollectorHost(url, rules.collectorHost)) return null;
  if (isExcluded(url, rules.excludedUrls)) return null;

  // Everything downstream reads the sanitised URL, never the original — including
  // the span name, which carries the host. A sanitiser that rewrites the authority
  // has to be reflected there too, or the URL would be recorded in the one place
  // nobody thought to sanitise.
  final sanitizedUrl = sanitizeUrl(url, rules.urlSanitizer);

  final attributes = <String, String>{
    'http.method': method,
    'http.url': sanitizedUrl,
    'http.client': client,
  };

  // Each of these is absent rather than empty when the URL does not yield it. An
  // attribute present with a meaningless value is worse than none.
  final derived = <String, String?>{
    'http.target': httpTarget(sanitizedUrl),
    'http.scheme': httpScheme(sanitizedUrl),
    'net.peer.name': peerName(sanitizedUrl),
  };
  derived.forEach((key, value) {
    if (value != null) attributes[key] = value;
  });

  // Two spans, not one (ADR-0016). A request span with no parent is classified as
  // a *transaction* at intake, and a transaction never carries a destination
  // service — so Kibana has no exit span to draw a service map edge from, and no
  // dependency to aggregate. The Request Transaction gives the request one to
  // belong to, and matches the parent the iOS Agent manufactures for itself:
  // same name, same kind, the same timestamps, and no attributes of its own.
  final spans = Edot.tracer.startRequest(
    spanNameFor(method, sanitizedUrl),
    attributes: attributes,
  );
  final span = spans.request;

  // Integers cannot travel with the creation attributes, which are strings only,
  // so they follow immediately after.
  final port = peerPort(sanitizedUrl);
  if (port != null) span.setInt('net.peer.port', port);
  if (requestSize != null) span.setInt('http.request_body.size', requestSize);

  // Decided on the URL as given, for the same reason the exclusions are.
  return EdotRequestTrace._(
    spans,
    shouldPropagate(url, rules.tracePropagationTargets),
  );
}