Add adaptive guided tours to the new UI - #2506
Conversation
b22a86a to
db17534
Compare
c3d3fb5 to
9546c7f
Compare
|
|
||
| public enum ProductTourStatus | ||
| { | ||
| Completed = 1, |
There was a problem hiding this comment.
shouldn't we have one for started? or would that be dismissed?
| { | ||
| public const string StepTagPrefix = "product-tour-step:"; | ||
|
|
||
| public static FrozenDictionary<string, string[]> Steps { get; } = new Dictionary<string, string[]>(StringComparer.Ordinal) |
There was a problem hiding this comment.
I don't love having this backend know about all the steps and or tour experiences, feels really tightly coupled. why do we need all the step names? It's not the end of the world but doesn't sit well.
| public ICollection<UserOrganizationPreference> OrganizationPreferences { get; init; } = new Collection<UserOrganizationPreference>(); | ||
| public ICollection<UserSavedViewOrderPreference> SavedViewOrders { get; init; } = new Collection<UserSavedViewOrderPreference>(); | ||
| public IDictionary<string, ProductTourProgress> ProductTours { get; init; } = new Dictionary<string, ProductTourProgress>(StringComparer.Ordinal); | ||
| public bool ProductTourAnalyticsEnabled { get; set; } = true; |
There was a problem hiding this comment.
do we really need this flag?
| Task<bool> UpdateSessionStartLastActivityAsync(string id, DateTime lastActivityUtc, bool isSessionEnd = false, bool hasError = false, bool sendNotifications = true); | ||
| Task<long> RemoveAllAsync(string organizationId, string? clientIpAddress, DateTime? utcStart, DateTime? utcEnd, CommandOptionsDescriptor<PersistentEvent>? options = null); | ||
| Task<long> RemoveAllByStackIdsAsync(string[] stackIds); | ||
| Task<ProductTourUsageResult> GetProductTourUsageAsync(string projectId, DateTime? utcStart, DateTime utcEnd, ProductTourUsageInterval? usageInterval = null); |
There was a problem hiding this comment.
interval should be auto computed by elastic with min number of data points, we can infer this from start and end times.
| if (interval is ProductTourUsageInterval.Auto && !utcStart.HasValue) | ||
| interval = ProductTourUsageInterval.Month; | ||
| string proximity = interval switch | ||
| { | ||
| ProductTourUsageInterval.Day => "~1d", | ||
| ProductTourUsageInterval.Month => "~1M", | ||
| ProductTourUsageInterval.Auto => String.Empty, | ||
| _ => throw new ArgumentOutOfRangeException(nameof(usageInterval)) | ||
| }; |
There was a problem hiding this comment.
can't elastic just do this and infer this, feels like we shouldn't do this.
| ProductTourUsageInterval Interval); | ||
|
|
||
| [JsonConverter(typeof(JsonStringEnumConverter))] | ||
| public enum ProductTourUsageInterval |
| return FindAsync(q => q.FieldEquals(u => u.OrganizationIds, organizationId).SortAscending(u => u.EmailAddress), o => commandOptions); | ||
| } | ||
|
|
||
| public async Task<ProductTourProgress> UpdateProductTourProgressAsync(string userId, string tourName, ProductTourProgress progress) |
There was a problem hiding this comment.
is this truely needed for an update, and or do we have great integration test coverage around htis?
| .RequireAuthorization(AuthorizationRoles.GlobalAdminPolicy) | ||
| .AddEndpointFilter<AutoValidationEndpointFilter>() | ||
| .Produces<ProductTourUsageResponse>(StatusCodes.Status200OK) | ||
| .ProducesProblem(StatusCodes.Status400BadRequest) |
There was a problem hiding this comment.
what produces a 400, any invalid parameters would be a 422.
| private static async Task<HttpIResult> GetProductTourUsageAsync( | ||
| IMediator mediator, | ||
| IMediatorResultMapper<HttpIResult> resultMapper, | ||
| DateTime? month = null, |
There was a problem hiding this comment.
this should uust be a start and end time no, and everything else is figured out...
| => (await mediator.InvokeAsync<Result<ProductTourProgress>>(new UserMessages.UpdateCurrentUserProductTour(tourName, progress))).ToHttpResult(resultMapper)) | ||
| .Accepts<UpdateProductTourProgress>(false, "application/json") | ||
| .Produces<ProductTourProgress>() | ||
| .ProducesProblem(StatusCodes.Status400BadRequest) |
There was a problem hiding this comment.
what produces a 400? 422 is preferred for any validation errors.
1233eba to
97a1a43
Compare
| .ProducesProblem(StatusCodes.Status400BadRequest) | ||
| .ProducesProblem(StatusCodes.Status413PayloadTooLarge) | ||
| .ProducesProblem(StatusCodes.Status422UnprocessableEntity) | ||
| .ProducesProblem(StatusCodes.Status404NotFound) |
There was a problem hiding this comment.
413? why would it be too big?
| DateTime utcEnd = timeProvider.GetUtcNow().UtcDateTime; | ||
| DateTime monthStart = (message.Month ?? utcEnd).ToUniversalTime().StartOfMonth(); | ||
| if (monthStart >= DateTime.MaxValue.StartOfMonth()) | ||
| return Result.Invalid(ValidationError.Create("month", "The month must have a representable end date.")); | ||
|
|
||
| DateTime? utcStart = message.History | ||
| ? appOptions.MaximumRetentionDays > 0 | ||
| ? utcEnd.SubtractDays(appOptions.MaximumRetentionDays) |
There was a problem hiding this comment.
greatly simplify this... we don't need half of this date logic..
| } | ||
|
|
||
| // Re-read the preference so a stale browser or authentication cache cannot bypass opt-out. | ||
| var user = await userRepository.GetByIdAsync(HttpContext.Request.GetUser().Id, options => options.Cache(false)); |
There was a problem hiding this comment.
why woudn't we get this from cache, we control consistency level.
| return Result.Unavailable("Guided-tour activity storage is unavailable."); | ||
| } | ||
|
|
||
| var ev = new Event |
There was a problem hiding this comment.
I don't like this, why wouldn't the client just submit an event from the client itself. this is bad. Just have the client submit an event to the event endpoint we have the exceptionless client.
| return Result.Unavailable("Guided-tour activity storage is unavailable."); | ||
| } | ||
|
|
||
| var ev = new Event |
There was a problem hiding this comment.
also we already have the user progress for tour do we really need this activity? unless it helps us with sessions feels like we would only care about start, dismissed and finished
| <script lang="ts" module> | ||
| import type { KeyboardShortcut } from '$features/shared/keyboard-shortcuts'; | ||
|
|
||
| export interface ProductTourShortcut { | ||
| label: string; | ||
| shortcut: KeyboardShortcut; | ||
| } | ||
| </script> | ||
|
|
||
| <script lang="ts"> | ||
| import type { Snippet } from 'svelte'; |
There was a problem hiding this comment.
what are we doing here, we can have models folder or something here.... this feels very hacky.
| {label} | ||
| <Kbd.Root>{formatKeyboardShortcut(shortcut.keys)}</Kbd.Root> | ||
| </span> | ||
| {/each} |
| <Button disabled={busy} onclick={onBrowse} size="sm" variant="ghost">Browse guides</Button> | ||
| </div> | ||
| <ProductTourPrivacyLink /> | ||
| </Alert.Root> |
There was a problem hiding this comment.
should alerts be in an alerts folder e.g., like dialogs, please think on this more..
| onSettled: () => | ||
| queryClient.invalidateQueries({ | ||
| queryKey: userQueryKeys.me() | ||
| }) |
| } | ||
|
|
||
| function requireError(context: ProductTourContext) { | ||
| if (!context.organizationId) return { available: false, reason: 'Create an organization and project first.' }; |
There was a problem hiding this comment.
this is a lint error, we should always have {} and multiline why is this not breaking linting, we have stylistic rules for this.
97c915f to
81599b4
Compare
PR #2506 review dispositionsImplementation:
Verified
Remaining verification
Regression evidence
|
fadd495 to
0f3e496
Compare
0f3e496 to
36742c8
Compare
Context
Replacement for #2458, which was merged prematurely and reverted by #2505. Keep this PR in draft and do not merge until Blake explicitly approves it.
What changed
app-*,project-*,event-*,saved-view-*, andexie-*Usage query design
Safety and compatibility
Verification
dotnet build src/Exceptionless.Web/Exceptionless.Web.csproj --no-restore: passed with 0 warnings and 0 errorsnpm run check: 0 Svelte errors and 0 warningsnpm run build: passedBreaking changes
None.