From e6841f39e96767bb12bb5f2a9d6b6e85d4f82f78 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 15 Aug 2023 21:24:59 +0200 Subject: [PATCH 1/4] Enhanced logging v1 --- packages/atlas/package.json | 2 + .../WithdrawFundsDialog.tsx | 2 + .../_video/VideoPlayer/VideoPlayer.tsx | 21 +- packages/atlas/src/embedded/main.tsx | 4 +- packages/atlas/src/hooks/useCreateMember.ts | 2 +- packages/atlas/src/hooks/useGetAssetUrl.ts | 44 +++- packages/atlas/src/main.tsx | 4 +- .../src/providers/assets/assets.helpers.ts | 23 +- .../src/providers/assets/assets.provider.tsx | 11 +- .../src/providers/uploads/uploads.hooks.ts | 16 +- .../src/providers/uploads/uploads.types.ts | 2 +- .../src/providers/user/user.provider.tsx | 4 +- packages/atlas/src/utils/getVideoCodec.ts | 36 ++++ packages/atlas/src/utils/logs/asset.ts | 204 ++++++++++++++---- .../YppAuthorizationDetailsFormStep.tsx | 4 +- .../VideoWorkspace/VideoWorkspace.hooks.ts | 24 ++- yarn.lock | 59 +++++ 17 files changed, 393 insertions(+), 69 deletions(-) create mode 100644 packages/atlas/src/utils/getVideoCodec.ts diff --git a/packages/atlas/package.json b/packages/atlas/package.json index 2d325a5239..d8d016bcf1 100644 --- a/packages/atlas/package.json +++ b/packages/atlas/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "@apollo/client": "^3.7.2", + "@elastic/apm-rum": "^5.12.0", "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", "@hcaptcha/react-hcaptcha": "^1.4.4", @@ -68,6 +69,7 @@ "immer": "^9.0.16", "localforage": "^1.10.0", "lodash-es": "^4.17.21", + "mp4box": "^0.5.2", "multihashes": "^4.0.3", "postcss-syntax": "^0.36.2", "rc-slider": "^10.1.0", diff --git a/packages/atlas/src/components/_overlays/SendTransferDialogs/WithdrawFundsDialog.tsx b/packages/atlas/src/components/_overlays/SendTransferDialogs/WithdrawFundsDialog.tsx index 9789acf88c..7a7c493938 100644 --- a/packages/atlas/src/components/_overlays/SendTransferDialogs/WithdrawFundsDialog.tsx +++ b/packages/atlas/src/components/_overlays/SendTransferDialogs/WithdrawFundsDialog.tsx @@ -15,6 +15,7 @@ import { useSegmentAnalytics } from '@/hooks/useSegmentAnalytics' import { hapiBnToTokenNumber, tokenNumberToHapiBn } from '@/joystream-lib/utils' import { useFee, useJoystream, useTokenPrice } from '@/providers/joystream' import { useTransaction } from '@/providers/transactions/transactions.hooks' +import { UserEventsLogger } from '@/utils/logs' import { formatNumber } from '@/utils/number' import { useChannelPaymentsHistory } from '@/views/studio/MyPaymentsView/PaymentsTransactions/PaymentTransactions.hooks' @@ -95,6 +96,7 @@ export const WithdrawFundsDialog: FC = ({ ), onTxSync: async () => { fetchPaymentsData() + UserEventsLogger.logFundsWithdrawal(channelId, formatNumber(data.amount || 0)) trackWithdrawnFunds(channelId, formatNumber(data.amount || 0)) onExitClick() }, diff --git a/packages/atlas/src/components/_video/VideoPlayer/VideoPlayer.tsx b/packages/atlas/src/components/_video/VideoPlayer/VideoPlayer.tsx index f24b1d27ec..da34b19d43 100644 --- a/packages/atlas/src/components/_video/VideoPlayer/VideoPlayer.tsx +++ b/packages/atlas/src/components/_video/VideoPlayer/VideoPlayer.tsx @@ -23,7 +23,7 @@ import { useMediaMatch } from '@/hooks/useMediaMatch' import { useSegmentAnalytics, videoPlaybackParams } from '@/hooks/useSegmentAnalytics' import { usePersonalDataStore } from '@/providers/personalData' import { isMobile } from '@/utils/browser' -import { ConsoleLogger, SentryLogger } from '@/utils/logs' +import { ConsoleLogger, SentryLogger, UserEventsLogger } from '@/utils/logs' import { formatDurationShort } from '@/utils/time' import { ControlsIndicator } from './ControlsIndicator' @@ -160,6 +160,8 @@ const VideoPlayerComponent: ForwardRefRenderFunction('loading') @@ -222,6 +224,9 @@ const VideoPlayerComponent: ForwardRefRenderFunction { if (event.type === 'waiting' || event.type === 'seeking') { + if (event.type === 'waiting') { + setWaitingStateCount((prev) => prev + 1) + if (waitingStateCount > 3 && !slowNetworkEventSent) { + UserEventsLogger.logPlaybackIsSlowEvent({ + ...videoPlaybackData, + dataObjectId: video?.media?.id ?? '', + dataObjectType: 'video', + resolvedUrl: player.src() ?? '', + }) + setSlowNetworkEventSent(true) + } + } setPlayerState('loading') } if (event.type === 'canplay' || event.type === 'seeked') { @@ -351,7 +368,7 @@ const VideoPlayerComponent: ForwardRefRenderFunction { player.off(['waiting', 'canplay', 'seeking', 'seeked'], handler) } - }, [player, playerState]) + }, [player, playerState, slowNetworkEventSent, video?.media?.id, videoPlaybackData, waitingStateCount]) useEffect(() => { if (!player) { diff --git a/packages/atlas/src/embedded/main.tsx b/packages/atlas/src/embedded/main.tsx index ed25485f9b..44951437e0 100644 --- a/packages/atlas/src/embedded/main.tsx +++ b/packages/atlas/src/embedded/main.tsx @@ -2,14 +2,14 @@ import { createRoot } from 'react-dom/client' import { atlasConfig } from '@/config' import { BUILD_ENV } from '@/config/env' -import { AssetLogger, SentryLogger } from '@/utils/logs' +import { SentryLogger, UserEventsLogger } from '@/utils/logs' import { App } from './App' const initApp = async () => { if (BUILD_ENV === 'production') { SentryLogger.initialize(atlasConfig.analytics.sentry?.dsn) - AssetLogger.initialize(atlasConfig.analytics.assetLogs?.url) + UserEventsLogger.initialize() } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/packages/atlas/src/hooks/useCreateMember.ts b/packages/atlas/src/hooks/useCreateMember.ts index 9c5b3f5cbe..a104968d8a 100644 --- a/packages/atlas/src/hooks/useCreateMember.ts +++ b/packages/atlas/src/hooks/useCreateMember.ts @@ -127,7 +127,7 @@ export const useCreateMember = () => { iconType: 'error', }) onError?.() - SentryLogger.error('Failed to upload member avatar', 'SignUpModal', error) + SentryLogger.error('Failed to upload member avatar', 'SignUpModal', JSON.stringify(error)) return } diff --git a/packages/atlas/src/hooks/useGetAssetUrl.ts b/packages/atlas/src/hooks/useGetAssetUrl.ts index baed3e55b1..dbee3bad78 100644 --- a/packages/atlas/src/hooks/useGetAssetUrl.ts +++ b/packages/atlas/src/hooks/useGetAssetUrl.ts @@ -1,13 +1,26 @@ +// import { init as initApm } from '@elastic/apm-rum' import { useEffect, useState } from 'react' import { atlasConfig } from '@/config' -import { testAssetDownload } from '@/providers/assets/assets.helpers' +import { logDistributorPerformance, testAssetDownload } from '@/providers/assets/assets.helpers' import { useOperatorsContext } from '@/providers/assets/assets.provider' -import { ConsoleLogger } from '@/utils/logs' +import { getVideoCodec } from '@/utils/getVideoCodec' +import { ConsoleLogger, DistributorEventEntry, SentryLogger, UserEventsLogger } from '@/utils/logs' import { withTimeout } from '@/utils/misc' +// const apm = initApm({ +// serviceName: 'gleev', +// +// // Set custom APM Server URL (default: http://localhost:8200) +// serverUrl: `https://atlas-services.joystream.org/apm`, +// +// // Set service version (required for sourcemap feature) +// serviceVersion: '', +// environment: 'development' +// }) + export const getSingleAssetUrl = async ( - urls: string[] | undefined | null, + urls: string[] | null | undefined, type: 'image' | 'video' | 'subtitle', timeout?: number ): Promise => { @@ -16,6 +29,12 @@ export const getSingleAssetUrl = async ( } for (const distributionAssetUrl of urls) { + const eventEntry: DistributorEventEntry = { + dataObjectId: '1', + dataObjectType: 'DataObjectTypeChannelAvatar', + resolvedUrl: distributionAssetUrl, + } + const assetTestPromise = testAssetDownload(distributionAssetUrl, type) const assetTestPromiseWithTimeout = withTimeout( assetTestPromise, @@ -23,11 +42,24 @@ export const getSingleAssetUrl = async ( ) try { + // const transaction = apm.startTransaction('Application start', 'custom') + // const httpSpan = transaction?.startSpan('GET ' + distributionAssetUrl, 'external.http') + await assetTestPromiseWithTimeout + // httpSpan?.end() + // transaction?.end() + logDistributorPerformance(distributionAssetUrl, eventEntry) + return distributionAssetUrl - } catch { - /**/ + } catch (err) { + if (err instanceof MediaError) { + const codec = getVideoCodec(distributionAssetUrl) + UserEventsLogger.logWrongCodecEvent(eventEntry, { codec }) + SentryLogger.error('Error during asset download test, media is not supported', 'AssetsManager', err, { + asset: { parent, distributionAssetUrl, mediaError: err, codec }, + }) + } } } @@ -39,7 +71,7 @@ export const getSingleAssetUrl = async ( promises.push(assetTestPromise) } - Promise.race(promises) + Promise.any(promises) .then(res) .catch((error) => { ConsoleLogger.warn(`Error during fallback asset promise race`, { diff --git a/packages/atlas/src/main.tsx b/packages/atlas/src/main.tsx index b1811d3c33..989f422d47 100644 --- a/packages/atlas/src/main.tsx +++ b/packages/atlas/src/main.tsx @@ -3,15 +3,15 @@ import { createRoot } from 'react-dom/client' import { atlasConfig } from '@/config' import { BUILD_ENV } from '@/config/env' -import { AssetLogger, SentryLogger } from '@/utils/logs' +import { SentryLogger, UserEventsLogger } from '@/utils/logs' import { App } from './App' const initApp = async () => { if (BUILD_ENV === 'production') { SentryLogger.initialize(atlasConfig.analytics.sentry?.dsn) - AssetLogger.initialize(atlasConfig.analytics.assetLogs?.url) } + UserEventsLogger.initialize(atlasConfig.analytics.assetLogs?.url) if (typeof globalThis !== 'undefined') { globalThis.Buffer = Buffer diff --git a/packages/atlas/src/providers/assets/assets.helpers.ts b/packages/atlas/src/providers/assets/assets.helpers.ts index aefff2e0d2..c08541fb32 100644 --- a/packages/atlas/src/providers/assets/assets.helpers.ts +++ b/packages/atlas/src/providers/assets/assets.helpers.ts @@ -1,7 +1,7 @@ import { axiosInstance } from '@/api/axios' import { BasicMembershipFieldsFragment } from '@/api/queries/__generated__/fragments.generated' import { BUILD_ENV } from '@/config/env' -import { AssetLogger, ConsoleLogger, DataObjectResponseMetric, DistributorEventEntry } from '@/utils/logs' +import { ConsoleLogger, DistributorEventEntry, DistributorEventMetric, UserEventsLogger } from '@/utils/logs' import { wait } from '@/utils/misc' export const getMemberAvatar = (member?: BasicMembershipFieldsFragment | null) => { @@ -47,6 +47,7 @@ export const testAssetDownload = (url: string, type: 'image' | 'video' | 'subtit const reject = (err?: unknown) => { cleanup() + UserEventsLogger.logAssetUploadFailedEvent({ resolvedUrl: url }, err as Error) _reject(err) } @@ -64,7 +65,7 @@ export const testAssetDownload = (url: string, type: 'image' | 'video' | 'subtit video.addEventListener('loadeddata', resolve) video.addEventListener('canplay', resolve) video.addEventListener('progress', resolve) - video.addEventListener('error', (err) => { + video.addEventListener('error', async (err) => { if (err.target) { reject((err.target as HTMLVideoElement).error) } else { @@ -73,7 +74,14 @@ export const testAssetDownload = (url: string, type: 'image' | 'video' | 'subtit }) video.src = url } else if (type === 'subtitle') { - fetch(url, { method: 'HEAD', cache: 'no-store' }).then(resolve).catch(reject) + fetch(url, { method: 'HEAD', mode: 'cors', cache: 'no-store' }) + .then((response) => { + if (!response.ok) { + UserEventsLogger.logAssetUploadFailedEvent({ resolvedUrl: url }, new Error(response.statusText)) + } + resolve() + }) + .catch(reject) } else { ConsoleLogger.warn('Encountered unknown asset type', { url, type }) reject() @@ -82,7 +90,7 @@ export const testAssetDownload = (url: string, type: 'image' | 'video' | 'subtit } export const logDistributorPerformance = async (assetUrl: string, eventEntry: DistributorEventEntry) => { - if (!AssetLogger.isEnabled) return + if (!UserEventsLogger) return // delay execution for 1s to make sure performance entries get populated await wait(1000) @@ -101,13 +109,12 @@ export const logDistributorPerformance = async (assetUrl: string, eventEntry: Di // if resource size is considerably larger than over-the-wire transfer size, we can assume we got the result from the cache return } - - const metric: DataObjectResponseMetric = { + const metric: DistributorEventMetric = { initialResponseTime: responseStart - fetchStart, fullResponseTime: responseEnd - fetchStart, + downloadSpeed: decodedBodySize / (responseEnd - responseStart), } - - AssetLogger.logDistributorResponseTime(eventEntry, metric) + UserEventsLogger.logDistributorResponseTime(eventEntry, metric) } export const getFastestImageUrl = async (urls: string[]) => { diff --git a/packages/atlas/src/providers/assets/assets.provider.tsx b/packages/atlas/src/providers/assets/assets.provider.tsx index 4f1b18982b..f0ed19d0ab 100644 --- a/packages/atlas/src/providers/assets/assets.provider.tsx +++ b/packages/atlas/src/providers/assets/assets.provider.tsx @@ -33,7 +33,7 @@ import { absoluteRoutes } from '@/config/routes' import { useMountEffect } from '@/hooks/useMountEffect' import { getFastestImageUrl } from '@/providers/assets/assets.helpers' import { UserCoordinates, useUserLocationStore } from '@/providers/userLocation' -import { ConsoleLogger, SentryLogger } from '@/utils/logs' +import { ConsoleLogger, SentryLogger, UserEventsLogger } from '@/utils/logs' import { OperatorInfo } from './assets.types' @@ -171,6 +171,13 @@ export const OperatorsContextProvider: FC = ({ children }) => const msDuration = performance.now() - startTime const previousTimeout = userBenchmarkTime ?? atlasConfig.storage.assetResponseTimeout if (msDuration > previousTimeout || msDuration < previousTimeout / 2) { + const newTime = (msDuration + previousTimeout) * 0.75 + if (newTime > 2_000) { + SentryLogger.message('User benchmark time is too high', 'OperatorsContextProvider', 'warning', { + event: { 'userBenchmarkTime': newTime }, + }) + } + UserEventsLogger.logUserBenchmarkTime(newTime) setUserBenchmarkTime((msDuration + previousTimeout) * 0.75) } } @@ -226,6 +233,7 @@ export const useStorageOperators = () => { } const workingBagOperators = bagOperators.filter((operator) => !failedStorageOperatorIds.includes(operator.id)) if (!workingBagOperators || !workingBagOperators.length) { + UserEventsLogger.logMissingOperatorsForBag(storageBagId) ConsoleLogger.warn('Missing storage operators for storage bag', { storageBagId }) return null } @@ -278,6 +286,7 @@ export const useStorageOperators = () => { const markStorageOperatorFailed = useCallback( (operatorId: string) => { + UserEventsLogger.logDistributorBlacklistedEvent({ distributorId: operatorId }) setFailedStorageOperatorIds((state) => [...state, operatorId]) }, [setFailedStorageOperatorIds] diff --git a/packages/atlas/src/providers/uploads/uploads.hooks.ts b/packages/atlas/src/providers/uploads/uploads.hooks.ts index 2c05a81217..ab52595c7b 100644 --- a/packages/atlas/src/providers/uploads/uploads.hooks.ts +++ b/packages/atlas/src/providers/uploads/uploads.hooks.ts @@ -12,7 +12,7 @@ import { useStorageOperators } from '@/providers/assets/assets.provider' import { OperatorInfo } from '@/providers/assets/assets.types' import { UploadStatus } from '@/types/storage' import { createAssetUploadEndpoint, createChannelBagId } from '@/utils/asset' -import { ConsoleLogger, SentryLogger } from '@/utils/logs' +import { ConsoleLogger, SentryLogger, UserEventsLogger } from '@/utils/logs' import { useUploadsStore } from './uploads.store' import { InputAssetUpload, StartFileUploadOptions } from './uploads.types' @@ -83,6 +83,7 @@ export const useStartFileUpload = () => { 'None of the storage operators are available at this time. Please reload the app and try again later.', iconType: 'error', }) + UserEventsLogger.logUserError('missing-storage-operator', { id: asset.id, assetType: asset.type, bagId }) SentryLogger.error('No storage operator available for upload', 'uploadsHooks') return } @@ -175,13 +176,13 @@ export const useStartFileUpload = () => { SentryLogger.error('Failed to upload asset', 'uploadsHooks', e, { asset: { dataObjectId: asset.id, uploadOperator }, }) - setAssetStatus({ lastStatus: 'error', progress: 0 }) const axiosError = e as AxiosError const networkFailure = axiosError.isAxiosError && (!axiosError.response?.status || (axiosError.response.status >= 400 && axiosError.response.status <= 500)) + UserEventsLogger.logDistributorError({ dataObjectId: asset.id, distributorId: uploadOperator.id }, e) if (networkFailure) { markStorageOperatorFailed(uploadOperator.id) } @@ -192,6 +193,17 @@ export const useStartFileUpload = () => { } const snackbarDescription = networkFailure ? 'Host is not responding' : 'Unexpected error occurred' + + UserEventsLogger.logAssetUploadFailedEvent( + { + dataObjectId: asset.id, + dataObjectType: asset.type, + distributorId: uploadOperator.id, + distributorUrl: uploadOperator.endpoint, + }, + e + ) + displaySnackbar({ title: 'Failed to upload asset', description: snackbarDescription, diff --git a/packages/atlas/src/providers/uploads/uploads.types.ts b/packages/atlas/src/providers/uploads/uploads.types.ts index aaabe85cf9..4b73ec6811 100644 --- a/packages/atlas/src/providers/uploads/uploads.types.ts +++ b/packages/atlas/src/providers/uploads/uploads.types.ts @@ -2,7 +2,7 @@ import { ChannelId, VideoId } from '@/joystream-lib/types' import { AssetDimensions, ImageCropData } from '@/types/cropper' import { UploadStatus } from '@/types/storage' -type AssetType = 'video' | 'thumbnail' | 'cover' | 'avatar' | 'subtitles' +export type AssetType = 'video' | 'thumbnail' | 'cover' | 'avatar' | 'subtitles' export type AssetParent = 'video' | 'channel' export type AssetUpload = { diff --git a/packages/atlas/src/providers/user/user.provider.tsx b/packages/atlas/src/providers/user/user.provider.tsx index 852888683e..3bf6fa6d06 100644 --- a/packages/atlas/src/providers/user/user.provider.tsx +++ b/packages/atlas/src/providers/user/user.provider.tsx @@ -3,7 +3,7 @@ import { FC, PropsWithChildren, createContext, useCallback, useContext, useEffec import { useMemberships } from '@/api/hooks/membership' import { ViewErrorFallback } from '@/components/ViewErrorFallback' import { useJoystream } from '@/providers/joystream/joystream.provider' -import { AssetLogger, SentryLogger } from '@/utils/logs' +import { SentryLogger, UserEventsLogger } from '@/utils/logs' import { UserContextValue } from './user.types' @@ -60,7 +60,7 @@ export const UserProvider: FC = ({ children }) => { } SentryLogger.setUser(user) - AssetLogger.setUser(user) + UserEventsLogger.setUser(user) }, [ currentMemberships, currentUser?.email, diff --git a/packages/atlas/src/utils/getVideoCodec.ts b/packages/atlas/src/utils/getVideoCodec.ts new file mode 100644 index 0000000000..74d371bb95 --- /dev/null +++ b/packages/atlas/src/utils/getVideoCodec.ts @@ -0,0 +1,36 @@ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-nocheck +//disabled because mp4box has no typescript support +import mp4box from 'mp4box/dist/mp4box.simple.js' + +import { ConsoleLogger } from '@/utils/logs' + +async function fetchPartialContent(url: string, range: { start: number; end: number }) { + const headers = new Headers() + headers.append('Range', `bytes=${range.start}-${range.end}`) + + try { + const response = await fetch(url, { headers }) + if (response.status === 206) { + return await response.arrayBuffer() + } + } catch (error) { + ConsoleLogger.warn('Error fetching partial content:', error) + } + return null +} + +export const getVideoCodec = async (url: string): string => { + const fetchRange = { start: 0, end: 8192 } + const arrayBuffer = await fetchPartialContent(url, fetchRange) + if (arrayBuffer) { + arrayBuffer.fileStart = 0 + const mp4boxFile = mp4box.createFile() + mp4boxFile.appendBuffer(arrayBuffer) + mp4boxFile.flush() + + const codec = mp4boxFile.getInfo()?.videoTracks[0]?.codec + return codec + } + return '' +} diff --git a/packages/atlas/src/utils/logs/asset.ts b/packages/atlas/src/utils/logs/asset.ts index 91159cef7f..234259d79f 100644 --- a/packages/atlas/src/utils/logs/asset.ts +++ b/packages/atlas/src/utils/logs/asset.ts @@ -1,7 +1,9 @@ -import { debounce } from 'lodash-es' +import { throttle } from 'lodash-es' import { axiosInstance } from '@/api/axios' import { DataObjectType } from '@/api/queries/__generated__/baseTypes.generated' +import { NftIssuanceInputMetadata } from '@/joystream-lib/types' +import { AssetType } from '@/providers/uploads/uploads.types' import { ConsoleLogger } from './console' import { SentryLogger } from './sentry' @@ -11,28 +13,48 @@ type DistributorEventDetails = { distributorUrl?: string | null } -type StorageProviderEventDetails = { - storageProviderId: string - storageProviderUrl?: string | null +export type DistributorEventMetric = { + initialResponseTime?: number + fullResponseTime?: number + downloadSpeed?: number } -type StorageEvent = { +type UserPerformanceEvent = UserLogEvent + +type ErrorEvent = UserErrorEvent | DistributorErrorEvent + +type UserErrorEvent = { + error?: string + details?: { + [key: string]: unknown + } +} & UserLogEvent + +type DistributorErrorEvent = { + error?: string + details?: { + [key: string]: unknown + } +} & DistributorEventEntry + +type UserLogEvent = { type: string + userDevice?: string + user?: Record [x: string]: unknown -} & (DistributorEventDetails | StorageProviderEventDetails) +} export type DistributorEventEntry = { - dataObjectId: string - dataObjectType: DataObjectType['__typename'] - resolvedUrl: string + dataObjectId?: string + dataObjectType?: DataObjectType['__typename'] | AssetType + resolvedUrl?: string } & DistributorEventDetails -export type DataObjectResponseMetric = { - initialResponseTime: number - fullResponseTime?: number +type CodecInfo = { + codec?: string } -class _AssetLogger { +class _UserEventsLogger { private logUrl = '' private user?: Record @@ -49,63 +71,171 @@ class _AssetLogger { this.user = user } - get isEnabled() { - return !!this.logUrl - } + private pendingPerformanceEvents: UserPerformanceEvent[] = [] + private pendingErrorEvents: ErrorEvent[] = [] - private pendingEvents: StorageEvent[] = [] + private sendPerformanceEvents = throttle(async () => { + if (!this.pendingPerformanceEvents.length) return - private sendEvents = debounce(async () => { - if (!this.pendingEvents.length) return - if (!this.logUrl) return + ConsoleLogger.debug(`Sending ${this.pendingPerformanceEvents.length} performance events`) - ConsoleLogger.debug(`Sending ${this.pendingEvents.length} asset events`) + const payload = { + events: this.pendingPerformanceEvents, + } + this.pendingPerformanceEvents = [] + + try { + await axiosInstance.post(this.logUrl, payload) + } catch (e) { + SentryLogger.error('Failed to send performance events', 'UserEventsLogger', e) + } + }, 60 * 1000) + + private sendErrorEvents = throttle(async () => { + if (!this.pendingErrorEvents.length) return + + ConsoleLogger.debug(`Sending ${this.pendingErrorEvents.length} error events`) const payload = { - events: this.pendingEvents, + events: this.pendingErrorEvents, } - this.pendingEvents = [] + this.pendingErrorEvents = [] try { await axiosInstance.post(this.logUrl, payload) } catch (e) { - SentryLogger.error('Failed to send asset events', 'AssetLogger', e, { request: { url: this.logUrl } }) + SentryLogger.error('Failed to send asset events', 'UserEventsLogger', e) } - }, 2000) + }, 5 * 1000) - private addEvent(event: StorageEvent) { + private addPerformanceEvent(event: UserPerformanceEvent | UserLogEvent) { + const eventWithUser = { + ...event, + user: this.user, + } + this.pendingPerformanceEvents.push(eventWithUser) + this.sendPerformanceEvents() + } + private addErrorEvent(event: ErrorEvent) { const eventWithUser = { ...event, user: this.user, } - this.pendingEvents.push(eventWithUser) - this.sendEvents() + this.pendingErrorEvents.push(eventWithUser) + this.sendErrorEvents() } - logDistributorResponseTime(entry: DistributorEventEntry, metric: DataObjectResponseMetric) { - const event: StorageEvent = { + logDistributorResponseTime(entry: DistributorEventEntry, metric: DistributorEventMetric) { + const event: UserPerformanceEvent = { type: 'distributor-response-time', ...entry, ...metric, } - this.addEvent(event) + this.addPerformanceEvent(event) } - logDistributorError(entry: DistributorEventEntry) { - const event: StorageEvent = { + logDistributorError(entry: DistributorEventEntry, error: Error) { + const event: UserLogEvent = { type: 'distributor-response-error', ...entry, + error: error.message, } - this.addEvent(event) + this.addErrorEvent(event) } logDistributorResponseTimeout(entry: DistributorEventEntry) { - const event: StorageEvent = { + const event: UserPerformanceEvent = { type: 'distributor-response-timeout', ...entry, } - this.addEvent(event) + this.addPerformanceEvent(event) + } + + logDistributorBlacklistedEvent(entry: DistributorEventEntry) { + const event: UserLogEvent = { + type: 'distributor-blacklisted', + ...entry, + } + this.addErrorEvent(event) + } + + logPlaybackIsSlowEvent(entry: DistributorEventEntry) { + const event: UserPerformanceEvent = { + type: 'playback-is-slow', + ...entry, + } + this.addPerformanceEvent(event) + } + + logWrongCodecEvent(entry: DistributorEventEntry, info: CodecInfo) { + const event: UserLogEvent = { + type: 'playback-wrong-codec', + ...entry, + ...info, + } + this.addErrorEvent(event) + } + + logAssetUploadFailedEvent(entry: DistributorEventEntry, error: Error) { + const event: UserLogEvent = { + type: 'asset-upload-failed', + ...entry, + error: error.message, + } + this.addErrorEvent(event) + } + + logMissingOperatorsForBag(storageBagId: string) { + const event: UserLogEvent = { + type: 'missing-operators-for-bag', + storageBagId, + } + this.addErrorEvent(event) + } + + logNftMintingFailedEvent(nft?: NftIssuanceInputMetadata, error?: Error) { + const event: UserLogEvent = { + type: 'nft-minting-failed', + details: { + nft, + }, + error: error?.message, + } + this.addErrorEvent(event) + } + + logFundsWithdrawal(channelId: string, amount: string) { + const event: UserLogEvent = { + type: 'funds-withdrawal', + channelId, + amount, + } + this.addPerformanceEvent(event) + } + + logUserBenchmarkTime(time: number) { + const event: UserLogEvent = { + type: 'user-statistics-benchmark-time', + time, + } + this.addPerformanceEvent(event) + } + + logUserError(type: string, error: Record) { + const event: UserLogEvent = { + type, + ...error, + } + this.addErrorEvent(event) + } + + logUserEvent(type: string, details: Record) { + const event: UserLogEvent = { + type, + ...details, + } + this.addPerformanceEvent(event) } } -export const AssetLogger = new _AssetLogger() +export const UserEventsLogger = new _UserEventsLogger() diff --git a/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationDetailsFormStep/YppAuthorizationDetailsFormStep.tsx b/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationDetailsFormStep/YppAuthorizationDetailsFormStep.tsx index 8798b1e20f..b1708f059f 100644 --- a/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationDetailsFormStep/YppAuthorizationDetailsFormStep.tsx +++ b/packages/atlas/src/views/global/YppLandingView/YppAuthorizationModal/YppAuthorizationSteps/YppAuthorizationDetailsFormStep/YppAuthorizationDetailsFormStep.tsx @@ -1,5 +1,6 @@ import { FC, useCallback, useEffect, useState } from 'react' import { Controller, useFormContext } from 'react-hook-form' +import shallow from 'zustand/shallow' import { GetExtendedBasicChannelsDocument, @@ -36,14 +37,13 @@ const categoriesSelectItems: SelectItem[] = export const YppAuthorizationDetailsFormStep: FC = () => { const [foundChannel, setFoundChannel] = useState() const { memberId } = useUser() + const { referrerId } = useYppStore((store) => store, shallow) const { control, formState: { errors }, setValue, } = useFormContext() - const referrerId = useYppStore((state) => state.referrerId) - useEffect(() => { if (referrerId) { setValue('referrerChannelId', referrerId) diff --git a/packages/atlas/src/views/studio/VideoWorkspace/VideoWorkspace.hooks.ts b/packages/atlas/src/views/studio/VideoWorkspace/VideoWorkspace.hooks.ts index b293c4b069..d880734512 100644 --- a/packages/atlas/src/views/studio/VideoWorkspace/VideoWorkspace.hooks.ts +++ b/packages/atlas/src/views/studio/VideoWorkspace/VideoWorkspace.hooks.ts @@ -21,7 +21,7 @@ import { useAuthorizedUser } from '@/providers/user/user.hooks' import { VideoFormData, VideoWorkspace, useVideoWorkspace, useVideoWorkspaceData } from '@/providers/videoWorkspace' import { modifyAssetUrlInCache, writeVideoDataInCache } from '@/utils/cachingAssets' import { createLookup } from '@/utils/data' -import { ConsoleLogger, SentryLogger } from '@/utils/logs' +import { ConsoleLogger, SentryLogger, UserEventsLogger } from '@/utils/logs' export const useHandleVideoWorkspaceSubmit = () => { const { setIsWorkspaceOpen, editedVideoInfo, setEditedVideo } = useVideoWorkspace() @@ -162,7 +162,12 @@ export const useHandleVideoWorkspaceSubmit = () => { uploadPromises.push(...subtitlesUploadPromises) } - Promise.all(uploadPromises).catch((e) => SentryLogger.error('Unexpected upload failure', 'VideoWorkspace', e)) + Promise.all(uploadPromises).catch((e) => { + if (videoInfo?.mintNft) { + UserEventsLogger.logNftMintingFailedEvent(data.nftMetadata, e) + } + SentryLogger.error('Unexpected upload failure', 'VideoWorkspace', e) + }) } const refetchDataAndUploadAssets = async (result: VideoExtrinsicResult) => { @@ -241,7 +246,20 @@ export const useHandleVideoWorkspaceSubmit = () => { }) if (completed) { - !!data.nftMetadata && trackNftMint(data.metadata.title ?? 'no data', channelId) + UserEventsLogger.logUserEvent(isNew ? 'user-action-video-created' : 'user-action-video-updated', { + memberId, + metadata: data.metadata, + nftMetadata: data.nftMetadata, + }) + + if (data.nftMetadata) { + trackNftMint(data.metadata.title ?? 'no data', channelId) + UserEventsLogger.logUserEvent('user-action-nft-minted', { + memberId, + metadata: data.metadata, + nftMetadata: data.nftMetadata, + }) + } assetsToBeRemoved?.forEach((asset) => { removeAssetFromUploads(asset) diff --git a/yarn.lock b/yarn.lock index 09483c3082..7028a35a6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2702,6 +2702,26 @@ __metadata: languageName: node linkType: hard +"@elastic/apm-rum-core@npm:^5.17.0": + version: 5.17.0 + resolution: "@elastic/apm-rum-core@npm:5.17.0" + dependencies: + error-stack-parser: ^1.3.5 + opentracing: ^0.14.3 + promise-polyfill: ^8.1.3 + checksum: 963d3aab9ef2bd61d57e88cdb42d607298571e3add55ecce5c9e395fe61668a6616227ae51ad3e9c48fe049130a73a691c3e9d5551565b11858c816bc66a6f1b + languageName: node + linkType: hard + +"@elastic/apm-rum@npm:^5.12.0": + version: 5.12.0 + resolution: "@elastic/apm-rum@npm:5.12.0" + dependencies: + "@elastic/apm-rum-core": ^5.17.0 + checksum: b1f0b8bc9b77d9bd0a2cb2993d94f741e878823d8e67a2428858bba6774a69c5ae0480e70d8b9d0816119078491eaa3d0f64c824d617fa49b7e637d81df0503d + languageName: node + linkType: hard + "@emotion/babel-plugin@npm:^11.10.5": version: 11.10.5 resolution: "@emotion/babel-plugin@npm:11.10.5" @@ -4221,6 +4241,7 @@ __metadata: resolution: "@joystream/atlas@workspace:packages/atlas" dependencies: "@apollo/client": ^3.7.2 + "@elastic/apm-rum": ^5.12.0 "@emotion/babel-plugin": ^11.10.5 "@emotion/react": ^11.10.5 "@emotion/styled": ^11.10.5 @@ -4296,6 +4317,7 @@ __metadata: localforage: ^1.10.0 lodash-es: ^4.17.21 madge: ^5.0.1 + mp4box: ^0.5.2 multihashes: ^4.0.3 postcss-syntax: ^0.36.2 rc-slider: ^10.1.0 @@ -12710,6 +12732,15 @@ __metadata: languageName: node linkType: hard +"error-stack-parser@npm:^1.3.5": + version: 1.3.6 + resolution: "error-stack-parser@npm:1.3.6" + dependencies: + stackframe: ^0.3.1 + checksum: 0bfbb5e234a8b23091c34f796697f4e7b4f805ca90bdc8b57d144f79051b834c8590917f5167a8f49c0f90206c8fb986bf5b28fe3ea8efa201f09caf3f0a7af2 + languageName: node + linkType: hard + "es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.5": version: 1.20.1 resolution: "es-abstract@npm:1.20.1" @@ -17460,6 +17491,13 @@ __metadata: languageName: node linkType: hard +"mp4box@npm:^0.5.2": + version: 0.5.2 + resolution: "mp4box@npm:0.5.2" + checksum: 93e6d9c69fa77e402927586e30932aee40d495ab832e9710cc8911a9bf2422b76f1b38e6a6520af41c1a199021ef908e6200c127cb4af1a86ddf3d4431725e90 + languageName: node + linkType: hard + "mpd-parser@npm:0.21.1": version: 0.21.1 resolution: "mpd-parser@npm:0.21.1" @@ -18100,6 +18138,13 @@ __metadata: languageName: node linkType: hard +"opentracing@npm:^0.14.3": + version: 0.14.7 + resolution: "opentracing@npm:0.14.7" + checksum: 5f7e44439062d056a2a72ac89eff463c9cf5659a2aea230ff7f5a226c5e960c195ce04ec2e2cc590140bbb9c5d2be11a5a50a23484cbe2d0e132af4309d4c904 + languageName: node + linkType: hard + "optimism@npm:^0.16.1": version: 0.16.1 resolution: "optimism@npm:0.16.1" @@ -18756,6 +18801,13 @@ __metadata: languageName: node linkType: hard +"promise-polyfill@npm:^8.1.3": + version: 8.3.0 + resolution: "promise-polyfill@npm:8.3.0" + checksum: 206373802076c77def0805758d0a8ece64120dfa6603f092404a1004211f8f2f67f33cadbc35953fc2a8ed0b0d38c774e88bdf01e20ce7a920723a60df84b7a5 + languageName: node + linkType: hard + "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -20714,6 +20766,13 @@ __metadata: languageName: node linkType: hard +"stackframe@npm:^0.3.1": + version: 0.3.1 + resolution: "stackframe@npm:0.3.1" + checksum: 510123ec2d9a02dba8153091f0b0c760141ae4ec4be9512e74b83333e39a762380f13d9afcb469fd9ce978bc07fb5181d6f28970a18ae4070bf21d1e4caa4698 + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" From a52a7828c22453307dca70f5b00302960c16c43c Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 22 Aug 2023 20:43:11 +0200 Subject: [PATCH 2/4] restored logs url --- packages/atlas/src/embedded/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/atlas/src/embedded/main.tsx b/packages/atlas/src/embedded/main.tsx index 44951437e0..fdb542ba0b 100644 --- a/packages/atlas/src/embedded/main.tsx +++ b/packages/atlas/src/embedded/main.tsx @@ -9,7 +9,7 @@ import { App } from './App' const initApp = async () => { if (BUILD_ENV === 'production') { SentryLogger.initialize(atlasConfig.analytics.sentry?.dsn) - UserEventsLogger.initialize() + UserEventsLogger.initialize(atlasConfig.analytics.assetLogs?.url) } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion From d30d1e53c946e784735b87925f502ae4bac25b19 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 29 Aug 2023 18:33:32 +0200 Subject: [PATCH 3/4] CR fixes --- .../src/components/AssetImage/AssetImage.tsx | 6 ++-- .../src/components/AssetVideo/AssetVideo.tsx | 2 +- .../atlas/src/components/Avatar/Avatar.tsx | 1 + .../components/Searchbar/SearchBox/Result.tsx | 7 ++-- .../_channel/ChannelCover/ChannelCover.tsx | 1 + .../components/_inputs/ComboBox/ComboBox.tsx | 3 +- .../_inputs/SubtitlesBox/SubtitlesBox.tsx | 2 +- .../BackgroundVideoPlayer.tsx | 2 +- .../VideoOverlays/EndingOverlay.tsx | 2 +- .../_video/VideoPlayer/videoJsPlayer.ts | 2 +- .../_video/VideoThumbnail/VideoThumbnail.tsx | 1 + packages/atlas/src/hooks/useGetAssetUrl.ts | 32 ++++++------------- .../src/providers/assets/assets.helpers.ts | 8 +++-- .../src/providers/assets/assets.provider.tsx | 2 +- .../src/providers/videoWorkspace/hooks.ts | 2 +- packages/atlas/src/utils/logs/asset.ts | 2 +- .../views/viewer/ChannelView/ChannelView.tsx | 2 +- .../views/viewer/MemberView/ActivityItem.tsx | 2 +- .../src/views/viewer/VideoView/VideoView.tsx | 2 +- 19 files changed, 39 insertions(+), 42 deletions(-) diff --git a/packages/atlas/src/components/AssetImage/AssetImage.tsx b/packages/atlas/src/components/AssetImage/AssetImage.tsx index d63140cf5d..9ba7d415cc 100644 --- a/packages/atlas/src/components/AssetImage/AssetImage.tsx +++ b/packages/atlas/src/components/AssetImage/AssetImage.tsx @@ -3,16 +3,18 @@ import { CSSTransition, SwitchTransition } from 'react-transition-group' import { SkeletonLoader } from '@/components/_loaders/SkeletonLoader' import { useGetAssetUrl } from '@/hooks/useGetAssetUrl' +import { AssetType } from '@/providers/uploads/uploads.types' import { cVar, transitions } from '@/styles' export type AssetImageProps = { isLoading?: boolean resolvedUrls: string[] | undefined | null + type: AssetType | null imagePlaceholder?: ReactNode } & Omit, 'src'> -export const AssetImage: FC = ({ resolvedUrls, isLoading, imagePlaceholder, ...imgProps }) => { - const { url, isLoading: isResolving } = useGetAssetUrl(resolvedUrls, 'image') +export const AssetImage: FC = ({ resolvedUrls, isLoading, type, imagePlaceholder, ...imgProps }) => { + const { url, isLoading: isResolving } = useGetAssetUrl(resolvedUrls, type) const loading = isLoading || isResolving diff --git a/packages/atlas/src/components/AssetVideo/AssetVideo.tsx b/packages/atlas/src/components/AssetVideo/AssetVideo.tsx index 911c1026a5..c11351b796 100644 --- a/packages/atlas/src/components/AssetVideo/AssetVideo.tsx +++ b/packages/atlas/src/components/AssetVideo/AssetVideo.tsx @@ -10,7 +10,7 @@ export type AssetVideoProps = { export const AssetVideo = forwardRef( ({ resolvedVideoUrls, resolvedPosterUrls, ...props }: AssetVideoProps, ref) => { const { url: videoSrc } = useGetAssetUrl(resolvedVideoUrls, 'video') - const { url: posterSrc } = useGetAssetUrl(resolvedPosterUrls, 'image') + const { url: posterSrc } = useGetAssetUrl(resolvedPosterUrls, 'cover') return