fix(fe2): error msg templating + Filters undefined error fix (#2411)
* fix(fe2): error msg templating + Filters undefined error fix * fixx * fixx
This commit is contained in:
committed by
GitHub
parent
4ab1afc190
commit
f074be89aa
@@ -18,10 +18,6 @@
|
||||
</div>
|
||||
|
||||
<PromoBannersWrapper v-if="promoBanners.length" :banners="promoBanners" />
|
||||
<div v-if="showErrorTest" class="w-full">
|
||||
<FormButton @click="testError">Test error</FormButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!showEmptyState"
|
||||
class="flex flex-col space-y-2 md:flex-row md:items-center mb-8 pt-4"
|
||||
@@ -134,7 +130,6 @@ const promoBanners = ref<PromoBanner[]>([
|
||||
}
|
||||
])
|
||||
|
||||
const route = useRoute()
|
||||
const { activeUser, isGuest } = useActiveUser()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const areQueriesLoading = useQueryLoading()
|
||||
@@ -160,8 +155,6 @@ const { onResult: onUserProjectsUpdate } = useSubscription(
|
||||
onUserProjectsUpdateSubscription
|
||||
)
|
||||
|
||||
const showErrorTest = computed(() => route.query.showErrorButton === '1')
|
||||
|
||||
const projects = computed(() => projectsPanelResult.value?.activeUser?.projects)
|
||||
const showEmptyState = computed(() => {
|
||||
const isFiltering =
|
||||
@@ -340,8 +333,4 @@ const clearSearch = () => {
|
||||
selectedRoles.value = []
|
||||
updateSearchImmediately()
|
||||
}
|
||||
|
||||
const testError = () => {
|
||||
throw new Error('what duhh hell')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -66,7 +66,7 @@ import { ChevronLeftIcon } from '@heroicons/vue/24/solid'
|
||||
import { VisualDiffMode } from '@speckle/viewer'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import { uniqBy, debounce } from 'lodash-es'
|
||||
import type { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
|
||||
import type { SpeckleObject } from '~~/lib/viewer/helpers/sceneExplorer'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
|
||||
defineEmits<{
|
||||
|
||||
@@ -75,7 +75,7 @@ import {
|
||||
CodeBracketIcon
|
||||
} from '@heroicons/vue/24/solid'
|
||||
import { ViewerEvent } from '@speckle/viewer'
|
||||
import type { ExplorerNode } from '~~/lib/common/helpers/sceneExplorer'
|
||||
import type { ExplorerNode } from '~~/lib/viewer/helpers/sceneExplorer'
|
||||
import {
|
||||
useInjectedViewer,
|
||||
useInjectedViewerLoadedResources,
|
||||
|
||||
@@ -82,11 +82,11 @@
|
||||
</div>
|
||||
<div v-if="activeFilter">
|
||||
<ViewerExplorerStringFilter
|
||||
v-if="activeFilter.type === 'string'"
|
||||
v-if="stringActiveFilter"
|
||||
:filter="stringActiveFilter"
|
||||
/>
|
||||
<ViewerExplorerNumericFilter
|
||||
v-if="activeFilter.type === 'number'"
|
||||
v-if="numericActiveFilter"
|
||||
:filter="numericActiveFilter"
|
||||
/>
|
||||
</div>
|
||||
@@ -95,13 +95,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/24/solid'
|
||||
import { ArrowPathIcon } from '@heroicons/vue/24/outline'
|
||||
import type {
|
||||
PropertyInfo,
|
||||
StringPropertyInfo,
|
||||
NumericPropertyInfo
|
||||
} from '@speckle/viewer'
|
||||
import type { PropertyInfo, StringPropertyInfo } from '@speckle/viewer'
|
||||
import { useFilterUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import {
|
||||
isNumericPropertyInfo,
|
||||
isStringPropertyInfo
|
||||
} from '~/lib/viewer/helpers/sceneExplorer'
|
||||
|
||||
const {
|
||||
setPropertyFilter,
|
||||
@@ -150,9 +150,8 @@ const relevantFilters = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const speckleTypeFilter = computed(
|
||||
() =>
|
||||
relevantFilters.value.find((f) => f.key === 'speckle_type') as StringPropertyInfo
|
||||
const speckleTypeFilter = computed(() =>
|
||||
relevantFilters.value.find((f) => f.key === 'speckle_type')
|
||||
)
|
||||
const activeFilter = computed(
|
||||
() => propertyFilter.filter.value || speckleTypeFilter.value
|
||||
@@ -169,9 +168,12 @@ watch(activeFilter, (newVal) => {
|
||||
})
|
||||
})
|
||||
|
||||
// Using these as casting activeFilter as XXX in the prop causes some syntax highliting bug to show. Apologies :)
|
||||
const stringActiveFilter = computed(() => activeFilter.value as StringPropertyInfo)
|
||||
const numericActiveFilter = computed(() => activeFilter.value as NumericPropertyInfo)
|
||||
const stringActiveFilter = computed(() =>
|
||||
isStringPropertyInfo(activeFilter.value) ? activeFilter.value : undefined
|
||||
)
|
||||
const numericActiveFilter = computed(() =>
|
||||
isNumericPropertyInfo(activeFilter.value) ? activeFilter.value : undefined
|
||||
)
|
||||
|
||||
const searchString = ref<string | undefined>(undefined)
|
||||
const relevantFiltersSearched = computed(() => {
|
||||
@@ -229,7 +231,7 @@ const toggleColors = () => {
|
||||
// Handles a rather complicated ux flow: user sets a numeric filter which only makes sense with colors on. we set the force colors flag in that scenario, so we can revert it if user selects a non-numeric filter afterwards.
|
||||
let forcedColors = false
|
||||
const refreshColorsIfSetOrActiveFilterIsNumeric = () => {
|
||||
if (activeFilter.value.type === 'number' && !colors.value) {
|
||||
if (!!numericActiveFilter.value && !colors.value) {
|
||||
forcedColors = true
|
||||
applyPropertyFilter()
|
||||
return
|
||||
|
||||
@@ -144,7 +144,7 @@ import type {
|
||||
ExplorerNode,
|
||||
SpeckleObject,
|
||||
SpeckleReference
|
||||
} from '~~/lib/common/helpers/sceneExplorer'
|
||||
} from '~~/lib/viewer/helpers/sceneExplorer'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import {
|
||||
getHeaderAndSubheaderForSpeckleObject,
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { ChevronDownIcon } from '@heroicons/vue/24/solid'
|
||||
import { ClipboardDocumentIcon } from '@heroicons/vue/24/outline'
|
||||
import type { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
|
||||
import type { SpeckleObject } from '~~/lib/viewer/helpers/sceneExplorer'
|
||||
import { getHeaderAndSubheaderForSpeckleObject } from '~~/lib/object-sidebar/helpers'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import { useHighlightedObjectsUtilities } from '~/lib/viewer/composables/ui'
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { type SpeckleObject, type SpeckleReference } from '@speckle/viewer'
|
||||
|
||||
// Note: minor typing hacks for less squiggly lines in the explorer.
|
||||
// TODO: ask alex re viewer data tree types
|
||||
|
||||
export type ExplorerNode = {
|
||||
guid?: string
|
||||
data?: SpeckleObject
|
||||
raw?: SpeckleObject
|
||||
atomic?: boolean
|
||||
model?: Record<string, unknown> & { id?: string }
|
||||
children: ExplorerNode[]
|
||||
}
|
||||
|
||||
export type { SpeckleObject, SpeckleReference }
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useScopedState } from '~~/lib/common/composables/scopedState'
|
||||
import * as Observability from '@speckle/shared/dist/esm/observability/index'
|
||||
import type {
|
||||
AbstractErrorHandler,
|
||||
AbstractErrorHandlerParams,
|
||||
AbstractUnhandledErrorHandler
|
||||
import {
|
||||
prettify,
|
||||
type AbstractErrorHandler,
|
||||
type AbstractErrorHandlerParams,
|
||||
type AbstractUnhandledErrorHandler
|
||||
} from '~/lib/core/helpers/observability'
|
||||
|
||||
const ENTER_STATE_AT_ERRORS_PER_MIN = 100
|
||||
@@ -65,7 +66,11 @@ export const useGetErrorLoggingTransports = () => {
|
||||
export const useLogToErrorLoggingTransports = () => {
|
||||
const transports = useGetErrorLoggingTransports()
|
||||
const invokeTransportsWithPayload = (payload: AbstractErrorHandlerParams) => {
|
||||
transports.forEach((handler) => handler.onError(payload))
|
||||
transports.forEach((handler) =>
|
||||
handler.onError(payload, {
|
||||
prettifyMessage: (msg) => prettify(payload.otherData || {}, msg)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,7 +8,7 @@ import createUploadLink from 'apollo-upload-client/createUploadLink.mjs'
|
||||
import { WebSocketLink } from '@apollo/client/link/ws'
|
||||
import { getMainDefinition } from '@apollo/client/utilities'
|
||||
import { Kind } from 'graphql'
|
||||
import type { OperationDefinitionNode } from 'graphql'
|
||||
import type { GraphQLError, OperationDefinitionNode } from 'graphql'
|
||||
import type { CookieRef, NuxtApp } from '#app'
|
||||
import type { Optional } from '@speckle/shared'
|
||||
import { useAuthCookie } from '~~/lib/auth/composables/auth'
|
||||
@@ -22,7 +22,7 @@ import { onError } from '@apollo/client/link/error'
|
||||
import { useNavigateToLogin, loginRoute } from '~~/lib/common/helpers/route'
|
||||
import { useAppErrorState } from '~~/lib/core/composables/error'
|
||||
import { isInvalidAuth } from '~~/lib/common/helpers/graphql'
|
||||
import { isBoolean, omit } from 'lodash-es'
|
||||
import { isArray, isBoolean, omit } from 'lodash-es'
|
||||
import { useRequestId } from '~/lib/core/composables/server'
|
||||
|
||||
const appName = 'frontend-2'
|
||||
@@ -337,12 +337,15 @@ function createLink(params: {
|
||||
? skipLoggingErrors
|
||||
: skipLoggingErrors?.(res)
|
||||
if (!isSubTokenMissingError && !shouldSkip) {
|
||||
const errMsg = res.networkError?.message || res.graphQLErrors?.[0]?.message
|
||||
const gqlErrors: Array<GraphQLError> = isArray(res.graphQLErrors)
|
||||
? res.graphQLErrors
|
||||
: []
|
||||
const errMsg = res.networkError?.message || gqlErrors[0]?.message
|
||||
logger.error(
|
||||
{
|
||||
...omit(res, ['forward', 'response']),
|
||||
networkErrorMessage: res.networkError?.message,
|
||||
gqlErrorMessages: res.graphQLErrors?.map((e) => e.message),
|
||||
gqlErrorMessages: gqlErrors.map((e) => e.message),
|
||||
errorMessage: errMsg,
|
||||
graphql: true
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ import type { Logger } from 'pino'
|
||||
/**
|
||||
* Add pino-pretty like formatting
|
||||
*/
|
||||
const prettify = (log: object, msg: string) =>
|
||||
export const prettify = (log: object, msg: string) =>
|
||||
msg.replace(/{([^{}]+)}/g, (match: string, p1: string) => {
|
||||
const val = get(log, p1)
|
||||
if (val === undefined) return match
|
||||
@@ -33,7 +33,7 @@ const prettify = (log: object, msg: string) =>
|
||||
* Wrap any logger call w/ logic that prettifies the error message like pino-pretty does
|
||||
* and emits bindings if they are provided
|
||||
*/
|
||||
const log =
|
||||
const prettifiedLoggerFactory =
|
||||
(logger: (...args: unknown[]) => void, bindings?: () => Record<string, unknown>) =>
|
||||
(...vals: unknown[]) => {
|
||||
const finalVals = vals.slice()
|
||||
@@ -72,16 +72,16 @@ export function buildFakePinoLogger(
|
||||
const errLogger = (...args: unknown[]) => {
|
||||
const { onError } = options || {}
|
||||
if (onError) onError(...args)
|
||||
log(console.error, bindings)(...args)
|
||||
prettifiedLoggerFactory(console.error, bindings)(...args)
|
||||
}
|
||||
|
||||
const logger = {
|
||||
debug: log(console.debug, bindings),
|
||||
info: log(console.info, bindings),
|
||||
warn: log(console.warn, bindings),
|
||||
debug: prettifiedLoggerFactory(console.debug, bindings),
|
||||
info: prettifiedLoggerFactory(console.info, bindings),
|
||||
warn: prettifiedLoggerFactory(console.warn, bindings),
|
||||
error: errLogger,
|
||||
fatal: errLogger,
|
||||
trace: log(console.trace, bindings),
|
||||
trace: prettifiedLoggerFactory(console.trace, bindings),
|
||||
silent: noop
|
||||
} as unknown as ReturnType<typeof Observability.getLogger>
|
||||
|
||||
@@ -121,13 +121,18 @@ export const formatAppError = (err: SimpleError) => {
|
||||
}
|
||||
}
|
||||
|
||||
export type AbstractErrorHandler = (params: {
|
||||
args: unknown[]
|
||||
firstString: Optional<string>
|
||||
firstError: Optional<Error>
|
||||
otherData: Record<string, unknown>
|
||||
nonObjectOtherData: unknown[]
|
||||
}) => void
|
||||
export type AbstractErrorHandler = (
|
||||
params: {
|
||||
args: unknown[]
|
||||
firstString: Optional<string>
|
||||
firstError: Optional<Error>
|
||||
otherData: Record<string, unknown>
|
||||
nonObjectOtherData: unknown[]
|
||||
},
|
||||
helpers: {
|
||||
prettifyMessage: (msg: string) => string
|
||||
}
|
||||
) => void
|
||||
|
||||
export type AbstractUnhandledErrorHandler = (params: {
|
||||
event: ErrorEvent | PromiseRejectionEvent
|
||||
@@ -175,13 +180,16 @@ export function enableCustomErrorHandling(params: {
|
||||
{},
|
||||
...otherDataObjects
|
||||
) as Record<string, unknown>
|
||||
onError({
|
||||
args,
|
||||
firstError,
|
||||
firstString,
|
||||
otherData: mergedOtherDataObject,
|
||||
nonObjectOtherData: otherDataNonObjects
|
||||
})
|
||||
onError(
|
||||
{
|
||||
args,
|
||||
firstError,
|
||||
firstString,
|
||||
otherData: mergedOtherDataObject,
|
||||
nonObjectOtherData: otherDataNonObjects
|
||||
},
|
||||
{ prettifyMessage: (msg) => prettify(mergedOtherDataObject, msg) }
|
||||
)
|
||||
}
|
||||
|
||||
return log(...args)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
|
||||
import type { SpeckleObject } from '~/lib/viewer/helpers/sceneExplorer'
|
||||
|
||||
export type HeaderSubheader = {
|
||||
header: string
|
||||
|
||||
@@ -49,7 +49,7 @@ import { nanoid } from 'nanoid'
|
||||
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
|
||||
import type { CommentBubbleModel } from '~~/lib/viewer/composables/commentBubbles'
|
||||
import { setupUrlHashState } from '~~/lib/viewer/composables/setup/urlHashState'
|
||||
import type { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
|
||||
import type { SpeckleObject } from '~/lib/viewer/helpers/sceneExplorer'
|
||||
import type { Box3 } from 'three'
|
||||
import { Vector3 } from 'three'
|
||||
import { writableAsyncComputed } from '~~/lib/common/composables/async'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import { MeasurementType } from '@speckle/viewer'
|
||||
import type { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
|
||||
import type { SpeckleObject } from '~/lib/viewer/helpers/sceneExplorer'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import { useCameraUtilities, useSelectionUtilities } from '~~/lib/viewer/composables/ui'
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CameraController, MeasurementsExtension } from '@speckle/viewer'
|
||||
import { until } from '@vueuse/shared'
|
||||
import { difference, isString, uniq } from 'lodash-es'
|
||||
import { useEmbedState } from '~/lib/viewer/composables/setup/embed'
|
||||
import type { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
|
||||
import type { SpeckleObject } from '~/lib/viewer/helpers/sceneExplorer'
|
||||
import { isNonNullable } from '~~/lib/common/helpers/utils'
|
||||
import {
|
||||
useInjectedViewer,
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
type SelectionEvent,
|
||||
type TreeNode
|
||||
} from '@speckle/viewer'
|
||||
import type { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
|
||||
import type { SpeckleObject } from '~/lib/viewer/helpers/sceneExplorer'
|
||||
|
||||
// NOTE: this is a preformance optimisation - this function is hot, and has to do
|
||||
// potentially large searches if many elements are hidden/isolated. We cache the
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
import {
|
||||
type NumericPropertyInfo,
|
||||
type PropertyInfo,
|
||||
type SpeckleObject,
|
||||
type SpeckleReference,
|
||||
type StringPropertyInfo
|
||||
} from '@speckle/viewer'
|
||||
|
||||
export const isStringPropertyInfo = (
|
||||
info: MaybeNullOrUndefined<PropertyInfo>
|
||||
): info is StringPropertyInfo => info?.type === 'string'
|
||||
export const isNumericPropertyInfo = (
|
||||
info: MaybeNullOrUndefined<PropertyInfo>
|
||||
): info is NumericPropertyInfo => info?.type === 'number'
|
||||
|
||||
// Note: minor typing hacks for less squiggly lines in the explorer.
|
||||
// TODO: ask alex re viewer data tree types
|
||||
|
||||
export type ExplorerNode = {
|
||||
guid?: string
|
||||
data?: SpeckleObject
|
||||
raw?: SpeckleObject
|
||||
atomic?: boolean
|
||||
model?: Record<string, unknown> & { id?: string }
|
||||
children: ExplorerNode[]
|
||||
}
|
||||
|
||||
export type { SpeckleObject, SpeckleReference }
|
||||
@@ -136,6 +136,10 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
...collectMainInfo({ isBrowser: true })
|
||||
})
|
||||
|
||||
logger = buildFakePinoLogger({
|
||||
consoleBindings: logCsrEmitProps ? collectCoreInfo : undefined
|
||||
})
|
||||
|
||||
// SEQ Browser integration
|
||||
if (logClientApiToken?.length && logClientApiEndpoint?.length) {
|
||||
const seq = await import('seq-logging/browser')
|
||||
@@ -195,24 +199,15 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
})
|
||||
}
|
||||
errorHandlers.push(errorLogger)
|
||||
|
||||
logger = buildFakePinoLogger({
|
||||
consoleBindings: logCsrEmitProps ? collectCoreInfo : undefined
|
||||
})
|
||||
logger.debug('Set up seq ingestion...')
|
||||
} else {
|
||||
// No seq integration, fallback to basic console logging
|
||||
logger = buildFakePinoLogger({
|
||||
consoleBindings: logCsrEmitProps ? collectCoreInfo : undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Register seq transports, if any
|
||||
if (errorHandlers.length) {
|
||||
registerErrorTransport({
|
||||
onError: (params) => {
|
||||
errorHandlers.forEach((handler) => handler(params))
|
||||
onError: (...params) => {
|
||||
errorHandlers.forEach((handler) => handler(...params))
|
||||
},
|
||||
onUnhandledError: (event) => {
|
||||
unhandledErrorHandlers.forEach((handler) => handler(event))
|
||||
@@ -220,19 +215,19 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Global error handler - handle all transports
|
||||
// Global error handler - handle all transports besides the core pino/console.log logger
|
||||
const transports = useGetErrorLoggingTransports()
|
||||
let serverFatalError: Optional<AbstractErrorHandlerParams> = undefined
|
||||
logger = enableCustomErrorHandling({
|
||||
logger,
|
||||
onError: (params) => {
|
||||
onError: (params, helpers) => {
|
||||
const { otherData } = params
|
||||
|
||||
if (import.meta.server && otherData?.isAppError) {
|
||||
serverFatalError = params
|
||||
}
|
||||
|
||||
transports.forEach((handler) => handler.onError(params))
|
||||
transports.forEach((handler) => handler.onError(params, helpers))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Plugin } from 'nuxt/dist/app/nuxt'
|
||||
import { isH3Error } from '~/lib/common/helpers/error'
|
||||
import { useRequestId, useServerRequestId } from '~/lib/core/composables/server'
|
||||
import { isBrave, isSafari } from '@speckle/shared'
|
||||
import { isString } from 'lodash-es'
|
||||
|
||||
type PluginNuxtApp = Parameters<Plugin>[0]
|
||||
|
||||
@@ -69,15 +70,28 @@ function initRumClient(app: PluginNuxtApp) {
|
||||
: {}
|
||||
|
||||
registerErrorTransport({
|
||||
onError: ({ args, firstError, firstString, otherData, nonObjectOtherData }) => {
|
||||
onError: (
|
||||
{ args, firstError, firstString, otherData, nonObjectOtherData },
|
||||
{ prettifyMessage }
|
||||
) => {
|
||||
if (!datadog || !('addError' in datadog)) return
|
||||
|
||||
const error = firstError || firstString || args[0]
|
||||
let error = firstError || firstString || args[0]
|
||||
const mainErrorMessageTemplate = firstString
|
||||
const mainErrorMessage = mainErrorMessageTemplate
|
||||
? prettifyMessage(mainErrorMessageTemplate)
|
||||
: undefined
|
||||
|
||||
if (isString(error)) {
|
||||
error = prettifyMessage(error)
|
||||
}
|
||||
|
||||
datadog.addError(error, {
|
||||
...otherData,
|
||||
...resolveH3Data(firstError),
|
||||
extraData: nonObjectOtherData,
|
||||
mainErrorMessage: firstString,
|
||||
mainErrorMessageTemplate,
|
||||
mainErrorMessage,
|
||||
isProperlySentError: true
|
||||
})
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user