diff --git a/lib/src/backend/api/request.dart b/lib/src/backend/api/request.dart index b958d63..4dad8f2 100644 --- a/lib/src/backend/api/request.dart +++ b/lib/src/backend/api/request.dart @@ -8,32 +8,31 @@ import 'package:engelsburg_planer/src/backend/api/api_response.dart'; import 'package:engelsburg_planer/src/backend/api/request_service.dart'; import 'package:engelsburg_planer/src/backend/api/requests.dart'; import 'package:engelsburg_planer/src/backend/util/query.dart'; +import 'package:firebase_performance/firebase_performance.dart' show HttpMethod; import 'package:flutter/material.dart'; import 'package:http/http.dart'; typedef Parser = T Function(dynamic json); -enum HttpMethod { get, head, post, put, patch, delete } - Parser ignore = (json) {}; Parser json = (json) => json; HttpMethod httpMethodFromString(String method) { switch (method) { case "get": - return HttpMethod.get; + return HttpMethod.Get; case "head": - return HttpMethod.head; + return HttpMethod.Head; case "put": - return HttpMethod.put; + return HttpMethod.Put; case "post": - return HttpMethod.post; + return HttpMethod.Post; case "patch": - return HttpMethod.patch; + return HttpMethod.Patch; case "delete": - return HttpMethod.delete; + return HttpMethod.Delete; default: - return HttpMethod.get; + return HttpMethod.Get; } } @@ -66,8 +65,6 @@ class Request { /// Key to cache response of request final String? cacheId; - final void Function(RequestAnalysis)? analytics; - const Request( this.method, this.host, @@ -77,7 +74,6 @@ class Request { this.headers, this.https, this.cacheId, - this.analytics, ); /// Deserialize request @@ -90,7 +86,6 @@ class Request { json["headers"], json["https"] == 1, json["cacheId"], - json["analytics"], ); /// Serialize request @@ -103,7 +98,6 @@ class Request { "headers": headers, "https": https ? 1 : 0, //Sqflite doesn't support bool "cacheId": cacheId, - "analytics": analytics, }; /// Get RequestBuilder @@ -136,7 +130,6 @@ class RequestBuilder { Map _headers = {}; bool _https = false; String? _cacheId; - void Function(RequestAnalysis)? _analytics; /// Build the actual request Request build() { @@ -154,7 +147,6 @@ class RequestBuilder { _headers, _https, _cacheId, - _analytics, ); } @@ -165,17 +157,17 @@ class RequestBuilder { } /// Shorthands to set http method - RequestBuilder get get => method(HttpMethod.get); + RequestBuilder get get => method(HttpMethod.Get); - RequestBuilder get head => method(HttpMethod.head); + RequestBuilder get head => method(HttpMethod.Head); - RequestBuilder get post => method(HttpMethod.post); + RequestBuilder get post => method(HttpMethod.Post); - RequestBuilder get patch => method(HttpMethod.patch); + RequestBuilder get patch => method(HttpMethod.Patch); - RequestBuilder get put => method(HttpMethod.put); + RequestBuilder get put => method(HttpMethod.Put); - RequestBuilder get delete => method(HttpMethod.delete); + RequestBuilder get delete => method(HttpMethod.Delete); /// Set host RequestBuilder host(String host) { @@ -233,10 +225,4 @@ class RequestBuilder { _cacheId = cacheId; return this; } - - /// Set callback to execute analytics call - RequestBuilder analytics(void Function(RequestAnalysis) analytics) { - _analytics = analytics; - return this; - } } diff --git a/lib/src/backend/api/request_service.dart b/lib/src/backend/api/request_service.dart index 1178b07..77ef1c0 100644 --- a/lib/src/backend/api/request_service.dart +++ b/lib/src/backend/api/request_service.dart @@ -7,6 +7,7 @@ import 'package:engelsburg_planer/src/backend/api/request.dart'; import 'package:engelsburg_planer/src/backend/api/requests.dart'; import 'package:engelsburg_planer/src/backend/database/state/network_state.dart'; import 'package:engelsburg_planer/src/utils/global_context.dart'; +import 'package:firebase_performance/firebase_performance.dart'; import 'package:flutter/cupertino.dart'; import 'package:http/http.dart' as http; import 'package:provider/provider.dart'; @@ -33,17 +34,21 @@ class RequestService { /// /// Will return empty body and status 0 after defined (default 5) timeout static Future execute(Request request, {Duration timeout = kTimeout}) async { - var stopwatch = Stopwatch()..start(); //Set network status loading globalContext().read().update(NetworkStatus.loading); //Set default headers if api request if (request.host == Host.api) request.headers.addAll(defaultApiHeaders); - //Set authorization header if request is an authenticated one //Append Hash-header if needed (modified check) request = appendModifiedCheck(request); - var prepareTime = stopwatch.elapsedMilliseconds; + + //Log performance of request + HttpMetric metric = FirebasePerformance.instance.newHttpMetric( + request.toUri().toString(), + request.method, + ); + metric.start(); //Execute request //TODO? create retry service? (callback?), exponential backoff? @@ -52,25 +57,22 @@ class RequestService { .onError((error, stackTrace) => http.Response("", 999)) .timeout(timeout, onTimeout: () => http.Response("", 999)) .then((response) { - stopwatch.stop(); + metric.httpResponseCode = response.statusCode; + metric.responseContentType = response.headers["Content-Type"]; + metric.requestPayloadSize = response.request?.contentLength; - RequestAnalysis analysis; if (response.statusCode == 999) { - globalContext().read().update(NetworkStatus.offline); + metric.responsePayloadSize = 0; + metric.stop(); - analysis = RequestAnalysis.timedOut(prepareTime); + globalContext().read().update(NetworkStatus.offline); } else { + metric.responsePayloadSize = response.contentLength; + metric.stop(); + //If request wasn't timed out then set network status as online globalContext().read().update(NetworkStatus.online); - - analysis = RequestAnalysis( - prepareTime: prepareTime, - responseTime: stopwatch.elapsedMilliseconds - prepareTime, - responseSize: response.contentLength, - timedOut: false, - ); } - request.analytics?.call(analysis); return response; }); @@ -85,18 +87,20 @@ class RequestService { //Execute request switch (request.method) { - case HttpMethod.get: + case HttpMethod.Get: return await http.get(uri, headers: headers); //GET - case HttpMethod.head: + case HttpMethod.Head: return await http.head(uri, headers: headers); //HEAD - case HttpMethod.post: + case HttpMethod.Post: return await http.post(uri, headers: headers, body: body); //POST - case HttpMethod.put: + case HttpMethod.Put: return await http.put(uri, headers: headers, body: body); //PUT - case HttpMethod.patch: + case HttpMethod.Patch: return await http.patch(uri, headers: headers, body: body); //PATCH - case HttpMethod.delete: + case HttpMethod.Delete: return await http.delete(uri, headers: headers, body: body); //DELETE + default: + throw FlutterError("Unsupported http method: ${request.method}"); } } @@ -117,23 +121,3 @@ class RequestService { */ } } - -@immutable -class RequestAnalysis { - final int prepareTime; - final int? responseTime; - final int? responseSize; - final bool timedOut; - - const RequestAnalysis({ - required this.prepareTime, - required this.responseTime, - required this.responseSize, - required this.timedOut, - }) : assert(timedOut || responseTime != null); - - const RequestAnalysis.timedOut(this.prepareTime) - : timedOut = true, - responseTime = null, - responseSize = null; -} diff --git a/lib/src/backend/api/requests.dart b/lib/src/backend/api/requests.dart index b5e015c..3f3d78e 100644 --- a/lib/src/backend/api/requests.dart +++ b/lib/src/backend/api/requests.dart @@ -7,7 +7,6 @@ import 'package:engelsburg_planer/src/backend/api/model/dto/update_notification_ import 'package:engelsburg_planer/src/backend/api/request.dart'; import 'package:engelsburg_planer/src/backend/util/paging.dart'; import 'package:engelsburg_planer/src/backend/util/query.dart'; -import 'package:engelsburg_planer/src/services/firebase/analytics.dart'; import 'package:flutter/foundation.dart'; /// @@ -77,15 +76,12 @@ RequestBuilder getArticles({int? date, Paging paging = const Paging(0, 20)}) => .get .path(Path.article) .params(Query.paging(paging) + Query.date(date)) - .cache("articles_${paging.page}_${paging.size}_$date") - .analytics(Analytics.api.article); + .cache("articles_${paging.page}_${paging.size}_$date"); -RequestBuilder getArticle(int id) => Request.builder().https.api.get.path("${Path.article}/$id") - .analytics(Analytics.api.article); +RequestBuilder getArticle(int id) => Request.builder().https.api.get.path("${Path.article}/$id"); RequestBuilder getChangedArticles(List hashes) => - Request.builder().https.api.patch.path(Path.article).jsonBody({"hashes": hashes}) - .analytics(Analytics.api.article); + Request.builder().https.api.patch.path(Path.article).jsonBody({"hashes": hashes}); /// /// Substitutes @@ -105,8 +101,7 @@ RequestBuilder getSubstitutes( classes: classes, teacher: teacher, )) - .cache("substitutes-$classes-$teacher") - .analytics(Analytics.api.substitute); + .cache("substitutes-$classes-$teacher"); RequestBuilder getSubstituteMessages(String substituteKey) => Request.builder() .https @@ -114,12 +109,10 @@ RequestBuilder getSubstituteMessages(String substituteKey) => Request.builder() .get .path(Path.substitute.messages) .params(Query.substitutes(substituteKey)) - .cache("substitute_messages") - .analytics(Analytics.api.substitute); + .cache("substitute_messages"); RequestBuilder getSubstituteKeyHash() => - Request.builder().https.api.get.path(Path.substitute.keyHash).cache("substitute_key_hash") - .analytics(Analytics.api.substitute); + Request.builder().https.api.get.path(Path.substitute.keyHash).cache("substitute_key_hash"); /// /// Information @@ -131,8 +124,7 @@ RequestBuilder getTeacher(String substituteKey) => Request.builder() .get .path(Path.info.teacher) .params(Query.substitutes(substituteKey)) - .cache("info_teacher") - .analytics(Analytics.api.info); + .cache("info_teacher"); RequestBuilder getClasses(String substituteKey) => Request.builder() .https @@ -140,8 +132,7 @@ RequestBuilder getClasses(String substituteKey) => Request.builder() .get .path(Path.info.classes) .params(Query.substitutes(substituteKey)) - .cache("info_classes") - .analytics(Analytics.api.info); + .cache("info_classes"); /// /// Notification @@ -152,23 +143,18 @@ RequestBuilder updateNotificationSettings(String token, Iterable priorit .api .post .path(Path.notificationSettings) - .jsonBody(UpdateNotificationSettingsDTO(token: token, priorityTopics: priorityTopics)) - .analytics(Analytics.api.notificationSettings); + .jsonBody(UpdateNotificationSettingsDTO(token: token, priorityTopics: priorityTopics)); RequestBuilder deleteNotificationSettings(String token) => - Request.builder().https.api.delete.path(Path.notificationSettings).param("token", token) - .analytics(Analytics.api.notificationSettings); + Request.builder().https.api.delete.path(Path.notificationSettings).param("token", token); /// /// Other /// -RequestBuilder getEvents() => Request.builder().https.api.get.path(Path.event).cache("event") - .analytics(Analytics.api.events); +RequestBuilder getEvents() => Request.builder().https.api.get.path(Path.event).cache("event"); RequestBuilder getCafeteria() => - Request.builder().https.api.get.path(Path.cafeteria).cache("cafeteria") - .analytics(Analytics.api.cafeteria); + Request.builder().https.api.get.path(Path.cafeteria).cache("cafeteria"); RequestBuilder getSolarSystem() => - Request.builder().https.api.get.path(Path.solarSystem).cache("solar_system") - .analytics(Analytics.api.solar); + Request.builder().https.api.get.path(Path.solarSystem).cache("solar_system"); diff --git a/lib/src/services/firebase/analytics.dart b/lib/src/services/firebase/analytics.dart index a2a7baa..cecf43f 100644 --- a/lib/src/services/firebase/analytics.dart +++ b/lib/src/services/firebase/analytics.dart @@ -3,7 +3,6 @@ */ import 'package:engelsburg_planer/src/backend/api/model/article.dart'; -import 'package:engelsburg_planer/src/backend/api/request_service.dart'; import 'package:engelsburg_planer/src/backend/database/nosql/storage/interceptor/analytics_interceptor.dart'; import 'package:engelsburg_planer/src/backend/database/state/app_state.dart'; import 'package:engelsburg_planer/src/utils/util.dart'; @@ -20,7 +19,6 @@ class Analytics { static final UserAnalytics user = UserAnalytics(_analytics); static final InteractionAnalytics interaction = InteractionAnalytics(_analytics); - static final ApiAnalytics api = ApiAnalytics(_analytics); static void initialize() => _analytics.setDefaultEventParameters({ if (kDebugMode) "debug_mode": kDebugMode.toString(), @@ -30,41 +28,6 @@ class Analytics { static void logAppOpen() => _analytics.logAppOpen(); } -/// Analytics label for api calls -class ApiAnalytics { - final FirebaseAnalytics _analytics; - - ApiAnalytics(this._analytics); - - void _request(String type, RequestAnalysis analysis) => _analytics.logEvent( - name: "api_request", - parameters: { - "api_request_type": type, - "api_request_prepare_time": analysis.prepareTime, - "api_request_timed_out": analysis.timedOut.toString(), - if (analysis.responseTime != null) - "api_response_time": analysis.responseTime, - if (analysis.responseSize != null) - "api_response_size": analysis.responseSize, - }, - ); - - void article(RequestAnalysis analysis) => _request("article", analysis); - - void substitute(RequestAnalysis analysis) => _request("substitute", analysis); - - void notificationSettings(RequestAnalysis analysis) => - _request("notification_settings", analysis); - - void cafeteria(RequestAnalysis analysis) => _request("cafeteria", analysis); - - void events(RequestAnalysis analysis) => _request("events", analysis); - - void solar(RequestAnalysis analysis) => _request("solar", analysis); - - void info(RequestAnalysis analysis) => _request("info", analysis); -} - class InteractionAnalytics { final FirebaseAnalytics _analytics;