From f734b179a99a97c2a5b86fdb7deb0fef4de05e8e Mon Sep 17 00:00:00 2001 From: andrewwallacespeckle Date: Fri, 14 Mar 2025 10:12:57 +0000 Subject: [PATCH 1/2] Improve "Open billing portal" logging --- .../frontend-2/lib/billing/composables/actions.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/frontend-2/lib/billing/composables/actions.ts b/packages/frontend-2/lib/billing/composables/actions.ts index 1f63d02bf..82cea6fde 100644 --- a/packages/frontend-2/lib/billing/composables/actions.ts +++ b/packages/frontend-2/lib/billing/composables/actions.ts @@ -48,9 +48,13 @@ export const useBillingActions = () => { const { mutate: cancelCheckoutSessionMutation } = useMutation( settingsBillingCancelCheckoutSessionMutation ) + const logger = useLogger() const billingPortalRedirect = async (workspaceId: MaybeNullOrUndefined) => { - if (!workspaceId) return + if (!workspaceId) { + logger.error('[Billing Portal] No workspaceId provided, returning early') + return + } mixpanel.track('Workspace Billing Portal Button Clicked', { // eslint-disable-next-line camelcase @@ -66,6 +70,11 @@ export const useBillingActions = () => { if (result.data?.workspace.customerPortalUrl) { window.open(result.data.workspace.customerPortalUrl, '_blank') + } else { + logger.warn( + '[Billing Portal] No portal URL returned, full response:', + result.data + ) } } From 50fd05afe8dcbf8f281eb5b9d0368856ae7a11e6 Mon Sep 17 00:00:00 2001 From: Kristaps Fabians Geikins Date: Fri, 14 Mar 2025 12:47:58 +0200 Subject: [PATCH 2/2] feat(fe2): more viewer debugging improvements (#4193) --- .../components/viewer/PreSetupWrapper.vue | 23 ++++++-- .../lib/viewer/composables/activity.ts | 18 +++---- .../lib/viewer/composables/serialization.ts | 17 +++--- .../lib/viewer/composables/setup.ts | 9 ++-- .../lib/viewer/composables/setup/dev.ts | 53 ++++++++++++++----- .../frontend-2/type-augmentations/window.d.ts | 1 + .../auth/tests/helpers/registration.ts | 4 +- 7 files changed, 84 insertions(+), 41 deletions(-) diff --git a/packages/frontend-2/components/viewer/PreSetupWrapper.vue b/packages/frontend-2/components/viewer/PreSetupWrapper.vue index eaf6df817..3909cc249 100644 --- a/packages/frontend-2/components/viewer/PreSetupWrapper.vue +++ b/packages/frontend-2/components/viewer/PreSetupWrapper.vue @@ -97,7 +97,11 @@ :url="route.path" /> - + @@ -113,17 +117,28 @@ import { useFilterUtilities } from '~/lib/viewer/composables/ui' import { projectsRoute } from '~~/lib/common/helpers/route' import { workspaceRoute } from '~/lib/common/helpers/route' import { useMixpanel } from '~/lib/core/composables/mp' +import { writableAsyncComputed } from '~/lib/common/composables/async' const emit = defineEmits<{ setup: [InjectableViewerState] }>() +const router = useRouter() const route = useRoute() const isWorkspacesEnabled = useIsWorkspacesEnabled() -const modelId = computed(() => route.params.modelId as string) - -const projectId = computed(() => route.params.id as string) +const resourceIdString = computed(() => route.params.modelId as string) +const projectId = writableAsyncComputed({ + get: () => route.params.id as string, + set: async (value: string) => { + // Just rewrite route id param + await router.push({ + params: { id: value } + }) + }, + initialState: route.params.id as string, + asyncRead: false +}) const state = useSetupViewer({ projectId diff --git a/packages/frontend-2/lib/viewer/composables/activity.ts b/packages/frontend-2/lib/viewer/composables/activity.ts index 0f9ea71ff..2258f570f 100644 --- a/packages/frontend-2/lib/viewer/composables/activity.ts +++ b/packages/frontend-2/lib/viewer/composables/activity.ts @@ -82,13 +82,13 @@ export function useViewerUserActivityBroadcasting( const apollo = useApolloClient().client const { isEnabled: isEmbedEnabled } = useEmbed() - const isSameState = ( - a: Optional, - b: Optional + const isSameMessage = ( + previousSerializedMessage: Optional, + newMessage: ViewerUserActivityMessageInput ) => { - if (xor(a, b)) return false - if (!a || !b) return false - return JSON.stringify(a) === JSON.stringify(b) + if (xor(previousSerializedMessage, newMessage)) return false + if (!previousSerializedMessage && !newMessage) return false + return previousSerializedMessage === JSON.stringify(newMessage) } const invokeMutation = async (message: ViewerUserActivityMessageInput) => { @@ -106,14 +106,14 @@ export function useViewerUserActivityBroadcasting( return result.data?.broadcastViewerUserActivity || false } - let previousMessage: Optional = undefined + let serializedPreviousMessage: Optional = undefined const invokeObservabilityEvent = async (message: ViewerUserActivityMessageInput) => { const dd = window.DD_RUM if (!dd || !('addAction' in dd)) return - if (isSameState(previousMessage, message)) return + if (isSameMessage(serializedPreviousMessage, message)) return - previousMessage = message + serializedPreviousMessage = JSON.stringify(message) dd.addAction('Viewer User Activity', { message }) } diff --git a/packages/frontend-2/lib/viewer/composables/serialization.ts b/packages/frontend-2/lib/viewer/composables/serialization.ts index 920061098..b9634f697 100644 --- a/packages/frontend-2/lib/viewer/composables/serialization.ts +++ b/packages/frontend-2/lib/viewer/composables/serialization.ts @@ -137,6 +137,7 @@ export enum StateApplyMode { export function useApplySerializedState() { const { + projectId, ui: { camera: { position, target, isOrthoProjection }, sectionBox, @@ -172,6 +173,16 @@ export function useApplySerializedState() { return } + if (state.projectId && state.projectId !== projectId.value) { + await projectId.update(state.projectId) + } + + if ( + [StateApplyMode.Spotlight, StateApplyMode.TheadFullContextOpen].includes(mode) + ) { + await resourceIdString.update(state.resources?.request?.resourceIdString || '') + } + position.value = new Vector3( state.ui?.camera?.position?.[0], state.ui?.camera?.position?.[1], @@ -257,12 +268,6 @@ export function useApplySerializedState() { } } - if ( - [StateApplyMode.Spotlight, StateApplyMode.TheadFullContextOpen].includes(mode) - ) { - await resourceIdString.update(state.resources?.request?.resourceIdString || '') - } - if ([StateApplyMode.Spotlight].includes(mode)) { await urlHashState.focusedThreadId.update( state.ui?.threads?.openThread?.threadId || null diff --git a/packages/frontend-2/lib/viewer/composables/setup.ts b/packages/frontend-2/lib/viewer/composables/setup.ts index 1c1f06d51..aa7d0167f 100644 --- a/packages/frontend-2/lib/viewer/composables/setup.ts +++ b/packages/frontend-2/lib/viewer/composables/setup.ts @@ -18,7 +18,6 @@ import { type VisualDiffMode, ViewMode } from '@speckle/viewer' -import type { MaybeRef } from '@vueuse/shared' import { inject, ref, provide } from 'vue' import type { ComputedRef, WritableComputedRef, Raw, Ref, ShallowRef } from 'vue' import { useScopedState } from '~~/lib/common/composables/scopedState' @@ -82,7 +81,7 @@ export type InjectableViewerState = Readonly<{ /** * The project which we're opening in the viewer (all loaded models should belong to it) */ - projectId: ComputedRef + projectId: AsyncWritableComputedRef /** * User viewer session ID. The same user will have different IDs in different tabs if multiple are open. * This is used to ignore user activity messages from the same tab. @@ -400,8 +399,6 @@ function setupInitialState(params: UseSetupViewerParams): InitialSetupState { public: { viewerDebug } } = useRuntimeConfig() - const projectId = computed(() => unref(params.projectId)) - const sessionId = computed(() => nanoid()) const isInitialized = ref(false) const { instance, initPromise, container } = useScopedState( @@ -412,7 +409,7 @@ function setupInitialState(params: UseSetupViewerParams): InitialSetupState { const hasDoneInitialLoad = ref(false) return { - projectId, + projectId: params.projectId, sessionId, viewer: import.meta.server ? ({ @@ -1030,7 +1027,7 @@ function setupInterfaceState( } } -type UseSetupViewerParams = { projectId: MaybeRef } +type UseSetupViewerParams = { projectId: AsyncWritableComputedRef } export function useSetupViewer(params: UseSetupViewerParams): InjectableViewerState { // Initialize full state object - each subsequent state initialization depends on diff --git a/packages/frontend-2/lib/viewer/composables/setup/dev.ts b/packages/frontend-2/lib/viewer/composables/setup/dev.ts index 7d54c3292..5afd21437 100644 --- a/packages/frontend-2/lib/viewer/composables/setup/dev.ts +++ b/packages/frontend-2/lib/viewer/composables/setup/dev.ts @@ -1,12 +1,13 @@ import { ViewerEvent } from '@speckle/viewer' import { StateApplyMode, - useApplySerializedState + useApplySerializedState, + useStateSerialization } from '~/lib/viewer/composables/serialization' import { useInjectedViewerState } from '~~/lib/viewer/composables/setup' import { useViewerEventListener } from '~~/lib/viewer/composables/viewer' import type { SpeckleViewer } from '@speckle/shared' -import { get } from 'lodash-es' +import { get, isString } from 'lodash-es' // eslint-disable-next-line @typescript-eslint/no-unused-vars function useDebugViewerEvents() { @@ -18,38 +19,62 @@ function useDebugViewerEvents() { } function useDebugViewer() { - const state = useInjectedViewerState() + const fullViewerState = useInjectedViewerState() const apply = useApplySerializedState() + const { serialize } = useStateSerialization() const { viewer: { instance } - } = state + } = fullViewerState + + const ensureObj = (obj: O | string): O => { + return isString(obj) ? JSON.parse(obj) : obj + } + + const applyState = ( + state: SpeckleViewer.ViewerState.SerializedViewerState | string + ) => { + return apply(ensureObj(state), StateApplyMode.TheadFullContextOpen) + } // Get current viewer instance window.VIEWER = instance // Get current viewer state - window.VIEWER_STATE = () => state + window.VIEWER_STATE = () => fullViewerState + + // Get serialized version of current state + window.VIEWER_SERIALIZED_STATE = (...args: Parameters) => { + const serialized = serialize(...args) + return JSON.stringify(serialized) + } // Apply viewer state window.APPLY_VIEWER_STATE = ( state: SpeckleViewer.ViewerState.SerializedViewerState - ) => apply(state, StateApplyMode.TheadFullContextOpen) + ) => applyState(state) // Apply DD user activity event - window.APPLY_VIEWER_DD_EVENT = (event: { - content: { - attributes: { - context: { message: { state: SpeckleViewer.ViewerState.SerializedViewerState } } - } - } - }) => { + window.APPLY_VIEWER_DD_EVENT = ( + event: + | { + content: { + attributes: { + context: { + message: { state: SpeckleViewer.ViewerState.SerializedViewerState } + } + } + } + } + | string + ) => { + event = ensureObj(event) const path = 'content.attributes.context.message.state' const state = get(event, path) if (!state) { throw new Error('Cant find serialized state at path: ' + path) } - return apply(state, StateApplyMode.TheadFullContextOpen) + return applyState(state) } } diff --git a/packages/frontend-2/type-augmentations/window.d.ts b/packages/frontend-2/type-augmentations/window.d.ts index 7065b2647..ffd1a5933 100644 --- a/packages/frontend-2/type-augmentations/window.d.ts +++ b/packages/frontend-2/type-augmentations/window.d.ts @@ -11,6 +11,7 @@ declare global { // Debug keys, don't need to type properly cause we only use them manually from dev tools VIEWER?: any VIEWER_STATE?: any + VIEWER_SERIALIZED_STATE?: any APPLY_VIEWER_STATE?: any APPLY_VIEWER_DD_EVENT?: any } diff --git a/packages/server/modules/auth/tests/helpers/registration.ts b/packages/server/modules/auth/tests/helpers/registration.ts index 3e8048d25..ebd3628c2 100644 --- a/packages/server/modules/auth/tests/helpers/registration.ts +++ b/packages/server/modules/auth/tests/helpers/registration.ts @@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker' import { RelativeURL } from '@speckle/shared' import { expect } from 'chai' import type { Express } from 'express' -import { has, isString, random } from 'lodash' +import { has, isString } from 'lodash' import request from 'supertest' export const appId = 'spklwebapp' // same values as on FE @@ -233,7 +233,7 @@ export type LocalAuthRestApiHelpers = ReturnType export const generateRegistrationParams = (): RegisterParams => ({ challenge: faker.string.uuid(), user: { - email: `${random(0, 1000)}@example.org`.toLowerCase(), + email: faker.internet.email().toLowerCase(), password: faker.internet.password(), name: faker.person.fullName() }