From 3bf34cc65226624f5e409b2e850ae348f611dac4 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Tue, 3 Jun 2025 15:18:17 +0200 Subject: [PATCH] missing stuff from saas specific code --- frontend/app/IFrameRoutes.tsx | 2 + frontend/app/Router.tsx | 2 + .../app/components/Client/Modules/Modules.tsx | 2 +- .../app/components/Client/Modules/extra.ts | 15 ++ .../app/components/Client/Modules/index.ts | 23 +-- frontend/app/dateRange.js | 32 +++- frontend/app/extraRoutes.ts | 2 + frontend/app/mstore/billingStore.ts | 2 + frontend/app/mstore/clipStore.ts | 2 + frontend/app/mstore/index.tsx | 6 + frontend/app/mstore/userStore.ts | 3 +- frontend/app/player/player/Player.ts | 2 +- frontend/app/routes.ts | 5 + frontend/app/services/ConfigService.ts | 6 + frontend/app/services/SessionService.ts | 80 +++++++++- frontend/app/svg/icons/arrow-pointer.svg | 3 + frontend/app/svg/icons/clips-icon.svg | 54 +++++++ frontend/app/svg/icons/hand-thumbs-down.svg | 3 + frontend/app/svg/icons/hand-thumbs-up.svg | 3 + frontend/app/svg/icons/tv-minimal-play.svg | 1 + frontend/app/svg/icons/user-issues.svg | 6 + frontend/app/utils/index.ts | 147 ++++++++++++++++++ 22 files changed, 374 insertions(+), 27 deletions(-) create mode 100644 frontend/app/components/Client/Modules/extra.ts create mode 100644 frontend/app/extraRoutes.ts create mode 100644 frontend/app/mstore/billingStore.ts create mode 100644 frontend/app/mstore/clipStore.ts create mode 100644 frontend/app/svg/icons/arrow-pointer.svg create mode 100644 frontend/app/svg/icons/clips-icon.svg create mode 100644 frontend/app/svg/icons/hand-thumbs-down.svg create mode 100644 frontend/app/svg/icons/hand-thumbs-up.svg create mode 100644 frontend/app/svg/icons/tv-minimal-play.svg create mode 100644 frontend/app/svg/icons/user-issues.svg diff --git a/frontend/app/IFrameRoutes.tsx b/frontend/app/IFrameRoutes.tsx index 434a6af56..b0a7d442f 100644 --- a/frontend/app/IFrameRoutes.tsx +++ b/frontend/app/IFrameRoutes.tsx @@ -10,6 +10,7 @@ import PublicRoutes from 'App/PublicRoutes'; import { useStore } from 'App/mstore'; import { observer } from 'mobx-react-lite'; import * as routes from './routes'; +import Tracker from 'App/Tracker'; const components: any = { SessionPure: lazy(() => import('Components/Session/Session')), @@ -43,6 +44,7 @@ function IFrameRoutes(props: Props) { + }> = (props) => { + diff --git a/frontend/app/components/Client/Modules/Modules.tsx b/frontend/app/components/Client/Modules/Modules.tsx index b31912865..0a457c8ab 100644 --- a/frontend/app/components/Client/Modules/Modules.tsx +++ b/frontend/app/components/Client/Modules/Modules.tsx @@ -13,7 +13,7 @@ function Modules() { const { userStore } = useStore(); const { updateModule } = userStore; const modules = userStore.account.settings?.modules ?? []; - const isEnterprise = userStore.account.edition === 'ee'; + const isEnterprise = userStore.isEnterprise; const [modulesState, setModulesState] = React.useState([]); const onToggle = async (module: any) => { diff --git a/frontend/app/components/Client/Modules/extra.ts b/frontend/app/components/Client/Modules/extra.ts new file mode 100644 index 000000000..9dae85e76 --- /dev/null +++ b/frontend/app/components/Client/Modules/extra.ts @@ -0,0 +1,15 @@ +const extra = (t: any) => [] + +export const enum MODULES { + ASSIST = 'assist', + HIGHLIGHTS = 'notes', + BUG_REPORTS = 'bug-reports', + OFFLINE_RECORDINGS = 'offline-recordings', + ALERTS = 'alerts', + ASSIST_STATS = 'assist-stats', + FEATURE_FLAGS = 'feature-flags', + RECOMMENDATIONS = 'recommendations', + USABILITY_TESTS = 'usability-tests', +} + +export default extra; diff --git a/frontend/app/components/Client/Modules/index.ts b/frontend/app/components/Client/Modules/index.ts index 85425d5f2..5a88748f2 100644 --- a/frontend/app/components/Client/Modules/index.ts +++ b/frontend/app/components/Client/Modules/index.ts @@ -1,18 +1,9 @@ import { TFunction } from 'i18next'; +import extraModules, { MODULES } from './extra'; +export * from './extra'; export { default } from './Modules'; -export const enum MODULES { - ASSIST = 'assist', - HIGHLIGHTS = 'notes', - BUG_REPORTS = 'bug-reports', - OFFLINE_RECORDINGS = 'offline-recordings', - ALERTS = 'alerts', - ASSIST_STATS = 'assist-stats', - FEATURE_FLAGS = 'feature-flags', - RECOMMENDATIONS = 'recommendations', - USABILITY_TESTS = 'usability-tests', -} export interface Module { label: string; @@ -73,15 +64,6 @@ export const modules = (t: TFunction) => [ key: MODULES.FEATURE_FLAGS, icon: 'toggles', }, - { - label: t('Recommendations'), - description: t( - 'Get personalized recommendations for sessions to watch, based on your replay history and search preferences.', - ), - key: MODULES.RECOMMENDATIONS, - icon: 'magic', - hidden: true, - }, { label: t('Usability Tests'), description: t( @@ -90,4 +72,5 @@ export const modules = (t: TFunction) => [ key: MODULES.USABILITY_TESTS, icon: 'clipboard-check', }, + ...extraModules(t), ]; diff --git a/frontend/app/dateRange.js b/frontend/app/dateRange.js index 7c0104690..ee8318c20 100644 --- a/frontend/app/dateRange.js +++ b/frontend/app/dateRange.js @@ -17,6 +17,12 @@ const DATE_RANGE_LABELS = { [CUSTOM_RANGE]: 'Custom Range', }; +const LONG_RANGE_LABELS = { + LAST_3_MONTHS: 'Last 3 Months', + LAST_YEAR: 'Last Year', + CUSTOM_RANGE: 'Custom Range', +} + const COMPARISON_DATE_RANGE_LABELS = { PREV_24_HOURS: 'Previous Day', PREV_7_DAYS: 'Previous Week', @@ -26,18 +32,28 @@ const COMPARISON_DATE_RANGE_LABELS = { }; const DATE_RANGE_VALUES = {}; +const LONG_RANGE_VALUES = {}; Object.keys(DATE_RANGE_LABELS).forEach((key) => { DATE_RANGE_VALUES[key] = key; }); +Object.keys(LONG_RANGE_LABELS).forEach((key) => { + LONG_RANGE_VALUES[key] = key; +}); -export { DATE_RANGE_VALUES }; +export { DATE_RANGE_VALUES, LONG_RANGE_LABELS }; export const dateRangeValues = Object.keys(DATE_RANGE_VALUES); -export const DATE_RANGE_OPTIONS = +export const DATE_RANGE_OPTIONS = Object.keys(DATE_RANGE_LABELS).map((key) => ({ label: DATE_RANGE_LABELS[key], value: key, })); +export const LONG_DATE_RANGE_OPTIONS = + Object.keys(LONG_RANGE_LABELS).map((key) => ({ + label: LONG_RANGE_LABELS[key], + value: key, + })); + export const DATE_RANGE_COMPARISON_OPTIONS = Object.keys(COMPARISON_DATE_RANGE_LABELS).map((key) => ({ label: COMPARISON_DATE_RANGE_LABELS[key], @@ -45,9 +61,11 @@ export const DATE_RANGE_COMPARISON_OPTIONS = })); export function getDateRangeLabel(value, t) { - return t(DATE_RANGE_LABELS[value]); + const string = DATE_RANGE_LABELS[value] ?? LONG_RANGE_LABELS[value]; + return t(string); } + export function getDateRangeFromValue(value) { const tz = JSON.parse(localStorage.getItem(TIMEZONE)); const offset = tz ? tz.value : undefined; @@ -106,6 +124,14 @@ export function getDateRangeFromValue(value) { // return Interval.fromDateTimes(now.startOf('year'), now.endOf('year')); // case DATE_RANGE_VALUES.CUSTOM_RANGE: // return Interval.fromDateTimes(now, now); + case LONG_RANGE_VALUES.LAST_YEAR: + const lastYear = now.minus({ years: 1 }); + return Interval.fromDateTimes(lastYear.startOf('year'), lastYear.endOf('year')); + case LONG_RANGE_VALUES.LAST_3_MONTHS: + return Interval.fromDateTimes( + now.minus({ months: 3 }).startOf('month'), + now.endOf('month'), + ); default: throw new Error('Invalid date range value'); } diff --git a/frontend/app/extraRoutes.ts b/frontend/app/extraRoutes.ts new file mode 100644 index 000000000..d381719f6 --- /dev/null +++ b/frontend/app/extraRoutes.ts @@ -0,0 +1,2 @@ +export const routeIdRequired = []; +export const changeAvailable = []; diff --git a/frontend/app/mstore/billingStore.ts b/frontend/app/mstore/billingStore.ts new file mode 100644 index 000000000..0586b152d --- /dev/null +++ b/frontend/app/mstore/billingStore.ts @@ -0,0 +1,2 @@ +export default class BillingStore {} +// SAAS only diff --git a/frontend/app/mstore/clipStore.ts b/frontend/app/mstore/clipStore.ts new file mode 100644 index 000000000..33ea78b19 --- /dev/null +++ b/frontend/app/mstore/clipStore.ts @@ -0,0 +1,2 @@ +export default class ClipStore {} +// SAAS only diff --git a/frontend/app/mstore/index.tsx b/frontend/app/mstore/index.tsx index ea9bd5631..2771ec220 100644 --- a/frontend/app/mstore/index.tsx +++ b/frontend/app/mstore/index.tsx @@ -34,6 +34,8 @@ import userStore from './userStore'; import UxtestingStore from './uxtestingStore'; import WeeklyReportStore from './weeklyReportConfigStore'; import logger from '@/logger'; +import BillingStore from "@/mstore/billingStore"; +import ClipStore from "@/mstore/clipStore"; const projectStore = new ProjectsStore(); const sessionStore = new SessionStore(); @@ -110,6 +112,8 @@ export class RootStore { searchStoreLive: SearchStoreLive; integrationsStore: IntegrationsStore; projectsStore: ProjectsStore; + billingStore: BillingStore; + clipStore: ClipStore; constructor() { this.dashboardStore = new DashboardStore(); @@ -142,6 +146,8 @@ export class RootStore { this.searchStore = searchStore; this.searchStoreLive = searchStoreLive; this.integrationsStore = new IntegrationsStore(); + this.billingStore = new BillingStore(); + this.clipStore = new ClipStore(); } initClient() { diff --git a/frontend/app/mstore/userStore.ts b/frontend/app/mstore/userStore.ts index ccbfb9e65..8e2ba796e 100644 --- a/frontend/app/mstore/userStore.ts +++ b/frontend/app/mstore/userStore.ts @@ -94,7 +94,8 @@ class UserStore { get isEnterprise() { return ( this.account?.edition === 'ee' || - this.authStore.authDetails?.edition === 'ee' + this.authStore.authDetails?.edition === 'ee' || + this.account?.plan?.type === 'enterprise' ); } diff --git a/frontend/app/player/player/Player.ts b/frontend/app/player/player/Player.ts index fa9a6368f..39dcb520b 100644 --- a/frontend/app/player/player/Player.ts +++ b/frontend/app/player/player/Player.ts @@ -118,7 +118,7 @@ export default class Player extends Animator { } // toggle range (start, end) - toggleRange(start: number, end: number) { + toggleRange = (start: number, end: number) => { this.pState.update({ range: [start, end] }); } diff --git a/frontend/app/routes.ts b/frontend/app/routes.ts index ad29a496f..6aca05cd2 100644 --- a/frontend/app/routes.ts +++ b/frontend/app/routes.ts @@ -1,4 +1,7 @@ import { CLIENT_TABS } from './utils/routeUtils' +import { routeIdRequired, changeAvailable } from './extraRoutes'; + +export * from './extraRoutes'; const hashed = (path: string, hash?: string | number): string => { if ((typeof hash === 'string' && hash !== '') || typeof hash === 'number') { @@ -191,6 +194,7 @@ export const highlights = (): string => '/highlights'; export const kai = (): string => '/kai'; const REQUIRED_SITE_ID_ROUTES = [ + ...routeIdRequired, liveSession(''), session(''), sessions(), @@ -263,6 +267,7 @@ export function isRoute(route: string, path: string): boolean { } const SITE_CHANGE_AVAILABLE_ROUTES = [ + ...changeAvailable, sessions(), notes(), bookmarks(), diff --git a/frontend/app/services/ConfigService.ts b/frontend/app/services/ConfigService.ts index 9edb6f68c..52e248f47 100644 --- a/frontend/app/services/ConfigService.ts +++ b/frontend/app/services/ConfigService.ts @@ -25,4 +25,10 @@ export default class ConfigService extends BaseService { .then((r) => r.json()) .then((j) => j.data); } + + async checkElasticConnection(params: any): Promise { + return this.client.post('/integartions/elasticsearch/test', params) + .then((r) => r.json()) + .then((j) => j.data) + } } diff --git a/frontend/app/services/SessionService.ts b/frontend/app/services/SessionService.ts index ef7d43243..48d9e91e3 100644 --- a/frontend/app/services/SessionService.ts +++ b/frontend/app/services/SessionService.ts @@ -45,7 +45,9 @@ export default class SettingsService { .catch((e) => Promise.reject(e)); } - getFirstMobUrl(sessionId: string): Promise<{ domURL: string[], fileKey?: string }> { + getFirstMobUrl( + sessionId: string, + ): Promise<{ domURL: string[]; fileKey?: string }> { return this.client .get(`/sessions/${sessionId}/first-mob`) .then((r) => r.json()) @@ -53,6 +55,44 @@ export default class SettingsService { .catch(console.error); } + getRecommendedSessions(sort?: any): Promise<{ + sessions: ISession[]; + total: number; + }> { + return this.client + .post('/sessions-recommendations', sort) + .then((r) => r.json()) + .then((response) => response || []) + .catch((e) => Promise.reject(e)); + } + + getFinetuneSessions(): Promise<{ sessions: string[] }> { + return this.client + .get('/PROJECT_ID/finetuning/sessions') + .then((r) => r.json()) + .catch(Promise.reject); + } + + sendFeedback(data: any): Promise { + return this.client + .post(`/session-feedback`, data) + .then((r) => r.json()) + .then((j) => j.data || []) + .catch(Promise.reject); + } + + signalFinetune() { + return this.client.get('/PROJECT_ID/finetune'); + } + + checkFeedback(sessionId: string): Promise { + return this.client + .get(`/session-feedback/${sessionId}`) + .then((r) => r.json()) + .then((j) => j.data || false) + .catch(Promise.reject); + } + getSessionInfo(sessionId: string, isLive?: boolean): Promise { return this.client .get( @@ -129,6 +169,22 @@ export default class SettingsService { .catch(Promise.reject); } + async fetchSimilarSessions( + sessionId: string, + params: any, + ): Promise<{ sessions: ISession[] }> { + try { + const r = await this.client.post( + `/PROJECT_ID/similar-sessions/${sessionId}`, + params, + ); + const j = await r.json(); + return j.sessions || []; + } catch (reason) { + return Promise.reject(reason); + } + } + async getAssistCredentials(): Promise { try { const r = await this.client.get('/config/assist/credentials'); @@ -138,4 +194,26 @@ export default class SettingsService { return Promise.reject(reason); } } + + generateShorts(projectId: string) { + try { + void this.client.post(`/${projectId}/generate/shorts`, {}); + } catch (reason) { + console.error('Error generating shorts:', reason); + } + } + + async fetchSessionClips(): Promise<{ clips: any[] }> { + try { + const r = await this.client.get('/PROJECT_ID/shorts-recommendations', { + sortBy: 'startTs', + sortOrder: 'desc', + }); + // .get('/PROJECT_ID/shorts-recommendations'); + const j = await r.json(); + return j || {}; + } catch (reason) { + return Promise.reject(reason); + } + } } diff --git a/frontend/app/svg/icons/arrow-pointer.svg b/frontend/app/svg/icons/arrow-pointer.svg new file mode 100644 index 000000000..c6b1b8988 --- /dev/null +++ b/frontend/app/svg/icons/arrow-pointer.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app/svg/icons/clips-icon.svg b/frontend/app/svg/icons/clips-icon.svg new file mode 100644 index 000000000..42a127300 --- /dev/null +++ b/frontend/app/svg/icons/clips-icon.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app/svg/icons/hand-thumbs-down.svg b/frontend/app/svg/icons/hand-thumbs-down.svg new file mode 100644 index 000000000..fad6ff75a --- /dev/null +++ b/frontend/app/svg/icons/hand-thumbs-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app/svg/icons/hand-thumbs-up.svg b/frontend/app/svg/icons/hand-thumbs-up.svg new file mode 100644 index 000000000..0ade6bcbd --- /dev/null +++ b/frontend/app/svg/icons/hand-thumbs-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/app/svg/icons/tv-minimal-play.svg b/frontend/app/svg/icons/tv-minimal-play.svg new file mode 100644 index 000000000..9f97451bf --- /dev/null +++ b/frontend/app/svg/icons/tv-minimal-play.svg @@ -0,0 +1 @@ + diff --git a/frontend/app/svg/icons/user-issues.svg b/frontend/app/svg/icons/user-issues.svg new file mode 100644 index 000000000..cc4c83e5b --- /dev/null +++ b/frontend/app/svg/icons/user-issues.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/app/utils/index.ts b/frontend/app/utils/index.ts index e74a56f87..f5d0f21c9 100644 --- a/frontend/app/utils/index.ts +++ b/frontend/app/utils/index.ts @@ -38,6 +38,7 @@ export function debounceCall(func, wait) { }; } + export function randomInt(a, b) { const min = (b ? a : 0) - 0.5; const max = b || a || Number.MAX_SAFE_INTEGER; @@ -623,6 +624,152 @@ export function exportAntCsv(tableColumns, tableData, filename = 'table.csv') { saveAsFile(blob, filename); } +export const numFormatter = (num: any) => { + if (num > 999 && num < 1000000) { + return num / 1000 + 'K'; + } else if (num >= 1000000) { + return num / 1000000 + 'M'; + } else if (num < 900) { + return num; + } +}; + +export const loadStripe = (callback: any) => { + loadDynamicScript('stripeTag', 'https://js.stripe.com/v3/', callback); +}; + +export const loadDynamicScript = (tag: string, path: string, callback: any) => { + const scriptExists = document.getElementById(tag) + if (scriptExists) return callback(); + + const scriptJs = document.createElement('script'); + scriptJs.src = path; + scriptJs.async = true; + scriptJs.id = tag; + scriptJs.onload = () => { + setTimeout(callback, 10); + }; + document.body && document.body.appendChild(scriptJs); +} + +export const getDeviceType = () => { + const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + const isTablet = /iPad|Android/i.test(navigator.userAgent) && !/Mobile/i.test(navigator.userAgent); + + if (isMobile) { + return 2; + } else if (isTablet) { + return 0; + } else { + return 1; + } +} + +export const sensitiveParams = new Set([ + 'password', + 'pass', + 'pwd', + 'mdp', + 'token', + 'bearer', + 'jwt', + 'api_key', + 'api-key', + 'apiKey', + 'key', + 'secret', + 'id', + 'user', + 'userId', + 'email', + 'ssn', + 'name', + 'firstname', + 'lastname', + 'birthdate', + 'dob', + 'address', + 'zip', + 'zipcode', + 'x-api-key', + 'www-authenticate', + 'x-csrf-token', + 'x-requested-with', + 'x-forwarded-for', + 'x-real-ip', + 'cookie', + 'authorization', + 'auth', + 'proxy-authorization', + 'set-cookie', + 'account_key', +]) + +export function filterHeaders(headers: Record | { name: string; value: string }[]) { + const filteredHeaders: Record = {} + if (Array.isArray(headers)) { + headers.forEach(({ name, value }) => { + if (sensitiveParams.has(name.toLowerCase())) { + filteredHeaders[name] = '******' + } else { + filteredHeaders[name] = value + } + }) + } else { + for (const [key, value] of Object.entries(headers)) { + if (sensitiveParams.has(key.toLowerCase())) { + filteredHeaders[key] = '******' + } else { + filteredHeaders[key] = value + } + } + } + return filteredHeaders +} + +export function filterBody>(body: T): T { + if (!body) { + return body + } + + obscureSensitiveData(body) + return body +} + +export function obscureSensitiveData(obj: Record | any[]) { + if (Array.isArray(obj)) { + obj.forEach(obscureSensitiveData) + } else if (obj && typeof obj === 'object') { + for (const key in obj) { + if (Object.hasOwn(obj, key)) { + if (sensitiveParams.has(key.toLowerCase())) { + obj[key] = '******' + } else if (obj[key] !== null && typeof obj[key] === 'object') { + obscureSensitiveData(obj[key]) + } + } + } + } +} + +export function tryFilterUrl(url: string) { + if (!url) return '' + try { + const urlObj = new URL(url) + if (urlObj.searchParams) { + for (const key of urlObj.searchParams.keys()) { + if (sensitiveParams.has(key.toLowerCase())) { + urlObj.searchParams.set(key, '******') + } + } + } + return urlObj.toString() + } catch (e) { + return url + } +} + + export function roundToNextMinutes(timestamp: number, minutes: number): number { const date = new Date(timestamp); date.setSeconds(0, 0);