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

Conversations Part 1: Ping #209

Merged
merged 21 commits into from
Jul 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,4 @@ pubspec.lock
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
coverage
67 changes: 41 additions & 26 deletions lib/src/core/network/wiredash_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,11 @@ class WiredashApi {

final response = await _send(req);

if (response.statusCode == 401) {
throw UnauthenticatedWiredashApiException(response, _projectId, _secret);
}

if (response.statusCode != 200) {
throw WiredashApiException(
message: '$type upload failed',
response: response,
);
if (response.statusCode == 200) {
final map = jsonDecode(response.body) as Map<String, dynamic>;
return AttachmentId(map['id'] as String);
}

final map = jsonDecode(response.body) as Map<String, dynamic>;
return AttachmentId(map['id'] as String);
_parseResponseForErrors(response);
}

/// Reports a feedback
Expand All @@ -97,13 +89,7 @@ class WiredashApi {
// success 🎉
return;
}
if (response.statusCode == 401) {
throw UnauthenticatedWiredashApiException(response, _projectId, _secret);
}
throw WiredashApiException(
message: 'submitting feedback failed',
response: response,
);
_parseResponseForErrors(response);
}

Future<void> sendNps(NpsRequestBody body) async {
Expand All @@ -119,13 +105,17 @@ class WiredashApi {
// success 🎉
return;
}
if (response.statusCode == 401) {
throw UnauthenticatedWiredashApiException(response, _projectId, _secret);
_parseResponseForErrors(response);
}

Future<PingResponse> ping() async {
final uri = Uri.parse('$_host/ping');
final Request request = Request('POST', uri);
final response = await _send(request);
if (response.statusCode == 200) {
return PingResponse();
}
throw WiredashApiException(
message: 'submitting nps failed',
response: response,
);
_parseResponseForErrors(response);
}

/// Sends a [BaseRequest] after attaching HTTP headers
Expand All @@ -138,6 +128,16 @@ class WiredashApi {
final streamedResponse = await _httpClient.send(request);
return Response.fromStream(streamedResponse);
}

Never _parseResponseForErrors(Response response) {
if (response.statusCode == 401) {
throw UnauthenticatedWiredashApiException(response, _projectId, _secret);
}
if (response.statusCode == 403) {
throw KillSwitchException(response: response);
}
throw WiredashApiException(response: response);
}
}

extension UploadScreenshotApi on WiredashApi {
Expand All @@ -156,7 +156,7 @@ extension UploadScreenshotApi on WiredashApi {

/// Generic error from the Wiredash API
class WiredashApiException implements Exception {
WiredashApiException({this.message, this.response});
const WiredashApiException({this.message, this.response});

String? get messageFromServer {
try {
Expand Down Expand Up @@ -189,6 +189,7 @@ class WiredashApiException implements Exception {
String toString() {
return 'WiredashApiException{'
'"$message", '
'endpoint: ${response?.request?.url.path}, '
'code: ${response?.statusCode}, '
'resp: $messageFromServer'
'}';
Expand Down Expand Up @@ -528,3 +529,17 @@ class NpsRequestBody {
return body;
}
}

class PingResponse {
// Nothing in here just yet but that will change in the future
PingResponse();
}

/// Backend returns an error which silences the SDK for one week
class KillSwitchException extends WiredashApiException {
const KillSwitchException({Response? response}) : super(response: response);
@override
String toString() {
return 'KillSwitchException{${response?.statusCode}, body: ${response?.body}}';
}
}
34 changes: 34 additions & 0 deletions lib/src/core/services/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wiredash/src/core/network/wiredash_api.dart';
import 'package:wiredash/src/core/options/wiredash_options_data.dart';
import 'package:wiredash/src/core/project_credential_validator.dart';
import 'package:wiredash/src/core/services/streampod.dart';
import 'package:wiredash/src/core/sync/ping_job.dart';
import 'package:wiredash/src/core/sync/sync_engine.dart';
import 'package:wiredash/src/core/sync/sync_feedback_job.dart';
import 'package:wiredash/src/core/widgets/backdrop/wiredash_backdrop.dart';
import 'package:wiredash/src/core/wiredash_model.dart';
import 'package:wiredash/src/core/wiredash_widget.dart';
Expand Down Expand Up @@ -60,10 +64,14 @@ class WiredashServices extends ChangeNotifier {

WiredashApi get api => _locator.get();

SyncEngine get syncEngine => _locator.get();

DiscardFeedbackUseCase get discardFeedback => _locator.get();

DiscardNpsUseCase get discardNps => _locator.get();

ProjectCredentialValidator get projectCredentialValidator => _locator.get();

void updateWidget(Wiredash wiredashWidget) {
inject<Wiredash>((_) => wiredashWidget);
}
Expand Down Expand Up @@ -103,6 +111,9 @@ void _setupServices(WiredashServices sl) {
);
sl.inject<DeviceIdGenerator>((_) => DeviceIdGenerator());
sl.inject<BuildInfoManager>((_) => BuildInfoManager());
sl.inject<ProjectCredentialValidator>(
(_) => const ProjectCredentialValidator(),
);
sl.inject<BackdropController>(
(_) => BackdropController(),
dispose: (model) => model.dispose(),
Expand Down Expand Up @@ -166,6 +177,29 @@ void _setupServices(WiredashServices sl) {
},
);

sl.inject<SyncEngine>(
(locator) {
final engine = SyncEngine();

engine.addJob(
'ping',
PingJob(
api: locator.api,
sharedPreferencesProvider: SharedPreferences.getInstance,
),
);
engine.addJob(
'feedback',
UploadPendingFeedbackJob(
feedbackSubmitter: locator.feedbackSubmitter,
),
);

return engine;
},
dispose: (engine) => engine.onWiredashDispose(),
);

sl.inject<DiscardFeedbackUseCase>((_) => DiscardFeedbackUseCase(sl));
sl.inject<DiscardNpsUseCase>((_) => DiscardNpsUseCase(sl));
}
Expand Down
9 changes: 9 additions & 0 deletions lib/src/core/services/streampod.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@
class Locator {
final Map<Type, InstanceFactory> _registry = {};

bool _disposed = false;

void dispose() {
for (final item in _registry.values) {
item.dispose?.call();
}
_disposed = true;
}

/// Retrieve a instance of type [T]
T get<T>() {
if (_disposed) {
throw Exception('Locator is disposed');
}
final provider = _registry[T];
return provider!.instance as T;
}
Expand All @@ -22,6 +28,9 @@ class Locator {
T Function(Locator, T oldInstance)? update,
void Function(T)? dispose,
}) {
if (_disposed) {
throw Exception('Locator is disposed');
}
late InstanceFactory<T> provider;
provider = InstanceFactory(this, create, update, () {
final instance = provider._instance;
Expand Down
101 changes: 101 additions & 0 deletions lib/src/core/sync/ping_job.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import 'package:clock/clock.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wiredash/src/core/network/wiredash_api.dart';
import 'package:wiredash/src/core/sync/sync_engine.dart';

class PingJob extends Job {
final WiredashApi api;
final Future<SharedPreferences> Function() sharedPreferencesProvider;

PingJob({
required this.api,
required this.sharedPreferencesProvider,
});

static const lastSuccessfulPingKey = 'io.wiredash.last_successful_ping';
static const silenceUntilKey = 'io.wiredash.silence_ping_until';

static const minPingGap = Duration(hours: 3);
static const killSwitchSilenceDuration = Duration(days: 7);

@override
bool shouldExecute(SdkEvent event) {
return event == SdkEvent.appStart;
}

@override
Future<void> execute() async {
final now = clock.now();

if (await _isSilenced(now)) {
// Received kill switch message, don't ping at all
syncDebugPrint('Sdk silenced, preventing ping');
return;
}

final lastPing = await _getLastSuccessfulPing();
if (lastPing != null && now.difference(lastPing) <= minPingGap) {
syncDebugPrint(
'Not syncing because within minSyncGapWindow\n'
'now: $now lastPing:$lastPing\n'
'diff (${now.difference(lastPing)}) <= minSyncGap ($minPingGap)',
);
// don't ping too often on app start, only once every minPingGap
return;
}

try {
await api.ping();
await _saveLastSuccessfulPing(now);
syncDebugPrint('ping');
} on KillSwitchException catch (_) {
// Server explicitly asks the SDK to be silent
final earliestNextPing = now.add(killSwitchSilenceDuration);
await _silenceUntil(earliestNextPing);
syncDebugPrint('Silenced Wiredash until $earliestNextPing');
} catch (e, stack) {
// Any other error, like network errors or server errors, silence the ping
// for a while, too
syncDebugPrint('Received an unknown error for ping');
syncDebugPrint(e);
syncDebugPrint(stack);
}
}

/// Silences the sdk, prevents automatic pings on app startup until the time is over
Future<void> _silenceUntil(DateTime dateTime) async {
final preferences = await sharedPreferencesProvider();
preferences.setInt(silenceUntilKey, dateTime.millisecondsSinceEpoch);
syncDebugPrint('Silenced Wiredash until $dateTime');
}

/// `true` when automatic pings should be prevented
Future<bool> _isSilenced(DateTime now) async {
final preferences = await sharedPreferencesProvider();

final int? millis = preferences.getInt(silenceUntilKey);
if (millis == null) {
return false;
}
final silencedUntil = DateTime.fromMillisecondsSinceEpoch(millis);
final silenced = silencedUntil.isAfter(now);
if (silenced) {
syncDebugPrint("Sdk is silenced until $silencedUntil (now $now)");
}
return silenced;
}

Future<DateTime?> _getLastSuccessfulPing() async {
final preferences = await sharedPreferencesProvider();
final lastPingInt = preferences.getInt(lastSuccessfulPingKey);
if (lastPingInt == null) {
return null;
}
return DateTime.fromMillisecondsSinceEpoch(lastPingInt);
}

Future<void> _saveLastSuccessfulPing(DateTime now) async {
final preferences = await sharedPreferencesProvider();
await preferences.setInt(lastSuccessfulPingKey, now.millisecondsSinceEpoch);
}
}
Loading