Skip to content

Commit

Permalink
Merge branch 'main' into feat/capture-http-response-body-for-sentry-h…
Browse files Browse the repository at this point in the history
…ttp-client
  • Loading branch information
martinhaintz authored Nov 13, 2024
2 parents 715ab7d + dd16d3e commit 52ecb5e
Show file tree
Hide file tree
Showing 35 changed files with 3,830 additions and 831 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/testflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 # [email protected]
- run: xcodes select 15.0.1
- uses: ruby/setup-ruby@7d3497fd78c07c0d84ebafa58d8dac60cd1f0763 # pin@v1.199.0
- uses: ruby/setup-ruby@a2bbe5b1b236842c1cb7dd11e8e3b51e0a616acc # pin@v1.202.0
with:
ruby-version: '2.7.5'
bundler-cache: true
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,37 @@
return event;
};
```
- Remove `sentry` frames if SDK falls back to current stack trace ([#2351](https://github.com/getsentry/sentry-dart/pull/2351))
- Flutter doesn't always provide stack traces for unhandled errors - this is normal Flutter behavior
- When no stack trace is provided (in Flutter errors, `captureException`, or `captureMessage`):
- SDK creates a synthetic trace using `StackTrace.current`
- Internal SDK frames are removed to reduce noise
- Original stack traces (when provided) are left unchanged

### Features

- Improve frame tracking accuracy ([#2372](https://github.com/getsentry/sentry-dart/pull/2372))
- Introduces `SentryWidgetsFlutterBinding` that tracks a frame starting from `handleBeginFrame` and ending in `handleDrawFrame`, this is approximately the [buildDuration](https://api.flutter.dev/flutter/dart-ui/FrameTiming/buildDuration.html) time
- By default, `SentryFlutter.init()` automatically initializes `SentryWidgetsFlutterBinding` through the `WidgetsFlutterBindingIntegration`
- If you need to initialize the binding before `SentryFlutter.init`, use `SentryWidgetsFlutterBinding.ensureInitialized` instead of `WidgetsFlutterBinding.ensureInitialized`:
```dart
void main() async {
// Replace WidgetsFlutterBinding.ensureInitialized()
SentryWidgetsFlutterBinding.ensureInitialized();
await SentryFlutter.init(...);
runApp(MyApp());
}
```
- ⚠️ Frame tracking will be disabled if a different binding is used

### Fixes

- Apply default IP address (`{{auto}}`) to transactions ([#2395](https://github.com/getsentry/sentry-dart/pull/2395))
- Previously, transactions weren't getting the default IP address when user context was loaded
- Now consistently applies default IP address to both events and transactions when:
- No user context exists
- User context exists but IP address is null

## 8.10.1

Expand Down
2 changes: 2 additions & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@ export 'src/spotlight.dart';
export 'src/protocol/sentry_proxy.dart';
// feedback
export 'src/protocol/sentry_feedback.dart';
// constants
export 'src/span_data_convention.dart';
29 changes: 21 additions & 8 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:math';

import 'package:meta/meta.dart';
import 'type_check_hint.dart';

import 'client_reports/client_report_recorder.dart';
import 'client_reports/discard_reason.dart';
Expand Down Expand Up @@ -126,10 +127,11 @@ class SentryClient {
return _emptySentryId;
}

SentryEvent? preparedEvent = _prepareEvent(event, stackTrace: stackTrace);

hint ??= Hint();

SentryEvent? preparedEvent =
_prepareEvent(event, hint, stackTrace: stackTrace);

if (scope != null) {
preparedEvent = await scope.applyToEvent(preparedEvent, hint);
} else {
Expand Down Expand Up @@ -211,7 +213,8 @@ class SentryClient {
return isMatchingRegexPattern(message, _options.ignoreErrors);
}

SentryEvent _prepareEvent(SentryEvent event, {dynamic stackTrace}) {
SentryEvent _prepareEvent(SentryEvent event, Hint hint,
{dynamic stackTrace}) {
event = event.copyWith(
serverName: event.serverName ?? _options.serverName,
dist: event.dist ?? _options.dist,
Expand Down Expand Up @@ -248,6 +251,7 @@ class SentryClient {
var sentryException = _exceptionFactory.getSentryException(
extractedException.exception,
stackTrace: extractedException.stackTrace,
removeSentryFrames: hint.get(TypeCheckHint.currentStackTrace),
);

SentryThread? sentryThread;
Expand Down Expand Up @@ -283,8 +287,14 @@ class SentryClient {
// therefore add it to the threads.
// https://develop.sentry.dev/sdk/event-payloads/stacktrace/
if (stackTrace != null || _options.attachStacktrace) {
stackTrace ??= getCurrentStackTrace();
final sentryStackTrace = _stackTraceFactory.parse(stackTrace);
if (stackTrace == null || stackTrace == StackTrace.empty) {
stackTrace = getCurrentStackTrace();
hint.addAll({TypeCheckHint.currentStackTrace: true});
}
final sentryStackTrace = _stackTraceFactory.parse(
stackTrace,
removeSentryFrames: hint.get(TypeCheckHint.currentStackTrace),
);
if (sentryStackTrace.frames.isNotEmpty) {
event = event.copyWith(threads: [
...?event.threads,
Expand Down Expand Up @@ -356,11 +366,11 @@ class SentryClient {
Scope? scope,
SentryTraceContextHeader? traceContext,
}) async {
SentryTransaction? preparedTransaction =
_prepareEvent(transaction) as SentryTransaction;

final hint = Hint();

SentryTransaction? preparedTransaction =
_prepareEvent(transaction, hint) as SentryTransaction;

if (scope != null) {
preparedTransaction = await scope.applyToEvent(preparedTransaction, hint)
as SentryTransaction?;
Expand Down Expand Up @@ -396,6 +406,9 @@ class SentryClient {
return _emptySentryId;
}

preparedTransaction = _createUserOrSetDefaultIpAddress(preparedTransaction)
as SentryTransaction;

preparedTransaction =
await _runBeforeSend(preparedTransaction, hint) as SentryTransaction?;

Expand Down
8 changes: 6 additions & 2 deletions dart/lib/src/sentry_exception_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class SentryExceptionFactory {
SentryException getSentryException(
dynamic exception, {
dynamic stackTrace,
bool? removeSentryFrames,
}) {
var throwable = exception;
Mechanism? mechanism;
Expand All @@ -38,17 +39,20 @@ class SentryExceptionFactory {
// throwable.stackTrace is null if its an exception that was never thrown
// hence we check again if stackTrace is null and if not, read the current stack trace
// but only if attachStacktrace is enabled

if (_options.attachStacktrace) {
if (stackTrace == null || stackTrace == StackTrace.empty) {
snapshot = true;
stackTrace = getCurrentStackTrace();
removeSentryFrames = true;
}
}

SentryStackTrace? sentryStackTrace;
if (stackTrace != null) {
sentryStackTrace =
_stacktraceFactory.parse(stackTrace).copyWith(snapshot: snapshot);
sentryStackTrace = _stacktraceFactory
.parse(stackTrace, removeSentryFrames: removeSentryFrames)
.copyWith(snapshot: snapshot);
if (sentryStackTrace.frames.isEmpty) {
sentryStackTrace = null;
}
Expand Down
16 changes: 13 additions & 3 deletions dart/lib/src/sentry_measurement.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,32 @@ class SentryMeasurement {
this.unit,
});

static const totalFramesName = 'frames_total';
static const slowFramesName = 'frames_slow';
static const frozenFramesName = 'frames_frozen';
static const framesDelayName = 'frames_delay';

/// Amount of frames drawn during a transaction
SentryMeasurement.totalFrames(this.value)
: name = 'frames_total',
: name = totalFramesName,
unit = SentryMeasurementUnit.none;

/// Amount of slow frames drawn during a transaction.
/// A slow frame is any frame longer than 1s / refreshrate.
/// So for example any frame slower than 16ms for a refresh rate of 60hz.
SentryMeasurement.slowFrames(this.value)
: name = 'frames_slow',
: name = slowFramesName,
unit = SentryMeasurementUnit.none;

/// Amount of frozen frames drawn during a transaction.
/// Typically defined as frames slower than 500ms.
SentryMeasurement.frozenFrames(this.value)
: name = 'frames_frozen',
: name = frozenFramesName,
unit = SentryMeasurementUnit.none;

/// Total duration of frames delayed.
SentryMeasurement.framesDelay(this.value)
: name = framesDelayName,
unit = SentryMeasurementUnit.none;

/// Duration of the Cold App start in milliseconds
Expand Down
8 changes: 4 additions & 4 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -590,13 +590,13 @@ class SentryOptions {
}

/// Adds an inAppExclude
void addInAppExclude(String inApp) {
_inAppExcludes.add(inApp);
void addInAppExclude(String inAppInclude) {
_inAppExcludes.add(inAppInclude);
}

/// Adds an inAppIncludes
void addInAppInclude(String inApp) {
_inAppIncludes.add(inApp);
void addInAppInclude(String inAppExclude) {
_inAppIncludes.add(inAppExclude);
}

/// Returns if tracing should be enabled. If tracing is disabled, starting transactions returns
Expand Down
15 changes: 11 additions & 4 deletions dart/lib/src/sentry_stack_trace_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,26 @@ class SentryStackTraceFactory {
return parse(stackTrace).frames;
}

SentryStackTrace parse(dynamic stackTrace) {
SentryStackTrace parse(dynamic stackTrace, {bool? removeSentryFrames}) {
final parsed = _parseStackTrace(stackTrace);
final frames = <SentryStackFrame>[];
var onlyAsyncGap = true;

for (var t = 0; t < parsed.traces.length; t += 1) {
final trace = parsed.traces[t];

// NOTE: We want to keep the Sentry frames for crash detection
// NOTE: We want to keep the Sentry frames for SDK crash detection
// this does not affect grouping since they're not marked as inApp
// only exception if there was no stack trace, we remove them
for (final frame in trace.frames) {
final stackTraceFrame = encodeStackTraceFrame(frame);
var stackTraceFrame = encodeStackTraceFrame(frame);

if (stackTraceFrame != null) {
if (removeSentryFrames == true &&
(stackTraceFrame.package == 'sentry' ||
stackTraceFrame.package == 'sentry_flutter')) {
continue;
}
frames.add(stackTraceFrame);
onlyAsyncGap = false;
}
Expand Down Expand Up @@ -100,7 +107,7 @@ class SentryStackTraceFactory {
final member = frame.member;

if (frame is UnparsedFrame && member != null) {
// if --split-debug-info is enabled, thats what we see:
// if --split-debug-info is enabled, that's what we see:
// #00 abs 000000723d6346d7 _kDartIsolateSnapshotInstructions+0x1e26d7

// we are only interested on the #01, 02... items which contains the 'abs' addresses.
Expand Down
10 changes: 10 additions & 0 deletions dart/lib/src/span_data_convention.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class SpanDataConvention {
SpanDataConvention._();

static const totalFrames = 'frames.total';
static const slowFrames = 'frames.slow';
static const frozenFrames = 'frames.frozen';
static const framesDelay = 'frames.delay';

// TODO: eventually add other data keys here as well
}
3 changes: 3 additions & 0 deletions dart/lib/src/type_check_hint.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ class TypeCheckHint {

/// Widget that was tapped in `sentry_flutter/SentryUserInteractionWidget`
static const widget = 'widget';

/// Used to indicate that the SDK added a synthetic current stack trace.
static const currentStackTrace = 'currentStackTrace';
}
88 changes: 86 additions & 2 deletions dart/test/sentry_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,38 @@ void main() {
true,
);
});

test('should remove sentry frames if null stackStrace', () async {
final throwable = Object();

final client = fixture.getSut(attachStacktrace: true);
await client.captureException(throwable, stackTrace: null);

final capturedEnvelope = (fixture.transport).envelopes.first;
final capturedEvent = await eventFromEnvelope(capturedEnvelope);

final sentryFramesCount = capturedEvent.exceptions?[0].stackTrace!.frames
.where((frame) => frame.package == 'sentry')
.length;

expect(sentryFramesCount, 0);
});

test('should remove sentry frames if empty stackStrace', () async {
final throwable = Object();

final client = fixture.getSut(attachStacktrace: true);
await client.captureException(throwable, stackTrace: StackTrace.empty);

final capturedEnvelope = (fixture.transport).envelopes.first;
final capturedEvent = await eventFromEnvelope(capturedEnvelope);

final sentryFramesCount = capturedEvent.exceptions?[0].stackTrace!.frames
.where((frame) => frame.package == 'sentry')
.length;

expect(sentryFramesCount, 0);
});
});

group('SentryClient captures transaction', () {
Expand Down Expand Up @@ -2069,10 +2101,62 @@ void main() {

final capturedEnvelope = (fixture.transport).envelopes.first;
final attachmentItem = capturedEnvelope.items.firstWhereOrNull(
(element) => element.header.type == SentryItemType.attachment);

(element) => element.header.type == SentryItemType.attachment,
);
expect(attachmentItem, isNull);
});

test(
'null stack trace marked in hint & sentry frames removed from thread stackTrace',
() async {
final beforeSendCallback = (SentryEvent event, Hint hint) {
expect(hint.get(TypeCheckHint.currentStackTrace), isTrue);
return event;
};
final client = fixture.getSut(
beforeSend: beforeSendCallback, attachStacktrace: true);
await client.captureEvent(fakeEvent);

final capturedEnvelope = (fixture.transport).envelopes.first;
final capturedEvent = await eventFromEnvelope(capturedEnvelope);

final sentryFramesCount = capturedEvent.threads?[0].stacktrace!.frames
.where((frame) => frame.package == 'sentry')
.length;

expect(sentryFramesCount, 0);
});

test(
'empty stack trace marked in hint & sentry frames removed from thread stackTrace',
() async {
final beforeSendCallback = (SentryEvent event, Hint hint) {
expect(hint.get(TypeCheckHint.currentStackTrace), isTrue);
return event;
};
final client = fixture.getSut(
beforeSend: beforeSendCallback, attachStacktrace: true);
await client.captureEvent(fakeEvent, stackTrace: StackTrace.empty);

final capturedEnvelope = (fixture.transport).envelopes.first;
final capturedEvent = await eventFromEnvelope(capturedEnvelope);

final sentryFramesCount = capturedEvent.threads?[0].stacktrace!.frames
.where((frame) => frame.package == 'sentry')
.length;

expect(sentryFramesCount, 0);
});

test('non-null stack trace not marked in hint', () async {
final beforeSendCallback = (SentryEvent event, Hint hint) {
expect(hint.get(TypeCheckHint.currentStackTrace), isNull);
return event;
};
final client = fixture.getSut(
beforeSend: beforeSendCallback, attachStacktrace: true);
await client.captureEvent(fakeEvent, stackTrace: StackTrace.current);
});
});

group('Capture metrics', () {
Expand Down
Loading

0 comments on commit 52ecb5e

Please sign in to comment.