diff --git a/src/PKHeX.Web/BackendApi/Repositories/UserRepository.cs b/src/PKHeX.Web/BackendApi/Repositories/UserRepository.cs index 5b8ba07..b72e197 100644 --- a/src/PKHeX.Web/BackendApi/Repositories/UserRepository.cs +++ b/src/PKHeX.Web/BackendApi/Repositories/UserRepository.cs @@ -14,7 +14,7 @@ public async Task GetSignedInUser() if (_cache.Get(SignedInUserKey) is UserRepresentation user) return user; - var firebaseUser = await auth.GetSignedInUser(); + var firebaseUser = await auth.GetSignedInUserOrThrow(); user = new UserRepresentation { Id = firebaseUser.Id, diff --git a/src/PKHeX.Web/Components/Pokemons/PokemonOnCloud.razor b/src/PKHeX.Web/Components/Pokemons/PokemonOnCloud.razor index f2e1de5..d230e03 100644 --- a/src/PKHeX.Web/Components/Pokemons/PokemonOnCloud.razor +++ b/src/PKHeX.Web/Components/Pokemons/PokemonOnCloud.razor @@ -1,6 +1,7 @@ @using PKHeX.Web.BackendApi @using PKHeX.Web.BackendApi.Repositories @using PKHeX.Web.BackendApi.Representation +@using PKHeX.Web.Services.Auth @implements IDisposable @inject INotificationService Notification @inject SyncPokemonWorker SyncWorker @@ -9,6 +10,8 @@ @inject MyPokemonRepository MyPokemonRepository @inject NavigationManager Navigation @inject UserRepository UserRepository +@inject AnalyticsService AnalyticsService +@inject AuthService Auth @@ -112,6 +115,8 @@ Description = $"You have reached your sync quota: {user.SyncQuota}.", NotificationType = NotificationType.Error }); + + AnalyticsService.TrackPokemonSyncQuotaExceeded(await Auth.GetSignedInUserOrThrow(), user.SyncQuota); } else { @@ -128,6 +133,11 @@ { await MyPokemonRepository.RemoveByLocalId(Pokemon.GetLocalSyncId()); } + + if (_syncedMetadata is not null) + { + AnalyticsService.TrackPokemonSyncStopped(_syncedMetadata, await Auth.GetSignedInUserOrThrow()); + } } await RefreshMetadata(); @@ -141,6 +151,7 @@ if (_syncedMetadata is null) return; await MyPokemonRepository.Share(_syncedMetadata.Id, isShared); + AnalyticsService.TrackPokemonPublicToggle(_syncedMetadata, isShared, await Auth.GetSignedInUserOrThrow()); await RefreshMetadata(); } @@ -150,21 +161,22 @@ if (_syncedMetadata is null) return; await MyPokemonRepository.AllowDownload(_syncedMetadata.Id, isAllowed); + AnalyticsService.TrackPokemonDownloadToggle(_syncedMetadata, isAllowed, await Auth.GetSignedInUserOrThrow()); await RefreshMetadata(); } - private Task HandlePokemonUploaded(PokemonUploadedArgs arg) + private async Task HandlePokemonUploaded(PokemonUploadedArgs arg) { - if (arg.Pokemon.GetLocalSyncId() != Pokemon.GetLocalSyncId()) return Task.CompletedTask; + if (arg.Pokemon.GetLocalSyncId() != Pokemon.GetLocalSyncId()) return; _syncedMetadata = arg.Metadata; + AnalyticsService.TrackPokemonSyncStarted(arg.Metadata, await Auth.GetSignedInUserOrThrow()); + SetSyncingStatuses(); StateHasChanged(); - - return Task.CompletedTask; } private void SetSyncingStatuses() diff --git a/src/PKHeX.Web/Pages/Home.razor b/src/PKHeX.Web/Pages/Home.razor index d7ccb06..92b8e7b 100644 --- a/src/PKHeX.Web/Pages/Home.razor +++ b/src/PKHeX.Web/Pages/Home.razor @@ -5,6 +5,7 @@ @inject AntdThemeService ThemeService @inject PlugInRegistry PlugInRegistry @inject PlugInRuntime PlugInRuntime +@inject IConfiguration Configuration Home @@ -46,7 +47,10 @@ } - + @if (Configuration.GetBackendApiOptions().Enabled) + { + + } diff --git a/src/PKHeX.Web/Pages/SharedPokemon.razor b/src/PKHeX.Web/Pages/SharedPokemon.razor index a815d6a..a0680fd 100644 --- a/src/PKHeX.Web/Pages/SharedPokemon.razor +++ b/src/PKHeX.Web/Pages/SharedPokemon.razor @@ -1,10 +1,12 @@ @page "/s/{Id}" -@using PKHeX.Web.BackendApi @using PKHeX.Web.BackendApi.Repositories @using PKHeX.Web.BackendApi.Representation +@using PKHeX.Web.Services.Auth @inject PublicPokemonRepository PublicPokemonRepository @inject NavigationManager Navigation @inject JsService Js +@inject AnalyticsService AnalyticsService +@inject AuthService Auth @inject ILogger Logger @if (_metadata is not null) @@ -42,8 +44,6 @@ private PokemonPublicMetadataRepresentation? _metadata; - private readonly ListGridType _grid = new() { Gutter = 16, Column = 4 }; - protected override async Task OnInitializedAsync() { var parsed = Guid.TryParse(Id, out _id); @@ -56,6 +56,7 @@ try { _metadata = await PublicPokemonRepository.GetById(_id); + AnalyticsService.TrackSharedPokemonSeen(_metadata, await Auth.GetSignedInUser()); } catch (Exception e) { @@ -79,7 +80,9 @@ private async Task HandleDownloadClick() { if (_metadata is null) return; - + + AnalyticsService.TrackSharedPokemonDownloaded(_metadata, await Auth.GetSignedInUser()); + var file = await PublicPokemonRepository.GetBinaries(_metadata.Id); var memoryStream = new MemoryStream(file); var fileName = $"{_metadata.Species}-{_metadata.Id}.bin"; diff --git a/src/PKHeX.Web/Services/AnalyticsService.cs b/src/PKHeX.Web/Services/AnalyticsService.cs index 846245a..af30acc 100644 --- a/src/PKHeX.Web/Services/AnalyticsService.cs +++ b/src/PKHeX.Web/Services/AnalyticsService.cs @@ -1,10 +1,12 @@ using Blazor.Analytics; -using Microsoft.AspNetCore.Components; using PKHeX.Facade; using PKHeX.Facade.Pokemons; +using PKHeX.Facade.Repositories; +using PKHeX.Web.BackendApi.Representation; using PKHeX.Web.Components; using PKHeX.Web.Extensions; using PKHeX.Web.Plugins; +using PKHeX.Web.Services.Auth; using PKHeX.Web.Services.Plugins; namespace PKHeX.Web.Services; @@ -67,6 +69,101 @@ public void TrackPokemon(string eventType, Pokemon pokemon, PokemonSource? sourc }); } + public void TrackSharedPokemonSeen(PokemonPublicMetadataRepresentation pokemon, AuthService.User? user = null) + { + analytics.TrackEvent("shared_pokemon_seen", new + { + id = pokemon.Id.ToString(), + species_id = (int)pokemon.Species, + species_name = pokemon.Species.ToString(), + gender = pokemon.Gender.Name, + ball_name = ItemRepository.GetItem(Convert.ToUInt16(pokemon.MetConditions.BallId)).Name, + level = pokemon.Level, + seen_by_user_id = user?.Id + }); + } + + public void TrackSharedPokemonDownloaded(PokemonPublicMetadataRepresentation pokemon, AuthService.User? user = null) + { + analytics.TrackEvent("shared_pokemon_downloaded", new + { + id = pokemon.Id.ToString(), + species_id = (int)pokemon.Species, + species_name = pokemon.Species.ToString(), + gender = pokemon.Gender.Name, + ball_name = ItemRepository.GetItem(Convert.ToUInt16(pokemon.MetConditions.BallId)).Name, + level = pokemon.Level, + downloaded_by_user_id = user?.Id + }); + } + + public void TrackPokemonSyncStarted(PokemonMetadataRepresentation pokemon, AuthService.User user) + { + analytics.TrackEvent("pokemon_sync_started", new + { + id = pokemon.Id.ToString(), + species_id = (int)pokemon.Species, + species_name = pokemon.Species.ToString(), + gender = pokemon.Gender.Name, + ball_name = ItemRepository.GetItem(Convert.ToUInt16(pokemon.MetConditions.BallId)).Name, + level = pokemon.Level, + user_id = user.Id + }); + } + + public void TrackPokemonSyncStopped(PokemonMetadataRepresentation pokemon, AuthService.User user) + { + analytics.TrackEvent("pokemon_sync_stopped", new + { + id = pokemon.Id.ToString(), + species_id = (int)pokemon.Species, + species_name = pokemon.Species.ToString(), + gender = pokemon.Gender.Name, + ball_name = ItemRepository.GetItem(Convert.ToUInt16(pokemon.MetConditions.BallId)).Name, + level = pokemon.Level, + user_id = user.Id + }); + } + + public void TrackPokemonPublicToggle(PokemonMetadataRepresentation pokemon, bool isShared, AuthService.User user) + { + analytics.TrackEvent("pokemon_sync_stopped", new + { + id = pokemon.Id.ToString(), + species_id = (int)pokemon.Species, + species_name = pokemon.Species.ToString(), + gender = pokemon.Gender.Name, + ball_name = ItemRepository.GetItem(Convert.ToUInt16(pokemon.MetConditions.BallId)).Name, + level = pokemon.Level, + is_shared = isShared, + user_id = user.Id + }); + } + + public void TrackPokemonDownloadToggle(PokemonMetadataRepresentation pokemon, bool isAllowed, AuthService.User user) + { + analytics.TrackEvent("pokemon_sync_stopped", new + { + id = pokemon.Id.ToString(), + species_id = (int)pokemon.Species, + species_name = pokemon.Species.ToString(), + gender = pokemon.Gender.Name, + ball_name = ItemRepository.GetItem(Convert.ToUInt16(pokemon.MetConditions.BallId)).Name, + level = pokemon.Level, + is_allowed = isAllowed, + user_id = user.Id + }); + } + + public void TrackPokemonSyncQuotaExceeded(AuthService.User user, int userQuota) + { + analytics.TrackEvent("pokemon_sync_quota_exceeded", new + { + user_id = user.Id, + user_quota = userQuota + }); + } + public void TrackItemModified(AddItemModal.ItemToBeAdded itemToBeAdded) { analytics.TrackEvent("item_modified", new diff --git a/src/PKHeX.Web/Services/Auth/AuthService.cs b/src/PKHeX.Web/Services/Auth/AuthService.cs index 8aa51d1..a7f7278 100644 --- a/src/PKHeX.Web/Services/Auth/AuthService.cs +++ b/src/PKHeX.Web/Services/Auth/AuthService.cs @@ -29,9 +29,9 @@ public async Task GetAuthToken() return authToken; } - public async Task GetSignedInUser() + public async Task GetSignedInUser() { - return await js.InvokeAsync("getSignedInUser"); + return await js.InvokeAsync("getSignedInUser"); } [JSInvokable] @@ -52,4 +52,13 @@ public record IdTokenChangedArgs( { public bool IsSignedIn => !string.IsNullOrEmpty(Token); } +} + +public static class AuthServiceExtensions +{ + public static async Task GetSignedInUserOrThrow(this AuthService auth) + { + var user = await auth.GetSignedInUser(); + return user ?? throw new InvalidOperationException("User is not signed in"); + } } \ No newline at end of file diff --git a/src/PKHeX.Web/_js/lib/firebase.ts b/src/PKHeX.Web/_js/lib/firebase.ts index e8aa829..d948bc5 100644 --- a/src/PKHeX.Web/_js/lib/firebase.ts +++ b/src/PKHeX.Web/_js/lib/firebase.ts @@ -50,7 +50,7 @@ export function initFirebase() { } window.getSignedInUser = () => { - if (!auth.currentUser) throw new Error('No user found') + if (!auth.currentUser) return null return { id: auth.currentUser.uid, email: auth.currentUser.email, diff --git a/src/PKHeX.Web/_js/lib/global.d.ts b/src/PKHeX.Web/_js/lib/global.d.ts index aa44c8d..47a0c6b 100644 --- a/src/PKHeX.Web/_js/lib/global.d.ts +++ b/src/PKHeX.Web/_js/lib/global.d.ts @@ -23,7 +23,7 @@ declare global { isSignedIn: () => boolean; getAuthToken: () => Promise; signInAnonymously: () => Promise; - getSignedInUser: () => User; + getSignedInUser: () => User | null; signOut: () => Promise; } } \ No newline at end of file