Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: auto ip guarded by sendDefaultPii #2726

Merged
merged 12 commits into from
Feb 19, 2025
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

### Behavioral changes

- ⚠️ Auto IP assignment for `SentryUser` is now guarded by `sendDefaultPii` ([#2726](https://github.com/getsentry/sentry-dart/pull/2726))
- If you rely on Sentry automatically processing the IP address of the user, set `options.sendDefaultPii = true` or manually set the IP address of the `SentryUser` to `{{auto}}`

### Features

- Disable `ScreenshotIntegration`, `WidgetsBindingIntegration` and `SentryWidget` in multi-view apps #2366 ([#2366](https://github.com/getsentry/sentry-dart/pull/2366))
Expand Down
19 changes: 14 additions & 5 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ import 'version.dart';
/// to true.
const _defaultIpAddress = '{{auto}}';

@visibleForTesting
String get defaultIpAddress => _defaultIpAddress;

/// Logs crash reports and events to the Sentry.io service.
class SentryClient {
final SentryOptions _options;
Expand Down Expand Up @@ -304,12 +307,18 @@ class SentryClient {
}

SentryEvent _createUserOrSetDefaultIpAddress(SentryEvent event) {
var user = event.user;
if (user == null) {
return event.copyWith(user: SentryUser(ipAddress: _defaultIpAddress));
} else if (event.user?.ipAddress == null) {
return event.copyWith(user: user.copyWith(ipAddress: _defaultIpAddress));
final user = event.user;
final effectiveIpAddress =
user?.ipAddress ?? (_options.sendDefaultPii ? _defaultIpAddress : null);

if (effectiveIpAddress != null) {
final updatedUser = user == null
? SentryUser(ipAddress: effectiveIpAddress)
: user.copyWith(ipAddress: effectiveIpAddress);

return event.copyWith(user: updatedUser);
}

return event;
}

Expand Down
42 changes: 37 additions & 5 deletions dart/test/sentry_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1003,14 +1003,14 @@ void main() {
});
});

group('SentryClient: sets user & user ip', () {
group('SentryClient: user & user ip', () {
late Fixture fixture;

setUp(() {
fixture = Fixture();
});

test('event has no user', () async {
test('event has no user and sendDefaultPii = true', () async {
final client = fixture.getSut(sendDefaultPii: true);
var fakeEvent = SentryEvent();

Expand All @@ -1021,7 +1021,20 @@ void main() {

expect(fixture.transport.envelopes.length, 1);
expect(capturedEvent.user, isNotNull);
expect(capturedEvent.user?.ipAddress, '{{auto}}');
expect(capturedEvent.user?.ipAddress, defaultIpAddress);
});

test('event has no user and sendDefaultPii = false', () async {
final client = fixture.getSut(sendDefaultPii: false);
var fakeEvent = SentryEvent();

await client.captureEvent(fakeEvent);

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

expect(fixture.transport.envelopes.length, 1);
expect(capturedEvent.user, isNull);
});

test('event has a user with IP address', () async {
Expand All @@ -1040,7 +1053,8 @@ void main() {
expect(capturedEvent.user?.email, fakeEvent.user!.email);
});

test('event has a user without IP address', () async {
test('event has a user without IP address and sendDefaultPii = true',
() async {
final client = fixture.getSut(sendDefaultPii: true);

final event = fakeEvent.copyWith(user: fakeUser);
Expand All @@ -1052,7 +1066,25 @@ void main() {

expect(fixture.transport.envelopes.length, 1);
expect(capturedEvent.user, isNotNull);
expect(capturedEvent.user?.ipAddress, '{{auto}}');
expect(capturedEvent.user?.ipAddress, defaultIpAddress);
expect(capturedEvent.user?.id, fakeUser.id);
expect(capturedEvent.user?.email, fakeUser.email);
});

test('event has a user without IP address and sendDefaultPii = false',
() async {
final client = fixture.getSut(sendDefaultPii: false);

final event = fakeEvent.copyWith(user: fakeUser);

await client.captureEvent(event);

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

expect(fixture.transport.envelopes.length, 1);
expect(capturedEvent.user, isNotNull);
expect(capturedEvent.user?.ipAddress, isNull);
expect(capturedEvent.user?.id, fakeUser.id);
expect(capturedEvent.user?.email, fakeUser.email);
});
Expand Down
75 changes: 72 additions & 3 deletions flutter/test/integrations/load_contexts_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ library flutter_test;

import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:sentry/src/sentry_tracer.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_flutter/src/integrations/load_contexts_integration.dart';
import 'package:sentry/src/sentry_tracer.dart';

import 'fixture.dart';

Expand Down Expand Up @@ -86,9 +86,10 @@ void main() {
});

test(
'apply default IP to user during captureEvent after loading context if ip is null',
'apply default IP to user during captureEvent after loading context if ip is null and sendDefaultPii is true',
() async {
fixture.options.enableScopeSync = true;
fixture.options.sendDefaultPii = true;

const expectedIp = '{{auto}}';
String? actualIp;
Expand Down Expand Up @@ -118,9 +119,42 @@ void main() {
});

test(
'apply default IP to user during captureTransaction after loading context if ip is null',
'does not apply default IP to user during captureEvent after loading context if ip is null and sendDefaultPii is false',
() async {
fixture.options.enableScopeSync = true;
// sendDefaultPii false is by default

String? actualIp;

const expectedId = '1';
String? actualId;

fixture.options.beforeSend = (event, hint) {
actualIp = event.user?.ipAddress;
actualId = event.user?.id;
return event;
};

final options = fixture.options;

final user = SentryUser(id: expectedId);
when(fixture.binding.loadContexts())
.thenAnswer((_) async => {'user': user.toJson()});

final client = SentryClient(options);
final event = SentryEvent();

await client.captureEvent(event);

expect(actualIp, isNull);
expect(actualId, expectedId);
});

test(
'apply default IP to user during captureTransaction after loading context if ip is null and sendDefaultPii is true',
() async {
fixture.options.enableScopeSync = true;
fixture.options.sendDefaultPii = true;

const expectedIp = '{{auto}}';
String? actualIp;
Expand Down Expand Up @@ -151,5 +185,40 @@ void main() {
expect(actualIp, expectedIp);
expect(actualId, expectedId);
});

test(
'does not apply default IP to user during captureTransaction after loading context if ip is null and sendDefaultPii is false',
() async {
fixture.options.enableScopeSync = true;
// sendDefaultPii false is by default

String? actualIp;

const expectedId = '1';
String? actualId;

fixture.options.beforeSendTransaction = (transaction) {
actualIp = transaction.user?.ipAddress;
actualId = transaction.user?.id;
return transaction;
};

final options = fixture.options;

final user = SentryUser(id: expectedId);
when(fixture.binding.loadContexts())
.thenAnswer((_) async => {'user': user.toJson()});

final client = SentryClient(options);
final tracer =
SentryTracer(SentryTransactionContext('name', 'op'), fixture.hub);
final transaction = SentryTransaction(tracer);

// ignore: invalid_use_of_internal_member
await client.captureTransaction(transaction);

expect(actualIp, isNull);
expect(actualId, expectedId);
});
});
}
Loading