diff --git a/.gitignore b/.gitignore index 13b7a9780..f28bd8b44 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,4 @@ DESIGN.md DESIGN.json next-env.d.ts +**/tsconfig.tsbuildinfo diff --git a/backend/apps/cloud/src/analytics/analytics.controller.ts b/backend/apps/cloud/src/analytics/analytics.controller.ts index e7741ba81..ea22a6824 100644 --- a/backend/apps/cloud/src/analytics/analytics.controller.ts +++ b/backend/apps/cloud/src/analytics/analytics.controller.ts @@ -43,16 +43,16 @@ import { DataType, getLowestPossibleTimeBucket, } from './analytics.service' +import { normalizeFiltersToV1Json } from './v2/query/filters.translator' import { VALID_PERIODS } from './decorators/validate-period.decorator' import { CurrentUserId } from '../auth/decorators/current-user-id.decorator' import { DEFAULT_TIMEZONE } from '../user/entities/user.entity' import { AuthenticationGuard } from '../auth/guards/authentication.guard' import { PageviewsDto } from './dto/pageviews.dto' import { EventsDto } from './dto/events.dto' -import { GetDataDto, ChartRenderMode, TimeBucketType } from './dto/getData.dto' +import { GetDataDto, ChartRenderMode } from './dto/getData.dto' import { GetCustomEventMetadata } from './dto/get-custom-event-meta.dto' import { GetPagePropertyMetaDto } from './dto/get-page-property-meta.dto' -import { GetUserFlowDto } from './dto/getUserFlow.dto' import { GetJourneysDto } from './dto/get-journeys.dto' import { GetJourneySessionsDto } from './dto/get-journey-sessions.dto' import { GetFunnelsDto } from './dto/getFunnels.dto' @@ -76,13 +76,7 @@ import { DataDeletionPreviewDto, } from './dto/data-deletion.dto' import { GetVersionFiltersDto } from './dto/get-version-filters.dto' -import { - IFunnel, - IGetFunnel, - IPageProperty, - IUserFlow, - PerfMeasure, -} from './interfaces' +import { IFunnel, IGetFunnel, IPageProperty, PerfMeasure } from './interfaces' import { GetSessionsDto } from './dto/get-sessions.dto' import { GetSessionDto } from './dto/get-session.dto' import { @@ -115,7 +109,7 @@ import { LiveVisitorsDto } from './dto/live-visitors.dto' import { GetHeartbeatStatsDto } from './dto/get-heartbeat-stats' import { GetKeywordsDto } from './dto/get-keywords.dto' import { GetBotStatsDto } from './dto/get-bot-stats.dto' -import { GSCService } from '../project/gsc.service' +import { GSC_ALL_TIME_DAYS, GSCService } from '../project/gsc.service' import { GetProfileIdDto, GetSessionIdDto } from './dto/get-id.dto' import { ExperimentService } from '../experiment/experiment.service' import { @@ -135,9 +129,6 @@ const BOT_RESPONSE = { message: 'Bot traffic detected, request is ignored' } const ONLINE_VISITORS_WINDOW_MINUTES = 5 // minutes -// Maximum range for "all time" GSC queries (~16 months, GSC's data retention) -const GSC_ALL_TIME_DAYS = 480 - // Performance object validator: none of the values cannot be bigger than 1000 * 60 * 5 (5 minutes) and are >= 0 const MAX_PERFORMANCE_VALUE = 1000 * 60 * 5 const isPerformanceValid = (perf: any) => { @@ -1022,59 +1013,6 @@ export class AnalyticsController { return this.getData(data, uid, {}, true) } - @Get('user-flow') - @Auth(true, true) - async getUserFlow( - @Query() data: GetUserFlowDto, - @CurrentUserId() uid: string, - @Headers() headers: { 'x-password'?: string }, - ): Promise { - const { pid, period, from, to, timezone = DEFAULT_TIMEZONE, filters } = data - - await this.analyticsService.checkProjectAccess( - pid, - uid, - headers['x-password'], - ) - - await this.analyticsService.checkBillingAccess(pid) - - this.logger.log( - `pid: ${pid}, period: ${period}`, - 'GET /analytics/user-flow', - ) - - let diff - - if (period === 'all') { - const res = await this.analyticsService.calculateTimeBucketForAllTime( - pid, - 'pageview', - ) - - diff = res.diff - } - - const safeTimezone = this.analyticsService.getSafeTimezone(timezone) - const { groupFrom, groupTo } = this.analyticsService.getGroupFromTo( - from, - to, - null, - period, - safeTimezone, - diff, - ) - - const [filtersQuery, filtersParams, appliedFilters] = - this.analyticsService.getFiltersQuery(filters, DataType.ANALYTICS, true) - - const params = { pid, groupFrom, groupTo, ...filtersParams } - - const flow = await this.analyticsService.getUserFlow(params, filtersQuery) - - return { ...flow, appliedFilters } - } - @Get('journeys') @Auth(true, true) async getJourneys( @@ -1122,7 +1060,11 @@ export class AnalyticsController { ) const [filtersQuery, filtersParams, appliedFilters] = - this.analyticsService.getFiltersQuery(filters, DataType.ANALYTICS, true) + this.analyticsService.getFiltersQuery( + normalizeFiltersToV1Json(filters, 'traffic'), + DataType.ANALYTICS, + true, + ) const params = { pid, groupFrom, groupTo, ...filtersParams } @@ -1198,7 +1140,7 @@ export class AnalyticsController { ) const [filtersQuery, filtersParams] = this.analyticsService.getFiltersQuery( - filters || '[]', + normalizeFiltersToV1Json(filters, 'traffic'), DataType.ANALYTICS, ) const params = { pid, groupFrom, groupTo, ...filtersParams } @@ -1251,139 +1193,6 @@ export class AnalyticsController { return { keywords } } - @Get('gsc-dashboard') - @Auth(true, true) - async getGSCDashboard( - @Query() data: GetKeywordsDto, - @CurrentUserId() uid: string, - @Headers() headers: { 'x-password'?: string }, - ) { - const { - pid, - period, - from, - to, - timezone = DEFAULT_TIMEZONE, - timeBucket, - filters, - } = data - - await this.analyticsService.checkProjectAccess( - pid, - uid, - headers['x-password'], - ) - - await this.analyticsService.checkBillingAccess(pid) - - const defaultTimeBucket = ['1h', 'today', 'yesterday', '1d'].includes( - period, - ) - ? TimeBucketType.HOUR - : TimeBucketType.DAY - const finalTimeBucket = (timeBucket || defaultTimeBucket) as TimeBucketType - let groupTimeBucket: TimeBucketType | null = finalTimeBucket - if (period === 'all' || period === 'custom') { - groupTimeBucket = null - } - - this.logger.log( - `pid: ${pid}, period: ${period}, timeBucket: ${finalTimeBucket}, filters: ${filters}`, - 'GET /analytics/gsc-dashboard', - ) - - const diff = period === 'all' ? GSC_ALL_TIME_DAYS : undefined - - const safeTimezone = this.analyticsService.getSafeTimezone(timezone) - const { groupFrom, groupTo } = this.analyticsService.getGroupFromTo( - from, - to, - groupTimeBucket, - period, - safeTimezone, - diff, - ) - - return this.gscService.getDashboard( - pid, - groupFrom, - groupTo, - finalTimeBucket, - filters, - ) - } - - @Get('gsc-details') - @Auth(true, true) - async getGSCDetails( - @Query() data: GetKeywordsDto, - @CurrentUserId() uid: string, - @Headers() headers: { 'x-password'?: string }, - ) { - const { - pid, - period, - from, - to, - timezone = DEFAULT_TIMEZONE, - page, - query, - } = data - - this.logger.log( - `pid: ${pid}, period: ${period}, page: ${page}, query: ${query}`, - 'GET /analytics/gsc-details', - ) - - await this.analyticsService.checkProjectAccess( - pid, - uid, - headers['x-password'], - ) - - await this.analyticsService.checkBillingAccess(pid) - - const diff = period === 'all' ? GSC_ALL_TIME_DAYS : undefined - - const safeTimezone = this.analyticsService.getSafeTimezone(timezone) - const { groupFrom, groupTo } = this.analyticsService.getGroupFromTo( - from, - to, - null, - period, - safeTimezone, - diff, - ) - - if (page) { - const keywords = await this.gscService.getKeywords( - pid, - groupFrom, - groupTo, - 50, - 0, - undefined, - page, - ) - return { type: 'queries', data: keywords } - } - - if (query) { - const pages = await this.gscService.getTopPages( - pid, - groupFrom, - groupTo, - 50, - 0, - undefined, - query, - ) - return { type: 'pages', data: pages } - } - - return { type: 'none', data: [] } - } - @Get('birdseye') @Auth(true, true) // returns overall short statistics per project @@ -2578,7 +2387,10 @@ export class AnalyticsController { ) const [filtersQuery, filtersParams, appliedFilters, customEVFilterApplied] = - this.analyticsService.getFiltersQuery(filters, DataType.ANALYTICS) + this.analyticsService.getFiltersQuery( + normalizeFiltersToV1Json(filters, 'traffic'), + DataType.ANALYTICS, + ) let timeBucket let diff @@ -3044,7 +2856,7 @@ export class AnalyticsController { return this.analyticsService.getDataDeletionPreview( body.pid, - body.filters || '[]', + normalizeFiltersToV1Json(body.filters, 'traffic'), body.from || null, body.to || null, ) @@ -3068,7 +2880,7 @@ export class AnalyticsController { await this.analyticsService.deleteData( body.pid, - body.filters || '[]', + normalizeFiltersToV1Json(body.filters, 'traffic'), body.from || null, body.to || null, body.types, diff --git a/backend/apps/cloud/src/analytics/analytics.module.ts b/backend/apps/cloud/src/analytics/analytics.module.ts index ec3de0c61..f0f0a817b 100644 --- a/backend/apps/cloud/src/analytics/analytics.module.ts +++ b/backend/apps/cloud/src/analytics/analytics.module.ts @@ -21,6 +21,16 @@ import { SESSION_REPLAY_EXPORT_QUEUE, } from './session-replay-export.service' import { SessionReplayExportProcessor } from './session-replay-export.processor' +import { AnalyticsV2Service } from './v2/analytics-v2.service' +import { SeoV2Service } from './v2/seo-v2.service' +import { TrafficV2Controller } from './v2/controllers/traffic-v2.controller' +import { PerformanceV2Controller } from './v2/controllers/performance-v2.controller' +import { CaptchaV2Controller } from './v2/controllers/captcha-v2.controller' +import { ErrorsV2Controller } from './v2/controllers/errors-v2.controller' +import { SessionsV2Controller } from './v2/controllers/sessions-v2.controller' +import { ProfilesV2Controller } from './v2/controllers/profiles-v2.controller' +import { ProjectV2Controller } from './v2/controllers/project-v2.controller' +import { SeoV2Controller } from './v2/controllers/seo-v2.controller' @Module({ imports: [ @@ -34,6 +44,8 @@ import { SessionReplayExportProcessor } from './session-replay-export.processor' ], providers: [ AnalyticsService, + AnalyticsV2Service, + SeoV2Service, BotDetectionService, SaltService, SessionReplayS3Service, @@ -44,6 +56,16 @@ import { SessionReplayExportProcessor } from './session-replay-export.processor' PublicProjectCacheInterceptor, ], exports: [AnalyticsService, BotDetectionService, SaltService], - controllers: [AnalyticsController], + controllers: [ + AnalyticsController, + TrafficV2Controller, + PerformanceV2Controller, + CaptchaV2Controller, + ErrorsV2Controller, + SessionsV2Controller, + ProfilesV2Controller, + ProjectV2Controller, + SeoV2Controller, + ], }) export class AnalyticsModule {} diff --git a/backend/apps/cloud/src/analytics/analytics.service.ts b/backend/apps/cloud/src/analytics/analytics.service.ts index c2e881921..c70e4b190 100644 --- a/backend/apps/cloud/src/analytics/analytics.service.ts +++ b/backend/apps/cloud/src/analytics/analytics.service.ts @@ -94,12 +94,8 @@ import { TrafficCHResponse, IGetGroupFromTo, GetFiltersQuery, - IUserFlowNode, - IUserFlowLink, - IUserFlow, IJourney, IJourneys, - IBuildUserFlow, IExtractChartData, IGenerateXAxis, IAggregatedMetadata, @@ -146,6 +142,12 @@ const LIVE_SESSION_THRESHOLD_SECONDS = 120 const MAX_CONCURRENCY_WINDOW_MINUTES = 366 * 24 * 60 const MAX_FILTERS = 100 const MAX_FILTER_VALUES = 100 +// Journeys keep only the first {steps} distinct pages of a session, but the +// dedupe happens after aggregation, so the cap has to sit far above {steps}: +// a session that reloads one page N times still needs its later, different +// pages to survive. 1000 raw pageviews is orders of magnitude beyond any real +// session while keeping a bot's session array bounded. +const MAX_JOURNEY_PAGEVIEWS_PER_SESSION = 1000 // Event types that can be targeted by the data-deletion tool. const DELETABLE_EVENT_TYPES = [...DATA_DELETION_EVENT_TYPES] @@ -534,10 +536,13 @@ const mapErrorColumn = (type: string): string => ERROR_COLUMN_MAP[type] || type const captchaMetaValue = (key: string, fallback: string) => `if(indexOf(\`meta.key\`, '${key}') = 0, '${fallback}', arrayElement(\`meta.value\`, indexOf(\`meta.key\`, '${key}')))` -const CAPTCHA_EVENT_SQL = captchaMetaValue('captcha_event', 'pass') -const CAPTCHA_DIFFICULTY_SQL = captchaMetaValue('captcha_difficulty', '0') -const CAPTCHA_REASON_SQL = captchaMetaValue('captcha_reason', '') -const CAPTCHA_SOLVE_MS_SQL = `toUInt32OrZero(${captchaMetaValue('solve_ms', '0')})` +export const CAPTCHA_EVENT_SQL = captchaMetaValue('captcha_event', 'pass') +export const CAPTCHA_DIFFICULTY_SQL = captchaMetaValue( + 'captcha_difficulty', + '0', +) +export const CAPTCHA_REASON_SQL = captchaMetaValue('captcha_reason', '') +export const CAPTCHA_SOLVE_MS_SQL = `toUInt32OrZero(${captchaMetaValue('solve_ms', '0')})` const CAPTCHA_SOLVE_TIME_SQL = `multiIf(${CAPTCHA_SOLVE_MS_SQL} = 0, 'unknown', ${CAPTCHA_SOLVE_MS_SQL} < 1000, '<1s', ${CAPTCHA_SOLVE_MS_SQL} < 3000, '1-3s', ${CAPTCHA_SOLVE_MS_SQL} < 10000, '3-10s', ${CAPTCHA_SOLVE_MS_SQL} < 30000, '10-30s', '30s+')` const CAPTCHA_META_COLUMN_MAP: Record = { @@ -547,7 +552,7 @@ const CAPTCHA_META_COLUMN_MAP: Record = { solve_time: CAPTCHA_SOLVE_TIME_SQL, } -const getCaptchaColumnExpression = (col: string): string => +export const getCaptchaColumnExpression = (col: string): string => CAPTCHA_META_COLUMN_MAP[col] || col const generateParamsQuery = ( @@ -1141,100 +1146,6 @@ export class AnalyticsService { } } - removeCyclicDependencies(links: IUserFlowLink[]): IUserFlowLink[] { - const visited = new Set() - - return links.filter((link) => { - const key = `${link.source}_${link.target}` - if (visited.has(key)) { - return false - } - visited.add(key) - return true - }) - } - - buildUserFlow(links: IUserFlowLink[]): IBuildUserFlow { - const nodes: IUserFlowNode[] = Array.from( - new Set( - links - .map((link: IUserFlowLink) => link.source) - .concat(links.map((link: IUserFlowLink) => link.target)), - ), - ).map((node: any) => ({ id: node })) - - return { nodes, links } - } - - async getUserFlow( - params: Record, - filtersQuery: string, - ): Promise { - const query = ` - WITH page_sequences AS ( - SELECT - psid, - pg, - created, - lagInFrame(pg) OVER (PARTITION BY psid ORDER BY created) AS prev_page - FROM events - WHERE - pid = {pid:FixedString(12)} - AND type = 'pageview' - AND created BETWEEN {groupFrom:String} AND {groupTo:String} - AND pg IS NOT NULL - ${filtersQuery} - ) - SELECT - prev_page AS source, - pg AS target, - count() AS value - FROM page_sequences - WHERE prev_page IS NOT NULL - AND prev_page != pg - GROUP BY - source, - target - ORDER BY value DESC - ` - - const { data } = await clickhouse - .query({ - query, - query_params: params, - }) - .then((res) => res.json()) - - if (_isEmpty(data)) { - const empty = { nodes: [], links: [] } - return { - ascending: empty, - descending: empty, - } - } - - const ascendingLinks: IUserFlowLink[] = [] - const descendingLinks: IUserFlowLink[] = [] - - this.removeCyclicDependencies(data).forEach((row: any) => { - const link: IUserFlowLink = { - source: row.source, - target: row.target, - value: row.value, - } - if (link.source < link.target) { - ascendingLinks.push(link) - } else { - descendingLinks.push(link) - } - }) - - return { - ascending: this.buildUserFlow(ascendingLinks), - descending: this.buildUserFlow(descendingLinks), - } - } - async getJourneys( params: Record, filtersQuery: string, @@ -1262,7 +1173,12 @@ export class AnalyticsService { SELECT psid, arraySlice( - arrayCompact(arrayMap(x -> x.2, arraySort(groupArray((created, pg))))), + arrayCompact( + arrayMap( + x -> x.2, + groupArraySorted({maxPageviews:UInt32})((created, pg)) + ) + ), 1, {steps:UInt32} ) AS path @@ -1303,7 +1219,12 @@ export class AnalyticsService { const { data } = await clickhouse .query({ query, - query_params: { ...params, steps, journeys }, + query_params: { + ...params, + steps, + journeys, + maxPageviews: MAX_JOURNEY_PAGEVIEWS_PER_SESSION, + }, }) .then((res) => res.json<{ path: string[]; value: string; totalSessions: string }>(), diff --git a/backend/apps/cloud/src/analytics/dto/getUserFlow.dto.ts b/backend/apps/cloud/src/analytics/dto/getUserFlow.dto.ts deleted file mode 100644 index 832756966..000000000 --- a/backend/apps/cloud/src/analytics/dto/getUserFlow.dto.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsNotEmpty, IsOptional, Matches } from 'class-validator' -import { DEFAULT_TIMEZONE } from '../../user/entities/user.entity' -import { ValidatePeriod } from '../decorators/validate-period.decorator' -import { PID_REGEX } from '../../common/constants' - -export class GetUserFlowDto { - @ApiProperty({ - example: 'aUn1quEid-3', - required: true, - description: 'The project ID', - }) - @IsNotEmpty() - @Matches(PID_REGEX, { message: 'The provided Project ID (pid) is incorrect' }) - pid: string - - @ApiProperty({ required: false }) - @IsOptional() - @ValidatePeriod() - period?: string - - @ApiProperty() - from: string - - @ApiProperty() - to: string - - @ApiProperty({ - description: 'Timezone to display data in', - default: DEFAULT_TIMEZONE, - }) - timezone: string - - @ApiProperty({ - description: - 'A stringified array of properties to filter [{ column, filter, isExclusive, isContains }]', - }) - filters: string -} diff --git a/backend/apps/cloud/src/analytics/interfaces/index.ts b/backend/apps/cloud/src/analytics/interfaces/index.ts index 3d810e2f3..fd93e2aa5 100644 --- a/backend/apps/cloud/src/analytics/interfaces/index.ts +++ b/backend/apps/cloud/src/analytics/interfaces/index.ts @@ -78,31 +78,11 @@ export interface GetFiltersQuery extends Array { 3: boolean } -export interface IUserFlowNode { - id: string -} - -export interface IUserFlowLink { - source: string - target: string - value: number -} - export interface IGenerateXAxis { x: string[] xShifted: string[] } -export interface IBuildUserFlow { - nodes: IUserFlowNode[] - links: IUserFlowLink[] -} - -export interface IUserFlow { - ascending: IBuildUserFlow - descending: IBuildUserFlow -} - export interface IJourney { path: string[] value: number diff --git a/backend/apps/cloud/src/analytics/protection/analytics-read.guard.ts b/backend/apps/cloud/src/analytics/protection/analytics-read.guard.ts index dd760bc8b..14b1e1919 100644 --- a/backend/apps/cloud/src/analytics/protection/analytics-read.guard.ts +++ b/backend/apps/cloud/src/analytics/protection/analytics-read.guard.ts @@ -54,7 +54,7 @@ export class AnalyticsReadGuard implements CanActivate { let isPublicProject = false try { - const pid = getSinglePid(req.query || {}) + const pid = getSinglePid(req.query || {}, req.params || {}) if (pid) { const project = await this.projectService.getRedisProject(pid) isPublicProject = Boolean(project?.public) diff --git a/backend/apps/cloud/src/analytics/protection/analytics-read.util.ts b/backend/apps/cloud/src/analytics/protection/analytics-read.util.ts index 2b3fe7f0d..3c77aea97 100644 --- a/backend/apps/cloud/src/analytics/protection/analytics-read.util.ts +++ b/backend/apps/cloud/src/analytics/protection/analytics-read.util.ts @@ -1,5 +1,6 @@ import _isString from 'lodash/isString' +import { PID_REGEX } from '../../common/constants' import { getIPFromHeaders } from '../../common/utils' const TRUSTED_PROXY_IPS = new Set( @@ -36,9 +37,22 @@ export const getTrustworthyIp = (req: { return getIPFromHeaders(req.headers) || req.ip || '' } -export const getSinglePid = (query: Record): string | null => { +export const getSinglePid = ( + query: Record, + params?: Record, +): string | null => { + const paramPid = params?.pid + + if (_isString(paramPid) && PID_REGEX.test(paramPid)) { + return paramPid + } + const { pid, pids } = query + if (pid !== undefined && pids !== undefined) { + return null + } + if (_isString(pid) && pid) { return pid } diff --git a/backend/apps/cloud/src/analytics/protection/cacheable-analytics.decorator.ts b/backend/apps/cloud/src/analytics/protection/cacheable-analytics.decorator.ts new file mode 100644 index 000000000..8a69a7d25 --- /dev/null +++ b/backend/apps/cloud/src/analytics/protection/cacheable-analytics.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from '@nestjs/common' + +export const CACHEABLE_ANALYTICS_KEY = 'cacheable_analytics' + +export const CacheableAnalytics = () => + SetMetadata(CACHEABLE_ANALYTICS_KEY, true) diff --git a/backend/apps/cloud/src/analytics/protection/public-project-cache.interceptor.ts b/backend/apps/cloud/src/analytics/protection/public-project-cache.interceptor.ts index d15f9bd5a..f82b44cf0 100644 --- a/backend/apps/cloud/src/analytics/protection/public-project-cache.interceptor.ts +++ b/backend/apps/cloud/src/analytics/protection/public-project-cache.interceptor.ts @@ -4,6 +4,7 @@ import { Injectable, NestInterceptor, } from '@nestjs/common' +import { Reflector } from '@nestjs/core' import { Observable, of } from 'rxjs' import { tap } from 'rxjs/operators' @@ -11,6 +12,7 @@ import { ProjectService } from '../../project/project.service' import { AppLoggerService } from '../../logger/logger.service' import { redis } from '../../common/constants' import { getSinglePid, getAnalyticsRoute } from './analytics-read.util' +import { CACHEABLE_ANALYTICS_KEY } from './cacheable-analytics.decorator' const CACHE_TTL = Number(process.env.PUBLIC_PROJECT_CACHE_TTL) || 10 // seconds @@ -29,11 +31,8 @@ const CACHEABLE_ROUTES = new Set([ 'error-overview', 'funnel', 'funnel-sessions', - 'user-flow', 'custom-events', 'keywords', - 'gsc-dashboard', - 'gsc-details', ]) /** @@ -44,6 +43,7 @@ export class PublicProjectCacheInterceptor implements NestInterceptor { constructor( private readonly projectService: ProjectService, private readonly logger: AppLoggerService, + private readonly reflector: Reflector, ) {} async intercept( @@ -56,12 +56,19 @@ export class PublicProjectCacheInterceptor implements NestInterceptor { return next.handle() } - const route = getAnalyticsRoute(req.path || '') - if (!CACHEABLE_ROUTES.has(route)) { - return next.handle() + const isCacheableV2 = this.reflector.get( + CACHEABLE_ANALYTICS_KEY, + context.getHandler(), + ) + + if (!isCacheableV2) { + const route = getAnalyticsRoute(req.path || '') + if (!CACHEABLE_ROUTES.has(route)) { + return next.handle() + } } - const pid = getSinglePid(req.query || {}) + const pid = getSinglePid(req.query || {}, req.params || {}) if (!pid) { return next.handle() } diff --git a/backend/apps/cloud/src/analytics/v2/__tests__/breakdown-query.builder.spec.ts b/backend/apps/cloud/src/analytics/v2/__tests__/breakdown-query.builder.spec.ts new file mode 100644 index 000000000..710fae981 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/__tests__/breakdown-query.builder.spec.ts @@ -0,0 +1,157 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { + buildBreakdownQuery, + parseSortParam, +} from '../query/breakdown-query.builder' +import { + getBreakdownDimension, + getMetric, + parseMetricsParam, +} from '../registry' + +const SUB_QUERY = `FROM events WHERE pid = {pid:FixedString(12)} AND type = 'pageview' AND created BETWEEN {groupFrom:String} AND {groupTo:String}` + +const normalize = (sql: string) => sql.replace(/\s+/g, ' ').trim() + +describe('parseSortParam', () => { + const dimension = getBreakdownDimension('country', 'traffic') + const metrics = parseMetricsParam('visitors,pageviews', 'traffic') + + it('defaults to first metric desc', () => { + expect(parseSortParam(undefined, dimension, metrics)).toEqual({ + field: 'visitors', + direction: 'desc', + }) + }) + + it('parses field:direction and validates both', () => { + expect(parseSortParam('pageviews:asc', dimension, metrics)).toEqual({ + field: 'pageviews', + direction: 'asc', + }) + expect(parseSortParam('value:asc', dimension, metrics)).toEqual({ + field: 'value', + direction: 'asc', + }) + expect(() => parseSortParam('visitors:up', dimension, metrics)).toThrow( + UnprocessableEntityException, + ) + expect(() => parseSortParam('users:desc', dimension, metrics)).toThrow( + UnprocessableEntityException, + ) + }) +}) + +describe('buildBreakdownQuery', () => { + it('builds a multi-metric traffic query with pagination and total', () => { + const query = normalize( + buildBreakdownQuery({ + dataType: 'traffic', + dimension: getBreakdownDimension('country', 'traffic'), + metrics: parseMetricsParam('visitors,pageviews', 'traffic'), + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false }, + sort: { field: 'visitors', direction: 'desc' }, + }), + ) + + expect(query).toContain('cc AS value') + expect(query).toContain('count(DISTINCT psid) AS visitors') + expect(query).toContain('count(*) AS pageviews') + expect(query).toContain('count() OVER () AS __total') + expect(query).toContain('GROUP BY value') + expect(query).toContain('ORDER BY visitors desc') + expect(query).toContain('LIMIT {v2_limit:UInt32} OFFSET {v2_offset:UInt32}') + }) + + it('degrades visitors to count(*) under custom-event filters (v1 parity)', () => { + const query = buildBreakdownQuery({ + dataType: 'traffic', + dimension: getBreakdownDimension('country', 'traffic'), + metrics: [getMetric('visitors', 'traffic')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: true }, + sort: { field: 'visitors', direction: 'desc' }, + }) + + expect(query).toContain('count(*) AS visitors') + expect(query).not.toContain('count(DISTINCT psid)') + }) + + it('includes extra fields for region and groups by them', () => { + const query = normalize( + buildBreakdownQuery({ + dataType: 'traffic', + dimension: getBreakdownDimension('region', 'traffic'), + metrics: [getMetric('visitors', 'traffic')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false }, + sort: { field: 'visitors', direction: 'desc' }, + }), + ) + + expect(query).toContain('rg AS value') + expect(query).toContain('cc AS country') + expect(query).toContain('rgc AS region_code') + expect(query).toContain('GROUP BY value, country, region_code') + expect(query).toContain('AND rg IS NOT NULL') + }) + + it('applies the measure to performance metrics', () => { + const query = buildBreakdownQuery({ + dataType: 'performance', + dimension: getBreakdownDimension('page', 'performance'), + metrics: [getMetric('load_time', 'performance')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false, measure: 'p95' }, + sort: { field: 'load_time', direction: 'desc' }, + }) + + expect(query).toContain( + 'round(divide(quantileExact(0.95)(pageLoad), 1000), 2) AS load_time', + ) + }) + + it('uses captcha meta-column expressions and per-dimension guards', () => { + const query = normalize( + buildBreakdownQuery({ + dataType: 'captcha', + dimension: getBreakdownDimension('solve_time', 'captcha'), + metrics: [getMetric('events', 'captcha')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false }, + sort: { field: 'events', direction: 'desc' }, + }), + ) + + expect(query).toContain('multiIf(') + expect(query).toContain("= 'pass'") + + const countryQuery = normalize( + buildBreakdownQuery({ + dataType: 'captcha', + dimension: getBreakdownDimension('country', 'captcha'), + metrics: [getMetric('events', 'captcha')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false }, + sort: { field: 'events', direction: 'desc' }, + }), + ) + + expect(countryQuery).toContain("= 'pass'") + }) + + it('rejects sessions-join metrics with a 422', () => { + expect(() => + buildBreakdownQuery({ + dataType: 'traffic', + dimension: getBreakdownDimension('country', 'traffic'), + metrics: [getMetric('bounce_rate', 'traffic')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false }, + sort: { field: 'bounce_rate', direction: 'desc' }, + }), + ).toThrow(UnprocessableEntityException) + }) +}) diff --git a/backend/apps/cloud/src/analytics/v2/__tests__/filters.translator.spec.ts b/backend/apps/cloud/src/analytics/v2/__tests__/filters.translator.spec.ts new file mode 100644 index 000000000..7039e6cb6 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/__tests__/filters.translator.spec.ts @@ -0,0 +1,211 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { + normalizeFiltersToV1Json, + parseV2Filters, + toV1FiltersJson, +} from '../query/filters.translator' + +describe('parseV2Filters', () => { + it('returns [] for empty input', () => { + expect(parseV2Filters(undefined)).toEqual([]) + expect(parseV2Filters('')).toEqual([]) + expect(parseV2Filters('""')).toEqual([]) + }) + + it('parses valid filters', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'country', operator: 'is', value: ['US', 'DE'] }, + { dimension: 'page', operator: 'contains', value: '/blog' }, + ]), + ) + + expect(filters).toHaveLength(2) + expect(filters[0]).toEqual({ + dimension: 'country', + operator: 'is', + value: ['US', 'DE'], + }) + }) + + it('rejects invalid JSON, non-arrays, bad operators and values', () => { + expect(() => parseV2Filters('not json')).toThrow( + UnprocessableEntityException, + ) + expect(() => parseV2Filters('{"dimension":"country"}')).toThrow( + UnprocessableEntityException, + ) + expect(() => + parseV2Filters( + JSON.stringify([ + { dimension: 'country', operator: 'equals', value: 'US' }, + ]), + ), + ).toThrow(UnprocessableEntityException) + expect(() => + parseV2Filters( + JSON.stringify([{ dimension: 'country', operator: 'is', value: 42 }]), + ), + ).toThrow(UnprocessableEntityException) + }) + + it('accepts null values (filter for unknown)', () => { + const filters = parseV2Filters( + JSON.stringify([{ dimension: 'region', operator: 'is', value: null }]), + ) + + expect(filters[0].value).toBeNull() + }) +}) + +describe('toV1FiltersJson', () => { + it('translates operators to isExclusive/isContains and columns to v1 codes', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'country', operator: 'is', value: 'US' }, + { dimension: 'page', operator: 'contains', value: '/blog' }, + { dimension: 'browser', operator: 'is_not', value: 'Chrome' }, + { dimension: 'referrer', operator: 'contains_not', value: 'google' }, + ]), + ) + + const v1 = JSON.parse(toV1FiltersJson(filters, 'traffic')) + + expect(v1).toEqual([ + { column: 'cc', filter: 'US', isExclusive: false, isContains: false }, + { column: 'pg', filter: '/blog', isExclusive: false, isContains: true }, + { column: 'br', filter: 'Chrome', isExclusive: true, isContains: false }, + { + column: 'ref', + filter: 'google', + isExclusive: true, + isContains: true, + }, + ]) + }) + + it('translates keyed metadata filters to ev:key: / tag:key:', () => { + const filters = parseV2Filters( + JSON.stringify([ + { + dimension: 'event_metadata', + key: 'plan', + operator: 'is_not', + value: 'free', + }, + { + dimension: 'page_property', + key: 'author', + operator: 'is', + value: 'Andrii', + }, + { dimension: 'event_metadata', operator: 'is', value: 'plan' }, + ]), + ) + + const v1 = JSON.parse(toV1FiltersJson(filters, 'traffic')) + + expect(v1[0].column).toBe('ev:key:plan') + expect(v1[0].isExclusive).toBe(true) + expect(v1[1].column).toBe('tag:key:author') + expect(v1[2].column).toBe('ev:key') + }) + + it('maps errors-only dimensions through v1 public names', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'error_name', operator: 'is', value: 'TypeError' }, + ]), + ) + + const v1 = JSON.parse(toV1FiltersJson(filters, 'errors')) + + expect(v1[0].column).toBe('name') + }) + + it('rejects unknown dimensions strictly, drops them in lenient mode', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'error_name', operator: 'is', value: 'TypeError' }, + { dimension: 'country', operator: 'is', value: 'US' }, + ]), + ) + + expect(() => toV1FiltersJson(filters, 'traffic')).toThrow( + UnprocessableEntityException, + ) + + const lenient = JSON.parse( + toV1FiltersJson(filters, 'traffic', { lenient: true }), + ) + + expect(lenient).toHaveLength(1) + expect(lenient[0].column).toBe('cc') + }) + + it('keeps array values intact (v1 expands them internally)', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'country', operator: 'is', value: ['US', null] }, + ]), + ) + + const v1 = JSON.parse(toV1FiltersJson(filters, 'traffic')) + + expect(v1[0].filter).toEqual(['US', null]) + }) + + it('translates entry_page / exit_page / referrer_name / event filters', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'entry_page', operator: 'is', value: '/' }, + { dimension: 'exit_page', operator: 'is', value: '/bye' }, + { dimension: 'referrer_name', operator: 'is', value: 'google.com' }, + { dimension: 'event', operator: 'is', value: 'signup' }, + ]), + ) + + const v1 = JSON.parse(toV1FiltersJson(filters, 'traffic')) + + expect(v1.map((filter: { column: string }) => filter.column)).toEqual([ + 'entryPage', + 'exitPage', + 'refn', + 'ev', + ]) + }) +}) + +describe('normalizeFiltersToV1Json', () => { + it('returns "" for empty input', () => { + expect(normalizeFiltersToV1Json(undefined, 'traffic')).toBe('') + expect(normalizeFiltersToV1Json('', 'traffic')).toBe('') + expect(normalizeFiltersToV1Json('""', 'traffic')).toBe('') + expect(normalizeFiltersToV1Json('[]', 'traffic')).toBe('') + }) + + it('passes legacy v1 filters through untouched (public API back-compat)', () => { + const v1 = JSON.stringify([ + { column: 'cc', filter: 'US', isExclusive: false, isContains: false }, + ]) + + expect(normalizeFiltersToV1Json(v1, 'traffic')).toBe(v1) + }) + + it('translates v2 filters into the v1 shape', () => { + const v2 = JSON.stringify([ + { dimension: 'country', operator: 'is_not', value: 'US' }, + ]) + + const v1 = JSON.parse(normalizeFiltersToV1Json(v2, 'traffic')) + + expect(v1).toEqual([ + { column: 'cc', filter: 'US', isExclusive: true, isContains: false }, + ]) + }) + + it('returns malformed input unchanged for getFiltersQuery to handle', () => { + expect(normalizeFiltersToV1Json('not json', 'traffic')).toBe('not json') + }) +}) diff --git a/backend/apps/cloud/src/analytics/v2/__tests__/ioredis.stub.ts b/backend/apps/cloud/src/analytics/v2/__tests__/ioredis.stub.ts new file mode 100644 index 000000000..734d69d92 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/__tests__/ioredis.stub.ts @@ -0,0 +1,13 @@ +class RedisStub { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(..._args: unknown[]) {} + + defineCommand(): void {} + + on(): this { + return this + } +} + +export default RedisStub +export { RedisStub as Redis } diff --git a/backend/apps/cloud/src/analytics/v2/__tests__/registry.spec.ts b/backend/apps/cloud/src/analytics/v2/__tests__/registry.spec.ts new file mode 100644 index 000000000..bd907ebec --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/__tests__/registry.spec.ts @@ -0,0 +1,122 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { + getBreakdownDimension, + getDefaultMetrics, + getMetric, + listDimensions, + parseMetricsParam, + V2_DIMENSIONS, +} from '../registry' + +const { + TRAFFIC_COLUMNS, + PERFORMANCE_COLUMNS, + ERROR_COLUMNS, + CAPTCHA_COLUMNS, + // eslint-disable-next-line @typescript-eslint/no-require-imports +} = require('../../../common/constants') + +describe('registry <-> v1 column allowlist lockstep', () => { + const columnsFor = (type: 'traffic' | 'performance' | 'captcha' | 'errors') => + V2_DIMENSIONS.filter( + (dimension) => dimension.types.includes(type) && !dimension.virtual, + ).map((dimension) => dimension.column) + + it('covers every v1 traffic column', () => { + for (const column of TRAFFIC_COLUMNS) { + expect(columnsFor('traffic')).toContain(column) + } + }) + + it('covers every v1 performance column', () => { + for (const column of PERFORMANCE_COLUMNS) { + expect(columnsFor('performance')).toContain(column) + } + }) + + it('covers every v1 error column', () => { + for (const column of ERROR_COLUMNS) { + expect(columnsFor('errors')).toContain(column) + } + }) + + it('covers every v1 captcha column', () => { + for (const column of CAPTCHA_COLUMNS) { + expect(columnsFor('captcha')).toContain(column) + } + }) +}) + +describe('dimension lookups', () => { + it('resolves valid breakdown dimensions', () => { + expect(getBreakdownDimension('country', 'traffic').column).toBe('cc') + expect(getBreakdownDimension('page', 'performance').column).toBe('pg') + }) + + it('rejects unknown and filter-only dimensions with 422', () => { + expect(() => getBreakdownDimension('nope', 'traffic')).toThrow( + UnprocessableEntityException, + ) + expect(() => getBreakdownDimension('referrer_name', 'traffic')).toThrow( + UnprocessableEntityException, + ) + expect(() => getBreakdownDimension('event', 'traffic')).toThrow( + UnprocessableEntityException, + ) + expect(() => getBreakdownDimension('referrer', 'performance')).toThrow( + UnprocessableEntityException, + ) + }) + + it('has unique api names per data type', () => { + for (const type of [ + 'traffic', + 'performance', + 'captcha', + 'errors', + ] as const) { + const names = listDimensions(type).map((dimension) => dimension.api) + expect(new Set(names).size).toBe(names.length) + } + }) +}) + +describe('metric lookups', () => { + it('resolves metrics and rejects unknown ones', () => { + expect(getMetric('visitors', 'traffic').api).toBe('visitors') + expect(() => getMetric('visitors', 'errors')).toThrow( + UnprocessableEntityException, + ) + }) + + it('provides sensible defaults per data type', () => { + expect(getDefaultMetrics('traffic').map((metric) => metric.api)).toEqual([ + 'visitors', + 'pageviews', + ]) + expect( + getDefaultMetrics('performance').map((metric) => metric.api), + ).toEqual(['load_time']) + expect(getDefaultMetrics('errors').map((metric) => metric.api)).toEqual([ + 'occurrences', + ]) + }) + + it('parses and dedupes the metrics CSV param', () => { + expect( + parseMetricsParam('visitors, pageviews,visitors', 'traffic').map( + (metric) => metric.api, + ), + ).toEqual(['visitors', 'pageviews']) + + expect(parseMetricsParam(undefined, 'traffic').map((m) => m.api)).toEqual([ + 'visitors', + 'pageviews', + ]) + + expect(() => parseMetricsParam('bogus', 'traffic')).toThrow( + UnprocessableEntityException, + ) + }) +}) diff --git a/backend/apps/cloud/src/analytics/v2/__tests__/seo.mapper.spec.ts b/backend/apps/cloud/src/analytics/v2/__tests__/seo.mapper.spec.ts new file mode 100644 index 000000000..d90e11b99 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/__tests__/seo.mapper.spec.ts @@ -0,0 +1,210 @@ +import { TimeBucketType } from '../../dto/getData.dto' +import { + bucketSeoDateSeries, + gscCountryToAlpha2, + mapSeoBreakdownRows, + mapSeoSummary, + mapSeoTimeseries, + SeoDateSeriesEntry, +} from '../mappers/seo.mapper' + +const day = ( + date: string, + clicks: number, + impressions: number, + position: number, +): SeoDateSeriesEntry => ({ + date, + clicks, + impressions, + position, + ctr: impressions > 0 ? (clicks / impressions) * 100 : 0, +}) + +describe('gscCountryToAlpha2', () => { + it('converts Search Console alpha-3 to the alpha-2 the rest of v2 uses', () => { + expect(gscCountryToAlpha2('usa')).toBe('US') + expect(gscCountryToAlpha2('deu')).toBe('DE') + expect(gscCountryToAlpha2('ukr')).toBe('UA') + }) + + it('passes through anything it cannot map rather than dropping the row', () => { + expect(gscCountryToAlpha2('zzz')).toBe('ZZZ') + expect(gscCountryToAlpha2('')).toBe('') + }) +}) + +describe('mapSeoBreakdownRows', () => { + it("reads getKeywords' `count` as clicks", () => { + expect( + mapSeoBreakdownRows( + 'query', + [ + { + name: 'swetrix', + count: 12, + impressions: 400, + ctr: 3, + position: 4.2, + }, + ], + ['clicks', 'impressions', 'ctr', 'position'], + ), + ).toEqual([ + { value: 'swetrix', clicks: 12, impressions: 400, ctr: 3, position: 4.2 }, + ]) + }) + + it('reads the label field each per-dimension getter returns', () => { + expect( + mapSeoBreakdownRows('page', [{ page: '/blog', clicks: 5 }], ['clicks']), + ).toEqual([{ value: '/blog', clicks: 5 }]) + + expect( + mapSeoBreakdownRows( + 'device', + [{ device: 'mobile', clicks: 3 }], + ['clicks'], + ), + ).toEqual([{ value: 'mobile', clicks: 3 }]) + }) + + it('normalises country rows to alpha-2', () => { + expect( + mapSeoBreakdownRows( + 'country', + [{ country: 'usa', clicks: 9 }], + ['clicks'], + ), + ).toEqual([{ value: 'US', clicks: 9 }]) + }) + + it('only emits the requested metrics', () => { + expect( + mapSeoBreakdownRows( + 'query', + [{ name: 'a', count: 1, impressions: 2, ctr: 50, position: 1 }], + ['clicks'], + ), + ).toEqual([{ value: 'a', clicks: 1 }]) + }) + + it('defaults missing values to zero rather than emitting NaN', () => { + expect( + mapSeoBreakdownRows('query', [{ name: 'a' }], ['clicks', 'position']), + ).toEqual([{ value: 'a', clicks: 0, position: 0 }]) + }) +}) + +describe('mapSeoSummary', () => { + it('reports the absolute change between periods, matching the other v2 summaries', () => { + expect( + mapSeoSummary( + { clicks: 120, impressions: 3000, ctr: 4, position: 8.1 }, + { clicks: 100, impressions: 2500, ctr: 4.5, position: 9.4 }, + ), + ).toEqual({ + current: { clicks: 120, impressions: 3000, ctr: 4, position: 8.1 }, + previous: { clicks: 100, impressions: 2500, ctr: 4.5, position: 9.4 }, + change: { clicks: 20, impressions: 500, ctr: -0.5, position: -1.3 }, + }) + }) + + it('treats a missing previous period as zeroes', () => { + const summary = mapSeoSummary( + { clicks: 10, impressions: 100, ctr: 10, position: 2 }, + null, + ) + + expect(summary.previous).toEqual({ + clicks: 0, + impressions: 0, + ctr: 0, + position: 0, + }) + expect(summary.change.clicks).toBe(10) + }) +}) + +describe('bucketSeoDateSeries', () => { + const series = [ + day('2026-01-05', 10, 100, 10), + day('2026-01-20', 30, 300, 2), + day('2026-02-10', 5, 50, 4), + ] + + it('leaves the native hour and day granularity untouched', () => { + expect(bucketSeoDateSeries(series, TimeBucketType.DAY)).toBe(series) + expect(bucketSeoDateSeries(series, TimeBucketType.HOUR)).toBe(series) + }) + + it('sums clicks and impressions into month buckets', () => { + const [january, february] = bucketSeoDateSeries( + series, + TimeBucketType.MONTH, + ) + + expect(january.date).toBe('2026-01-01') + expect(january.clicks).toBe(40) + expect(january.impressions).toBe(400) + expect(february.date).toBe('2026-02-01') + expect(february.clicks).toBe(5) + }) + + it('recomputes ctr from the bucket totals rather than averaging it', () => { + const [january] = bucketSeoDateSeries(series, TimeBucketType.MONTH) + // 40 clicks / 400 impressions, not the mean of the two daily CTRs. + expect(january.ctr).toBe(10) + }) + + it('averages position weighted by impressions', () => { + const [january] = bucketSeoDateSeries(series, TimeBucketType.MONTH) + // (10*100 + 2*300) / 400 = 4, whereas a plain mean would give 6. + expect(january.position).toBe(4) + }) + + it('rolls a whole year up into one bucket', () => { + const yearly = bucketSeoDateSeries(series, TimeBucketType.YEAR) + + expect(yearly).toHaveLength(1) + expect(yearly[0].date).toBe('2026-01-01') + expect(yearly[0].clicks).toBe(45) + }) + + it('reports zeroed rates for a bucket with no impressions', () => { + const [bucket] = bucketSeoDateSeries( + [day('2026-01-05', 0, 0, 0)], + TimeBucketType.MONTH, + ) + + expect(bucket.ctr).toBe(0) + expect(bucket.position).toBe(0) + }) +}) + +describe('mapSeoTimeseries', () => { + it('emits ISO timestamps in the requested timezone', () => { + const [row] = mapSeoTimeseries( + [day('2026-01-05', 10, 100, 3)], + ['clicks'], + 'Europe/Kyiv', + ) + + expect(row.timestamp).toBe('2026-01-05T00:00:00+02:00') + expect(row.clicks).toBe(10) + }) + + it('only emits the requested metrics', () => { + const [row] = mapSeoTimeseries( + [day('2026-01-05', 10, 100, 3)], + ['clicks', 'position'], + 'UTC', + ) + + expect(Object.keys(row).sort()).toEqual(['clicks', 'position', 'timestamp']) + }) + + it('drops rows Search Console returned without a date', () => { + expect(mapSeoTimeseries([day('', 1, 1, 1)], ['clicks'], 'UTC')).toEqual([]) + }) +}) diff --git a/backend/apps/cloud/src/analytics/v2/__tests__/seo.registry.spec.ts b/backend/apps/cloud/src/analytics/v2/__tests__/seo.registry.spec.ts new file mode 100644 index 000000000..145d9407b --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/__tests__/seo.registry.spec.ts @@ -0,0 +1,134 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { V2Filter } from '../query/filters.translator' +import { + getDefaultSeoMetrics, + getSeoBreakdownDimension, + getSeoMetric, + listSeoDimensions, + parseSeoMetricsParam, + toGscFiltersJson, + validateSeoFilters, + V2_SEO_DIMENSIONS, +} from '../registry/seo' + +const filter = (overrides: Partial = {}): V2Filter => + ({ + dimension: 'country', + operator: 'is', + value: 'US', + ...overrides, + }) as V2Filter + +describe('seo dimension lookups', () => { + it('resolves every declared dimension to its Search Console name', () => { + expect(getSeoBreakdownDimension('query').gsc).toBe('query') + expect(getSeoBreakdownDimension('page').gsc).toBe('page') + expect(getSeoBreakdownDimension('country').gsc).toBe('country') + expect(getSeoBreakdownDimension('device').gsc).toBe('device') + }) + + it('rejects unknown dimensions with 422', () => { + expect(() => getSeoBreakdownDimension('nope')).toThrow( + UnprocessableEntityException, + ) + // Valid for traffic, but Search Console reports nothing like it. + expect(() => getSeoBreakdownDimension('browser')).toThrow( + UnprocessableEntityException, + ) + }) + + it('has unique api names', () => { + const names = listSeoDimensions().map((dimension) => dimension.api) + expect(new Set(names).size).toBe(names.length) + }) + + it('only maps onto dimensions Search Console actually serves', () => { + for (const dimension of V2_SEO_DIMENSIONS) { + expect(['query', 'page', 'country', 'device']).toContain(dimension.gsc) + } + }) +}) + +describe('seo metric lookups', () => { + it('resolves metrics and rejects unknown ones', () => { + expect(getSeoMetric('clicks').api).toBe('clicks') + expect(() => getSeoMetric('visitors')).toThrow(UnprocessableEntityException) + }) + + it('returns all four metrics by default', () => { + expect(getDefaultSeoMetrics().map((metric) => metric.api)).toEqual([ + 'clicks', + 'impressions', + 'ctr', + 'position', + ]) + }) + + it('parses and dedupes the metrics CSV param', () => { + expect( + parseSeoMetricsParam('clicks, impressions,clicks').map( + (metric) => metric.api, + ), + ).toEqual(['clicks', 'impressions']) + + expect(parseSeoMetricsParam(undefined).map((metric) => metric.api)).toEqual( + ['clicks', 'impressions', 'ctr', 'position'], + ) + + expect(() => parseSeoMetricsParam('bogus')).toThrow( + UnprocessableEntityException, + ) + }) +}) + +describe('validateSeoFilters', () => { + it('accepts scalar filters on known dimensions', () => { + const filters = [filter(), filter({ dimension: 'query', value: 'swetrix' })] + expect(validateSeoFilters(filters)).toBe(filters) + }) + + it('rejects filters on dimensions Search Console cannot filter by', () => { + expect(() => + validateSeoFilters([filter({ dimension: 'browser' })]), + ).toThrow(UnprocessableEntityException) + }) + + it('rejects array values, which have no dimensionFilterGroups equivalent', () => { + expect(() => validateSeoFilters([filter({ value: ['US', 'DE'] })])).toThrow( + UnprocessableEntityException, + ) + }) + + it('rejects null values, since Search Console reports no unset values', () => { + expect(() => validateSeoFilters([filter({ value: null })])).toThrow( + UnprocessableEntityException, + ) + }) +}) + +describe('toGscFiltersJson', () => { + it('renames query -> keywords, the key GSCService.parseFilters expects', () => { + const json = toGscFiltersJson([ + filter({ dimension: 'query', value: 'swetrix' }), + ]) + + expect(JSON.parse(json)).toEqual([ + { dimension: 'keywords', operator: 'is', value: 'swetrix' }, + ]) + }) + + it('leaves dimensions that already share a name alone', () => { + const json = toGscFiltersJson([ + filter({ dimension: 'page', operator: 'contains', value: '/blog' }), + ]) + + expect(JSON.parse(json)).toEqual([ + { dimension: 'page', operator: 'contains', value: '/blog' }, + ]) + }) + + it('returns undefined for no filters so no filter group is sent', () => { + expect(toGscFiltersJson([])).toBeUndefined() + }) +}) diff --git a/backend/apps/cloud/src/analytics/v2/__tests__/timeseries.mapper.spec.ts b/backend/apps/cloud/src/analytics/v2/__tests__/timeseries.mapper.spec.ts new file mode 100644 index 000000000..1c97d0db3 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/__tests__/timeseries.mapper.spec.ts @@ -0,0 +1,72 @@ +import { + computeBounceRateSeries, + toIsoTimestamp, + zipTimeseries, +} from '../mappers/timeseries.mapper' + +describe('toIsoTimestamp', () => { + it('applies the timezone offset for day labels', () => { + expect(toIsoTimestamp('2026-07-01', 'Europe/Kyiv')).toBe( + '2026-07-01T00:00:00+03:00', + ) + }) + + it('handles hour labels and UTC', () => { + expect(toIsoTimestamp('2026-07-01 13:00:00', 'Etc/GMT')).toBe( + '2026-07-01T13:00:00Z', + ) + }) + + it('handles month and year labels', () => { + expect(toIsoTimestamp('2026-07', 'Etc/GMT')).toBe('2026-07-01T00:00:00Z') + expect(toIsoTimestamp('2026', 'Etc/GMT')).toBe('2026-01-01T00:00:00Z') + }) +}) + +describe('zipTimeseries', () => { + it('zips columnar series into self-describing rows', () => { + const rows = zipTimeseries( + ['2026-07-01', '2026-07-02'], + { + visitors: [10, 20], + pageviews: [30, 40], + skipped: undefined, + }, + 'Etc/GMT', + ) + + expect(rows).toEqual([ + { + timestamp: '2026-07-01T00:00:00Z', + visitors: 10, + pageviews: 30, + }, + { + timestamp: '2026-07-02T00:00:00Z', + visitors: 20, + pageviews: 40, + }, + ]) + }) + + it('fills missing values with null', () => { + const rows = zipTimeseries( + ['2026-07-01', '2026-07-02'], + { visitors: [10] }, + 'Etc/GMT', + ) + + expect(rows[1].visitors).toBeNull() + }) +}) + +describe('computeBounceRateSeries', () => { + it('computes per-bucket bounce rate as bounces / uniques * 100', () => { + expect(computeBounceRateSeries([1, 0, 3], [4, 0, 6])).toEqual([25, 0, 50]) + }) + + it('returns undefined when either input is missing', () => { + expect(computeBounceRateSeries(undefined, [1])).toBeUndefined() + expect(computeBounceRateSeries([1], undefined)).toBeUndefined() + }) +}) diff --git a/backend/apps/cloud/src/analytics/v2/analytics-v2.service.ts b/backend/apps/cloud/src/analytics/v2/analytics-v2.service.ts new file mode 100644 index 000000000..3d7756d4a --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/analytics-v2.service.ts @@ -0,0 +1,1967 @@ +import { + BadRequestException, + Injectable, + InternalServerErrorException, + UnprocessableEntityException, +} from '@nestjs/common' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' + +import { clickhouse } from '../../common/integrations/clickhouse' +import { AppLoggerService } from '../../logger/logger.service' +import { MAX_METRICS_IN_VIEW } from '../../project/dto/create-project-view.dto' +import { IFunnel, PerfMeasure } from '../interfaces' +import { + AnalyticsService, + getLowestPossibleTimeBucket, +} from '../analytics.service' +import { ChartRenderMode, TimeBucketType } from '../dto/getData.dto' +import { GetCustomEventMetadata } from '../dto/get-custom-event-meta.dto' +import { GetPagePropertyMetaDto } from '../dto/get-page-property-meta.dto' +import { + findDimension, + getBreakdownDimension, + getMetric, + listDimensions, + listMetrics, + parseMetricsParam, + V2_TO_V1_DATATYPE, + V2DataType, + V2MetricDef, +} from './registry' +import { + parseV2Filters, + toV1FiltersJson, + V2Filter, +} from './query/filters.translator' +import { + buildBreakdownQuery, + parseSortParam, +} from './query/breakdown-query.builder' +import { + computeBounceRateSeries, + toIsoTimestamp, + zipTimeseries, + TimeseriesRow, +} from './mappers/timeseries.mapper' +import { renameEntityKeys, renameEntityList } from './mappers/entity.mapper' +import { mapTrafficSummary, V1TrafficSummary } from './mappers/summary.mapper' +import { envelope, V2Envelope } from './mappers/envelope' +import { + V2_DEFAULT_PERIOD, + V2BaseQueryDto, + V2BreakdownDto, + V2TimeseriesDto, +} from './dto/v2-base.dto' + +dayjs.extend(utc) + +type EventsAllTimeType = + | 'pageview' + | 'custom_event' + | 'performance' + | 'error' + | 'captcha' + +const ALL_TIME_EVENT_TYPES: Record = { + traffic: ['pageview', 'custom_event', 'error'], + performance: ['performance'], + captcha: ['captcha'], + errors: ['error'], +} + +const TRAFFIC_TIMESERIES_METRICS = [ + 'visitors', + 'pageviews', + 'session_duration', + 'bounce_rate', + 'concurrency', +] + +const PERFORMANCE_TIMESERIES_METRICS: Record = { + dns: 'dns', + tls: 'tls', + connection: 'conn', + response: 'response', + render: 'render', + dom_load: 'domLoad', + ttfb: 'ttfb', +} + +const CAPTCHA_TIMESERIES_METRICS: Record = { + generated: 'generated', + passed: 'passed', + failed: 'failed', + validation_failed: 'validationFailed', + replayed: 'replayed', +} + +const ONLINE_VISITORS_WINDOW_MINUTES = 5 + +export interface ResolvedTimeframe { + period: string | null + timeBucket: TimeBucketType | null + allowedTimeBuckets: TimeBucketType[] | null + safeTimezone: string + groupFrom: string + groupTo: string + groupFromUTC: string + groupToUTC: string +} + +interface PreparedFilters { + appliedFilters: V2Filter[] + v1FiltersJson: string + filtersQuery: string + filtersParams: Record + customEVFilterApplied: boolean +} + +const DEFAULT_BREAKDOWN_LIMIT = 30 + +@Injectable() +export class AnalyticsV2Service { + constructor( + private readonly analyticsService: AnalyticsService, + private readonly logger: AppLoggerService, + ) {} + + async assertReadAccess( + pid: string, + uid: string | null, + password?: string, + ): Promise { + await this.analyticsService.checkProjectAccess(pid, uid, password) + await this.analyticsService.checkBillingAccess(pid) + } + + async resolveTimeframe(opts: { + pid: string + dataType: V2DataType + period?: string + from?: string + to?: string + timeBucket?: TimeBucketType + timezone?: string + allTimeEventTypes?: readonly EventsAllTimeType[] + }): Promise { + const { pid, dataType, from, to, timezone } = opts + + const safeTimezone = this.analyticsService.getSafeTimezone(timezone) + + if (Boolean(from) !== Boolean(to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(from && to) + const period = hasCustomRange + ? null + : opts.period && opts.period !== 'custom' + ? opts.period + : V2_DEFAULT_PERIOD + + let timeBucket = + opts.timeBucket || + getLowestPossibleTimeBucket( + period ?? undefined, + hasCustomRange ? from : undefined, + hasCustomRange ? to : undefined, + ) + + let diff: number | undefined + let allowedTimeBuckets: TimeBucketType[] | null = null + + if (period === 'all') { + const allTime = await this.analyticsService.calculateTimeBucketForAllTime( + pid, + opts.allTimeEventTypes ?? ALL_TIME_EVENT_TYPES[dataType], + ) + + diff = allTime.diff + allowedTimeBuckets = allTime.timeBucket + timeBucket = + opts.timeBucket && allTime.timeBucket.includes(opts.timeBucket) + ? opts.timeBucket + : allTime.timeBucket[0] + } + + const { groupFrom, groupTo, groupFromUTC, groupToUTC } = + this.analyticsService.getGroupFromTo( + hasCustomRange ? from : undefined, + hasCustomRange ? to : undefined, + timeBucket, + period ?? undefined, + safeTimezone, + diff, + ) + + return { + period, + timeBucket, + allowedTimeBuckets, + safeTimezone, + groupFrom, + groupTo, + groupFromUTC, + groupToUTC, + } + } + + prepareFilters( + filters: string | undefined, + dataType: V2DataType, + ignoreEV?: boolean, + ): PreparedFilters { + const appliedFilters = parseV2Filters(filters) + const v1FiltersJson = toV1FiltersJson(appliedFilters, dataType) + + const [filtersQuery, filtersParams, , customEVFilterApplied] = + this.analyticsService.getFiltersQuery( + v1FiltersJson, + V2_TO_V1_DATATYPE[dataType], + ignoreEV, + ) + + return { + appliedFilters, + v1FiltersJson, + filtersQuery, + filtersParams, + customEVFilterApplied, + } + } + + buildSubQuery( + dataType: V2DataType, + filtersQuery: string, + customEVFilterApplied: boolean, + ): string { + if (dataType === 'traffic') { + return this.analyticsService.buildAnalyticsEventsSubQuery( + filtersQuery, + customEVFilterApplied, + ) + } + + if (dataType === 'captcha') { + return this.analyticsService.buildAnalyticsEventsSubQuery( + filtersQuery, + false, + true, + ) + } + + const eventType = dataType === 'performance' ? 'performance' : 'error' + + return `FROM events WHERE pid = {pid:FixedString(12)} AND type = '${eventType}' ${filtersQuery} AND created BETWEEN {groupFrom:String} AND {groupTo:String}` + } + + private baseMeta( + pid: string, + timeframe: ResolvedTimeframe, + appliedFilters: V2Filter[], + ): Record { + return { + pid, + period: timeframe.period, + from: toIsoTimestamp(timeframe.groupFrom, timeframe.safeTimezone), + to: toIsoTimestamp(timeframe.groupTo, timeframe.safeTimezone), + timezone: timeframe.safeTimezone, + appliedFilters, + } + } + + async getBreakdown( + pid: string, + dto: V2BreakdownDto, + dataType: V2DataType, + measure?: PerfMeasure, + ): Promise[]>> { + const dimension = getBreakdownDimension(dto.dimension, dataType) + const metrics = + dimension.virtual && !dto.metrics + ? [getMetric('visitors', dataType)] + : parseMetricsParam(dto.metrics, dataType) + const filters = this.prepareFilters( + dto.filters, + dataType, + dataType === 'performance' || dataType === 'errors', + ) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + const sort = parseSortParam(dto.sort, dimension, metrics) + const subQuery = this.buildSubQuery( + dataType, + filters.filtersQuery, + filters.customEVFilterApplied, + ) + + const meta: Record = { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + dimension: dimension.api, + metrics: metrics.map((metric) => metric.api), + limit, + offset, + sort: `${sort.field}:${sort.direction}`, + } + + if (dimension.virtual) { + const rows = await this.getVirtualPageBreakdown( + dimension.virtual, + dataType, + metrics, + filters, + subQuery, + { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + sort, + ) + + meta.total = rows.length + + return envelope(rows.slice(offset, offset + limit), meta) + } + + const query = buildBreakdownQuery({ + dataType, + dimension, + metrics, + subQuery, + ctx: { + customEVFilterApplied: filters.customEVFilterApplied, + measure, + }, + sort, + }) + + const { data } = await clickhouse + .query({ + query, + query_params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + v2_limit: limit, + v2_offset: offset, + }, + }) + .then((resultSet) => resultSet.json>()) + + let total = 0 + + const rows = (data || []).map((row) => { + const { __total, value, ...rest } = row + + total = Number(__total) || 0 + + const mapped: Record = { value } + + for (const field of dimension.extraFields ?? []) { + mapped[field.api] = rest[field.api] ?? null + } + + for (const metric of metrics) { + mapped[metric.api] = Number(rest[metric.api]) || 0 + } + + return mapped + }) + + meta.total = total + + return envelope(rows, meta) + } + + private async getVirtualPageBreakdown( + kind: 'entry_page' | 'exit_page', + dataType: V2DataType, + metrics: V2MetricDef[], + filters: PreparedFilters, + subQuery: string, + params: Record, + sort: { field: string; direction: 'asc' | 'desc' }, + ): Promise[]> { + if (dataType !== 'traffic') { + throw new UnprocessableEntityException( + `The '${kind}' dimension is only available for traffic breakdowns`, + ) + } + + if (filters.customEVFilterApplied) { + throw new UnprocessableEntityException( + `The '${kind}' dimension cannot be combined with custom event filters`, + ) + } + + const unsupported = metrics.filter((metric) => metric.api !== 'visitors') + + if (unsupported.length > 0) { + throw new UnprocessableEntityException( + `The '${kind}' dimension only supports the 'visitors' metric`, + ) + } + + const paramsData = { params } + + const rows = + kind === 'entry_page' + ? await this.analyticsService.getEntryPages(subQuery, paramsData) + : await this.analyticsService.getExitPages(subQuery, paramsData) + + const mapped = (rows || []).map((row) => ({ + value: row.name, + visitors: Number(row.count) || 0, + })) + + const directionFactor = sort.direction === 'asc' ? 1 : -1 + + mapped.sort((a, b) => { + if (sort.field === 'value') { + return String(a.value).localeCompare(String(b.value)) * directionFactor + } + + return (a.visitors - b.visitors) * directionFactor + }) + + return mapped + } + + async getTrafficTimeseries( + pid: string, + dto: V2TimeseriesDto, + ): Promise> { + const dataType: V2DataType = 'traffic' + const metrics = parseMetricsParam(dto.metrics, dataType) + + for (const metric of metrics) { + if (!TRAFFIC_TIMESERIES_METRICS.includes(metric.api)) { + throw new UnprocessableEntityException( + `The '${metric.api}' metric is not supported on the traffic timeseries. Supported metrics: ${TRAFFIC_TIMESERIES_METRICS.join(', ')}`, + ) + } + } + + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + }) + + const mode = dto.mode || ChartRenderMode.PERIODICAL + + const includeConcurrency = metrics.some( + (metric) => metric.api === 'concurrency', + ) + + const result = (await this.analyticsService.groupChartByTimeBucket( + timeframe.timeBucket, + timeframe.groupFrom, + timeframe.groupTo, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + filters.customEVFilterApplied, + mode, + includeConcurrency, + )) as { + chart: { + x: string[] + visits: number[] + uniques: number[] + sdur: number[] + bounces?: number[] + concurrency?: number[] + } + } + + const { chart } = result + + const availableSeries: Record = { + visitors: chart.uniques, + pageviews: chart.visits, + session_duration: chart.sdur, + bounce_rate: computeBounceRateSeries(chart.bounces, chart.uniques), + concurrency: chart.concurrency, + } + + const series: Record = {} + + for (const metric of metrics) { + series[metric.api] = availableSeries[metric.api] + } + + return envelope(zipTimeseries(chart.x, series, timeframe.safeTimezone), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: timeframe.allowedTimeBuckets, + metrics: metrics.map((metric) => metric.api), + mode, + }) + } + + async getTrafficSummary( + pid: string, + dto: V2BaseQueryDto, + ): Promise>> { + const dataType: V2DataType = 'traffic' + const appliedFilters = parseV2Filters(dto.filters) + const v1FiltersJson = toV1FiltersJson(appliedFilters, dataType) + + const hasCustomRange = Boolean(dto.from && dto.to) + + if (Boolean(dto.from) !== Boolean(dto.to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const period = hasCustomRange + ? undefined + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + const result = await this.analyticsService.getAnalyticsSummary( + [pid], + undefined, + period, + hasCustomRange ? dto.from : undefined, + hasCustomRange ? dto.to : undefined, + dto.timezone, + v1FiltersJson, + false, + ) + + const summary = result?.[pid] as V1TrafficSummary | undefined + + if (!summary) { + throw new InternalServerErrorException( + 'Failed to compute the traffic summary for this project', + ) + } + + return envelope(mapTrafficSummary(summary), { + pid, + period: period ?? null, + from: hasCustomRange ? dto.from : undefined, + to: hasCustomRange ? dto.to : undefined, + timezone: this.analyticsService.getSafeTimezone(dto.timezone), + appliedFilters, + }) + } + + async getTrafficCustomEvents( + pid: string, + dto: V2BaseQueryDto & { limit?: number; offset?: number }, + ): Promise> { + const dataType: V2DataType = 'traffic' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const customs = await this.analyticsService.getCustomEvents( + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + ) + + const rows = Object.entries(customs || {}) + .map(([event, count]) => ({ event, count: Number(count) || 0 })) + .sort((a, b) => b.count - a.count) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + return envelope(rows.slice(offset, offset + limit), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + total: rows.length, + limit, + offset, + }) + } + + async getTrafficCustomMetrics( + pid: string, + dto: V2BaseQueryDto & { metrics: string }, + ): Promise> { + const dataType: V2DataType = 'traffic' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const parsedMetrics = this.analyticsService.parseMetrics(dto.metrics) + + if (parsedMetrics.length > MAX_METRICS_IN_VIEW) { + throw new UnprocessableEntityException( + `The maximum number of metrics within one request is ${MAX_METRICS_IN_VIEW}`, + ) + } + + if (!parsedMetrics.length) { + return envelope([], this.baseMeta(pid, timeframe, filters.appliedFilters)) + } + + const meta = await this.analyticsService.getMetaResults( + pid, + parsedMetrics, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + timeframe.period ?? undefined, + dto.from, + dto.to, + ) + + return envelope( + meta ?? [], + this.baseMeta(pid, timeframe, filters.appliedFilters), + ) + } + + async getTrafficPageProperties( + pid: string, + dto: V2BaseQueryDto & { limit?: number; offset?: number }, + ): Promise> { + const dataType: V2DataType = 'traffic' + const filters = this.prepareFilters(dto.filters, dataType) + + if (filters.customEVFilterApplied) { + throw new UnprocessableEntityException( + 'Page properties cannot be combined with custom event filters', + ) + } + + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const properties = await this.analyticsService.getPageProperties( + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + ) + + const rows = Object.entries(properties || {}) + .map(([property, count]) => ({ property, count: Number(count) || 0 })) + .sort((a, b) => b.count - a.count) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + return envelope(rows.slice(offset, offset + limit), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + total: rows.length, + limit, + offset, + }) + } + + async getPerformanceTimeseries( + pid: string, + dto: V2TimeseriesDto & { measure?: string }, + ): Promise> { + const dataType: V2DataType = 'performance' + const measure = (dto.measure || 'median') as PerfMeasure + + this.analyticsService.checkIfPerfMeasureIsValid(measure) + + const filters = this.prepareFilters(dto.filters, dataType, true) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + }) + + const chart = (await this.analyticsService.getPerfChartData( + timeframe.timeBucket, + timeframe.groupFrom, + timeframe.groupTo, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + measure, + )) as { x: string[] } & Record + + const series: Record = {} + let metricNames: string[] + + if (measure === 'quantiles') { + metricNames = ['p50', 'p75', 'p95'] + + for (const name of metricNames) { + series[name] = chart[name] + } + } else { + metricNames = dto.metrics + ? [ + ...new Set( + dto.metrics + .split(',') + .map((name) => name.trim()) + .filter(Boolean), + ), + ] + : Object.keys(PERFORMANCE_TIMESERIES_METRICS) + + for (const name of metricNames) { + const v1Key = Object.hasOwn(PERFORMANCE_TIMESERIES_METRICS, name) + ? PERFORMANCE_TIMESERIES_METRICS[name] + : undefined + + if (!v1Key) { + throw new UnprocessableEntityException( + `The '${name}' metric is not supported on the performance timeseries. Supported metrics: ${Object.keys(PERFORMANCE_TIMESERIES_METRICS).join(', ')}`, + ) + } + + series[name] = chart[v1Key] + } + } + + return envelope(zipTimeseries(chart.x, series, timeframe.safeTimezone), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: timeframe.allowedTimeBuckets, + metrics: metricNames, + measure, + }) + } + + async getPerformanceSummary( + pid: string, + dto: V2BaseQueryDto & { measure?: string }, + ): Promise>> { + const dataType: V2DataType = 'performance' + let measure = (dto.measure || 'median') as PerfMeasure + + this.analyticsService.checkIfPerfMeasureIsValid(measure) + + if (measure === 'quantiles') { + measure = 'median' + } + + const appliedFilters = parseV2Filters(dto.filters) + const v1FiltersJson = toV1FiltersJson(appliedFilters, dataType) + + if (Boolean(dto.from) !== Boolean(dto.to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(dto.from && dto.to) + const period = hasCustomRange + ? undefined + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + const result = await this.analyticsService.getPerformanceSummary( + [pid], + period, + hasCustomRange ? dto.from : undefined, + hasCustomRange ? dto.to : undefined, + dto.timezone, + v1FiltersJson, + measure, + ) + + const summary = result?.[pid] as + | { + current: Record + previous: Record + frontendChange: number + networkChange: number + backendChange: number + } + | undefined + + if (!summary) { + throw new InternalServerErrorException( + 'Failed to compute the performance summary for this project', + ) + } + + return envelope( + { + current: summary.current, + previous: summary.previous, + change: { + frontend: summary.frontendChange, + network: summary.networkChange, + backend: summary.backendChange, + }, + }, + { + pid, + period: period ?? null, + from: hasCustomRange ? dto.from : undefined, + to: hasCustomRange ? dto.to : undefined, + timezone: this.analyticsService.getSafeTimezone(dto.timezone), + measure, + appliedFilters, + }, + ) + } + + async getCaptchaTimeseries( + pid: string, + dto: V2TimeseriesDto, + ): Promise> { + const dataType: V2DataType = 'captcha' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + }) + + const mode = dto.mode || ChartRenderMode.PERIODICAL + + const { xShifted } = this.analyticsService.generateXAxis( + timeframe.timeBucket, + timeframe.groupFrom, + timeframe.groupTo, + timeframe.safeTimezone, + ) + + const query = this.analyticsService.generateCaptchaAggregationQuery( + timeframe.timeBucket, + filters.filtersQuery, + mode, + ) + + const { data } = await clickhouse + .query({ + query, + query_params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + timezone: timeframe.safeTimezone, + }, + }) + .then((resultSet) => resultSet.json>()) + + const chart = this.analyticsService.extractCaptchaChartData( + data, + xShifted, + ) as Record + + const metricNames = dto.metrics + ? [ + ...new Set( + dto.metrics + .split(',') + .map((name) => name.trim()) + .filter(Boolean), + ), + ] + : Object.keys(CAPTCHA_TIMESERIES_METRICS) + + const series: Record = {} + + for (const name of metricNames) { + const v1Key = Object.hasOwn(CAPTCHA_TIMESERIES_METRICS, name) + ? CAPTCHA_TIMESERIES_METRICS[name] + : undefined + + if (!v1Key) { + throw new UnprocessableEntityException( + `The '${name}' metric is not supported on the captcha timeseries. Supported metrics: ${Object.keys(CAPTCHA_TIMESERIES_METRICS).join(', ')}`, + ) + } + + series[name] = chart[v1Key] + } + + return envelope(zipTimeseries(xShifted, series, timeframe.safeTimezone), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: timeframe.allowedTimeBuckets, + metrics: metricNames, + mode, + }) + } + + async getCaptchaSummary( + pid: string, + dto: V2BaseQueryDto, + ): Promise>> { + const dataType: V2DataType = 'captcha' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const baseParams = { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + } + + const currentPromise = this.analyticsService.getCaptchaSummary( + filters.filtersQuery, + { params: baseParams }, + ) + + let previousPromise: Promise = Promise.resolve(null) + + if (timeframe.period !== 'all') { + const periodSubtracted = dayjs + .utc(timeframe.groupFromUTC) + .subtract( + Math.abs( + dayjs + .utc(timeframe.groupFromUTC) + .diff(dayjs.utc(timeframe.groupToUTC), 'minutes'), + ), + 'minutes', + ) + .format('YYYY-MM-DD HH:mm:ss') + + previousPromise = this.analyticsService.getCaptchaSummary( + filters.filtersQuery, + { + params: { + ...baseParams, + groupFrom: periodSubtracted, + groupTo: timeframe.groupFromUTC, + }, + }, + ) + } + + const [current, previous] = await Promise.all([ + currentPromise, + previousPromise, + ]) + + return envelope( + { current, previous }, + this.baseMeta(pid, timeframe, filters.appliedFilters), + ) + } + + async getErrorsList( + pid: string, + dto: V2BaseQueryDto & { + limit?: number + offset?: number + show_resolved?: boolean + }, + ): Promise[]>> { + const dataType: V2DataType = 'errors' + const filters = this.prepareFilters(dto.filters, dataType, true) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + const errors = await this.analyticsService.getErrorsList( + JSON.stringify({ showResolved: Boolean(dto.show_resolved) }), + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + limit, + offset, + ) + + return envelope(renameEntityList(errors), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + limit, + offset, + showResolved: Boolean(dto.show_resolved), + }) + } + + async getErrorsTimeseries( + pid: string, + dto: V2TimeseriesDto, + ): Promise> { + const dataType: V2DataType = 'errors' + const filters = this.prepareFilters(dto.filters, dataType, true) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + }) + + const mode = dto.mode || ChartRenderMode.PERIODICAL + + const { x, format } = this.analyticsService.generateUTCXAxis( + timeframe.timeBucket, + timeframe.groupFromUTC, + timeframe.groupToUTC, + ) + + const query = this.analyticsService.generateErrorsAggregationQuery( + timeframe.timeBucket, + filters.filtersQuery, + mode, + ) + + const { data } = await clickhouse + .query({ + query, + query_params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }) + .then((resultSet) => resultSet.json>()) + + const { count, affectedUsers } = + this.analyticsService.extractErrorsChartData(data as never[], x) + + const labels = this.analyticsService.shiftToTimezone( + x, + timeframe.safeTimezone, + format, + ) + + const availableSeries: Record = { + occurrences: count, + affected_users: affectedUsers, + } + + const metricNames = dto.metrics + ? [ + ...new Set( + dto.metrics + .split(',') + .map((name) => name.trim()) + .filter(Boolean), + ), + ] + : Object.keys(availableSeries) + + const series: Record = {} + + for (const name of metricNames) { + if (!Object.hasOwn(availableSeries, name) || !availableSeries[name]) { + throw new UnprocessableEntityException( + `The '${name}' metric is not supported on the errors timeseries. Supported metrics: ${Object.keys(availableSeries).join(', ')}`, + ) + } + + series[name] = availableSeries[name] + } + + return envelope(zipTimeseries(labels, series, timeframe.safeTimezone), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: timeframe.allowedTimeBuckets, + metrics: metricNames, + mode, + }) + } + + async getErrorsOverview( + pid: string, + dto: V2BaseQueryDto & { show_resolved?: boolean }, + ): Promise> { + const dataType: V2DataType = 'errors' + const filters = this.prepareFilters(dto.filters, dataType, true) + + const sessionV1FiltersJson = toV1FiltersJson( + filters.appliedFilters, + 'traffic', + { lenient: true }, + ) + const [sessionFiltersQuery, sessionFiltersParams] = + this.analyticsService.getFiltersQuery( + sessionV1FiltersJson, + V2_TO_V1_DATATYPE.traffic, + true, + ) + + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const overview = await this.analyticsService.getErrorOverview( + pid, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + timeframe.groupFromUTC, + timeframe.groupToUTC, + timeframe.timeBucket, + Boolean(dto.show_resolved), + sessionFiltersQuery, + sessionFiltersParams, + ) + + return envelope(overview, { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + showResolved: Boolean(dto.show_resolved), + }) + } + + async getErrorDetails( + pid: string, + eid: string, + dto: V2BaseQueryDto & { timeBucket?: TimeBucketType }, + ): Promise> { + const dataType: V2DataType = 'errors' + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + }) + + const details = await this.analyticsService.getErrorDetails( + pid, + eid, + timeframe.safeTimezone, + timeframe.groupFromUTC, + timeframe.groupToUTC, + timeframe.timeBucket, + ) + + return envelope(details, { + ...this.baseMeta(pid, timeframe, []), + eid, + timeBucket: timeframe.timeBucket, + }) + } + + async getErrorSessions( + pid: string, + eid: string, + dto: V2BaseQueryDto & { limit?: number; offset?: number }, + ): Promise[]>> { + const dataType: V2DataType = 'errors' + const filters = this.prepareFilters(dto.filters, dataType, true) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const limit = dto.limit ?? 10 + const offset = dto.offset ?? 0 + + const result = (await this.analyticsService.getErrorAffectedSessions( + pid, + eid, + timeframe.groupFromUTC, + timeframe.groupToUTC, + limit, + offset, + filters.filtersQuery, + filters.filtersParams, + )) as { sessions?: unknown } | Record[] + + const sessions = Array.isArray(result) + ? result + : ((result?.sessions ?? []) as Record[]) + + return envelope(renameEntityList(sessions), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + eid, + limit, + offset, + }) + } + + async getSessionsList( + pid: string, + dto: V2BaseQueryDto & { + limit?: number + offset?: number + event_type?: 'traffic' | 'performance' | 'error' + }, + ): Promise[]>> { + const eventType = dto.event_type || 'traffic' + const dataType: V2DataType = + eventType === 'performance' + ? 'performance' + : eventType === 'error' + ? 'errors' + : 'traffic' + + const filters = this.prepareFilters( + dto.filters, + dataType, + eventType !== 'traffic', + ) + + const timeframe = await this.resolveTimeframe({ + pid, + dataType: 'traffic', + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + allTimeEventTypes: + eventType === 'traffic' + ? ['pageview', 'custom_event', 'error'] + : [eventType], + }) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + const sessions = await this.analyticsService.getSessionsList( + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + limit, + offset, + filters.customEVFilterApplied, + eventType, + ) + + return envelope(renameEntityList(sessions), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + eventType, + limit, + offset, + }) + } + + async getSessionDetails( + pid: string, + psid: string, + timezone?: string, + ): Promise>> { + const safeTimezone = this.analyticsService.getSafeTimezone(timezone) + + const result = (await this.analyticsService.getSessionDetails( + pid, + psid, + safeTimezone, + )) as Record + + const details = result?.details as Record | undefined + + return envelope( + { + ...result, + details: details ? renameEntityKeys(details) : details, + }, + { pid, psid, timezone: safeTimezone }, + ) + } + + async getProfilesList( + pid: string, + dto: V2BaseQueryDto & { + limit?: number + offset?: number + profile_type?: 'all' | 'anonymous' | 'identified' + }, + ): Promise[]>> { + const dataType: V2DataType = 'traffic' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + const profiles = await this.analyticsService.getProfilesList( + pid, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + limit, + offset, + dto.profile_type || 'all', + filters.customEVFilterApplied, + ) + + return envelope(renameEntityList(profiles), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + profileType: dto.profile_type || 'all', + limit, + offset, + }) + } + + async getProfileDetails( + pid: string, + profileId: string, + timezone?: string, + ): Promise>> { + const safeTimezone = this.analyticsService.getSafeTimezone(timezone) + + const [details, topPages, activityCalendar] = await Promise.all([ + this.analyticsService.getProfileDetails(pid, profileId, safeTimezone), + this.analyticsService.getProfileTopPages(pid, profileId), + this.analyticsService.getProfileActivityCalendar(pid, profileId), + ]) + + return envelope( + { + ...renameEntityKeys((details ?? {}) as Record), + topPages, + activityCalendar, + }, + { pid, profileId, timezone: safeTimezone }, + ) + } + + async getProfileSessions( + pid: string, + profileId: string, + dto: V2BaseQueryDto & { limit?: number; offset?: number }, + ): Promise[]>> { + const dataType: V2DataType = 'traffic' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + const sessions = await this.analyticsService.getProfileSessionsList( + pid, + profileId, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + limit, + offset, + filters.customEVFilterApplied, + ) + + return envelope(renameEntityList(sessions), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + profileId, + limit, + offset, + }) + } + + async getTrafficCustomEventMetadata( + pid: string, + dto: V2BaseQueryDto & { event: string; timeBucket?: TimeBucketType }, + ): Promise> { + const appliedFilters = parseV2Filters(dto.filters) + const v1FiltersJson = toV1FiltersJson(appliedFilters, 'traffic') + + if (Boolean(dto.from) !== Boolean(dto.to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(dto.from && dto.to) + const from = hasCustomRange ? dto.from : undefined + const to = hasCustomRange ? dto.to : undefined + const period = hasCustomRange + ? undefined + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + const { result } = await this.analyticsService.getCustomEventMetadata({ + pid, + period, + timeBucket: + dto.timeBucket || getLowestPossibleTimeBucket(period, from, to), + from, + to, + filters: v1FiltersJson, + timezone: dto.timezone, + event: dto.event, + } as GetCustomEventMetadata) + + return envelope(result, { + pid, + event: dto.event, + period: period ?? null, + timezone: this.analyticsService.getSafeTimezone(dto.timezone), + appliedFilters, + }) + } + + async getTrafficPagePropertyMetadata( + pid: string, + dto: V2BaseQueryDto & { property: string; timeBucket?: TimeBucketType }, + ): Promise> { + const appliedFilters = parseV2Filters(dto.filters) + const v1FiltersJson = toV1FiltersJson(appliedFilters, 'traffic') + + if (Boolean(dto.from) !== Boolean(dto.to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(dto.from && dto.to) + const from = hasCustomRange ? dto.from : undefined + const to = hasCustomRange ? dto.to : undefined + const period = hasCustomRange + ? undefined + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + const { result } = await this.analyticsService.getPagePropertyMeta({ + pid, + period, + timeBucket: + dto.timeBucket || getLowestPossibleTimeBucket(period, from, to), + from, + to, + filters: v1FiltersJson, + timezone: dto.timezone, + property: dto.property, + } as GetPagePropertyMetaDto) + + return envelope(result, { + pid, + property: dto.property, + period: period ?? null, + timezone: this.analyticsService.getSafeTimezone(dto.timezone), + appliedFilters, + }) + } + + async getTrafficCustomEventsTimeseries( + pid: string, + dto: V2BaseQueryDto & { events: string; timeBucket?: TimeBucketType }, + ): Promise> { + const dataType: V2DataType = 'traffic' + + let events: string[] + try { + const parsed = JSON.parse(dto.events) + events = Array.isArray(parsed) ? parsed.map(String) : [] + } catch { + events = dto.events + .split(',') + .map((event) => event.trim()) + .filter(Boolean) + } + + events = events.filter((event) => event && event !== '__proto__') + + if (events.length === 0) { + throw new UnprocessableEntityException( + 'At least one custom event name has to be provided', + ) + } + + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + allTimeEventTypes: ['custom_event'], + }) + + const result = (await this.analyticsService.groupCustomEVByTimeBucket( + timeframe.timeBucket, + timeframe.groupFrom, + timeframe.groupTo, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + events, + )) as { chart: { x: string[]; events: Record } } + + const series: Record = {} + + for (const event of events) { + series[event] = result.chart.events?.[event] ?? [] + } + + return envelope( + zipTimeseries(result.chart.x, series, timeframe.safeTimezone), + { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: timeframe.allowedTimeBuckets, + events, + }, + ) + } + + async getFunnel( + pid: string, + dto: V2BaseQueryDto & { steps?: string; funnelId?: string }, + ): Promise> { + const steps = await this.resolveFunnelSteps(pid, dto) + + const { params, filters, timeframe } = await this.resolveFunnelContext( + pid, + dto, + ) + + let funnel: IFunnel[] = [] + let totalPageviews = 0 + let timeToConvert: unknown + let stepDetails: Record>> = { + countries: {}, + devices: {}, + browsers: {}, + sources: {}, + campaigns: {}, + pages: {}, + profileTypes: {}, + } + + await Promise.all([ + (async () => { + funnel = await this.analyticsService.getFunnel( + steps, + params, + filters.filtersQuery, + ) + })(), + (async () => { + totalPageviews = await this.analyticsService.getTotalPageviews( + pid, + params.groupFrom as string, + params.groupTo as string, + filters.filtersQuery, + filters.filtersParams, + ) + })(), + (async () => { + try { + stepDetails = await this.analyticsService.getFunnelStepDetails( + steps, + params, + filters.filtersQuery, + ) + } catch (reason) { + this.logger.log(reason, 'AnalyticsV2Service -> getFunnelStepDetails') + } + })(), + (async () => { + timeToConvert = await this.analyticsService.getFunnelTimeToConvert( + steps, + params, + filters.filtersQuery, + ) + })(), + ]) + + for (let i = 0; i < funnel.length; i++) { + funnel[i].breakdowns = { + countries: stepDetails.countries[i + 1] || {}, + devices: stepDetails.devices[i + 1] || {}, + browsers: stepDetails.browsers[i + 1] || {}, + sources: stepDetails.sources[i + 1] || {}, + campaigns: stepDetails.campaigns[i + 1] || {}, + pages: stepDetails.pages[i + 1] || {}, + profileTypes: stepDetails.profileTypes[i + 1] || {}, + } + } + + return envelope( + { steps: funnel, totalPageviews, timeToConvert }, + this.baseMeta(pid, timeframe, filters.appliedFilters), + ) + } + + async getFunnelSessions( + pid: string, + dto: V2BaseQueryDto & { + steps?: string + funnelId?: string + step: number + dropoff?: boolean + limit?: number + offset?: number + }, + ): Promise[]>> { + const steps = await this.resolveFunnelSteps(pid, dto) + + if (dto.step < 1 || dto.step > steps.length) { + throw new BadRequestException( + 'Step must be between 1 and the number of funnel steps', + ) + } + + const { params, filters, timeframe } = await this.resolveFunnelContext( + pid, + dto, + ) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + const sessions = await this.analyticsService.getFunnelSessionsList( + steps, + params, + timeframe.safeTimezone, + dto.step, + limit, + offset, + filters.filtersQuery, + Boolean(dto.dropoff), + ) + + return envelope(renameEntityList(sessions), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + step: dto.step, + dropoff: Boolean(dto.dropoff), + limit, + offset, + }) + } + + private async resolveFunnelSteps( + pid: string, + dto: { steps?: string; funnelId?: string }, + ): Promise { + if (dto.steps && dto.funnelId) { + throw new BadRequestException( + "The 'steps' and 'funnelId' parameters are mutually exclusive", + ) + } + + return this.analyticsService.getPagesArray(dto.steps, dto.funnelId, pid) + } + + private async resolveFunnelContext( + pid: string, + dto: V2BaseQueryDto, + ): Promise<{ + params: Record + filters: PreparedFilters + timeframe: ResolvedTimeframe + }> { + const filters = this.prepareFilters(dto.filters, 'traffic') + const safeTimezone = this.analyticsService.getSafeTimezone(dto.timezone) + + if (Boolean(dto.from) !== Boolean(dto.to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(dto.from && dto.to) + const period = hasCustomRange + ? null + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + let diff: number | undefined + + if (period === 'all') { + const [pageviewRes, customEventRes] = await Promise.all([ + this.analyticsService.calculateTimeBucketForAllTime(pid, 'pageview'), + this.analyticsService.calculateTimeBucketForAllTime( + pid, + 'custom_event', + ), + ]) + + diff = Math.max(pageviewRes.diff, customEventRes.diff) + } + + const { groupFrom, groupTo, groupFromUTC, groupToUTC } = + this.analyticsService.getGroupFromTo( + hasCustomRange ? dto.from : undefined, + hasCustomRange ? dto.to : undefined, + null, + period ?? undefined, + safeTimezone, + diff, + ) + + return { + params: { + pid, + groupFrom, + groupTo, + ...filters.filtersParams, + }, + filters, + timeframe: { + period, + timeBucket: null, + allowedTimeBuckets: null, + safeTimezone, + groupFrom, + groupTo, + groupFromUTC, + groupToUTC, + }, + } + } + + async getLiveVisitors( + pid: string, + ): Promise> { + const since = dayjs + .utc() + .subtract(ONLINE_VISITORS_WINDOW_MINUTES, 'minute') + .format('YYYY-MM-DD HH:mm:ss') + + const query = ` + SELECT + dv, + br, + os, + cc, + psidCasted AS psid + FROM ( + SELECT + any(dv) AS dv, + any(br) AS br, + any(os) AS os, + any(cc) AS cc, + toString(psid) AS psidCasted + FROM events + WHERE + pid = {pid:FixedString(12)} + AND type IN ('pageview', 'custom_event') + AND created >= {since:DateTime} + AND psid IS NOT NULL + AND psid != 0 + GROUP BY psid + ) + ` + + const [{ data }, count] = await Promise.all([ + clickhouse + .query({ query, query_params: { pid, since } }) + .then((resultSet) => resultSet.json>()), + this.analyticsService.getOnlineUserCount(pid), + ]) + + return envelope( + { count, visitors: renameEntityList(data) }, + { pid, windowMinutes: ONLINE_VISITORS_WINDOW_MINUTES }, + ) + } + + getDimensions(type: V2DataType): V2Envelope<{ + dimensions: Record[] + metrics: Record[] + }> { + const dimensions = listDimensions(type).map((dimension) => ({ + name: dimension.api, + description: dimension.description, + filterOnly: Boolean(dimension.filterOnly), + extraFields: (dimension.extraFields ?? []).map((field) => field.api), + })) + + const metrics = listMetrics(type).map((metric) => ({ + name: metric.api, + description: metric.description, + format: metric.format ?? 'integer', + default: Boolean(metric.isDefault), + })) + + return envelope({ dimensions, metrics }, { type }) + } + + async getDimensionValues( + pid: string, + dimensionApi: string, + type: 'traffic' | 'errors', + ): Promise> { + const dimension = findDimension( + dimensionApi, + type === 'errors' ? 'errors' : 'traffic', + ) + + if (!dimension) { + throw new UnprocessableEntityException( + `Unknown ${type} dimension '${dimensionApi}'`, + ) + } + + if (dimension.column === 'brv' || dimension.column === 'osv') { + const values = await this.analyticsService.getVersionFilters( + pid, + type, + dimension.column === 'brv' ? 'br' : 'os', + ) + + return envelope(values, { pid, dimension: dimensionApi, type }) + } + + const values = + type === 'errors' && !dimension.virtual + ? await this.analyticsService.getErrorsFilters(pid, dimension.column) + : await this.analyticsService.getFilters(pid, dimension.column) + + return envelope(values, { pid, dimension: dimensionApi, type }) + } + + validateMetric(api: string, dataType: V2DataType): V2MetricDef { + return getMetric(api, dataType) + } +} diff --git a/backend/apps/cloud/src/analytics/v2/controllers/captcha-v2.controller.ts b/backend/apps/cloud/src/analytics/v2/controllers/captcha-v2.controller.ts new file mode 100644 index 000000000..b2e0c313b --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/controllers/captcha-v2.controller.ts @@ -0,0 +1,125 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2BreakdownDto, + V2ProjectParamsDto, + V2SummaryDto, + V2TimeseriesDto, +} from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/captcha') +export class CaptchaV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get('summary') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Captcha summary', + description: + 'Captcha counters, pass rate, solve-time quantiles, and difficulty/solve-time distributions for the selected period, plus the previous period for comparison.', + }) + async getSummary( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SummaryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/captcha/summary') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getCaptchaSummary(pid, query) + } + + @Get('timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Captcha timeseries', + description: + 'Captcha counters (generated, passed, failed, validation_failed, replayed) grouped by time bucket.', + }) + async getTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2TimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/captcha/timeseries') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getCaptchaTimeseries(pid, query) + } + + @Get('breakdown') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Captcha breakdown', + description: + 'Captcha events grouped by a single dimension (country, browser, os, device, captcha_event, captcha_difficulty, captcha_reason, solve_time).', + }) + async getBreakdown( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2BreakdownDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${query.dimension}`, + 'GET /v2/projects/:pid/captcha/breakdown', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getBreakdown(pid, query, 'captcha') + } +} diff --git a/backend/apps/cloud/src/analytics/v2/controllers/errors-v2.controller.ts b/backend/apps/cloud/src/analytics/v2/controllers/errors-v2.controller.ts new file mode 100644 index 000000000..902f3bdd8 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/controllers/errors-v2.controller.ts @@ -0,0 +1,207 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2ErrorDetailQueryDto, + V2ErrorParamsDto, + V2ErrorSessionsQueryDto, + V2ErrorsQueryDto, +} from '../dto/entities.dto' +import { + V2BreakdownDto, + V2ProjectParamsDto, + V2TimeseriesDto, +} from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/errors') +export class ErrorsV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get() + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Error groups', + description: + 'Paginated list of error groups (grouped by error ID) with occurrence counts, affected users and status.', + }) + async getErrors( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2ErrorsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/errors') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getErrorsList(pid, query) + } + + @Get('overview') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Errors overview', + description: + 'Aggregated error statistics: occurrences, affected users and sessions for the selected period.', + }) + async getOverview( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2ErrorsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/errors/overview') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getErrorsOverview(pid, query) + } + + @Get('timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Errors timeseries', + description: 'Error occurrences and affected users grouped by time bucket.', + }) + async getTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2TimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/errors/timeseries') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getErrorsTimeseries(pid, query) + } + + @Get('breakdown') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Errors breakdown', + description: + 'Error occurrences grouped by a single dimension (page, browser, os, country, ...).', + }) + async getBreakdown( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2BreakdownDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${query.dimension}`, + 'GET /v2/projects/:pid/errors/breakdown', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getBreakdown(pid, query, 'errors') + } + + @Get(':eid') + @Auth(true, true) + @ApiOperation({ + summary: 'Error details', + description: + 'Details for a single error group: metadata, occurrence chart and affected user counts.', + }) + async getErrorDetails( + @Param() { pid, eid }: V2ErrorParamsDto, + @Query() query: V2ErrorDetailQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, eid: ${eid}`, + 'GET /v2/projects/:pid/errors/:eid', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getErrorDetails(pid, eid, query) + } + + @Get(':eid/sessions') + @Auth(true, true) + @ApiOperation({ + summary: 'Error sessions', + description: 'Paginated sessions affected by a specific error group.', + }) + async getErrorSessions( + @Param() { pid, eid }: V2ErrorParamsDto, + @Query() query: V2ErrorSessionsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, eid: ${eid}`, + 'GET /v2/projects/:pid/errors/:eid/sessions', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getErrorSessions(pid, eid, query) + } +} diff --git a/backend/apps/cloud/src/analytics/v2/controllers/performance-v2.controller.ts b/backend/apps/cloud/src/analytics/v2/controllers/performance-v2.controller.ts new file mode 100644 index 000000000..664f8d91e --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/controllers/performance-v2.controller.ts @@ -0,0 +1,134 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { PerfMeasure } from '../../interfaces' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2PerfBreakdownDto, + V2PerfSummaryDto, + V2PerfTimeseriesDto, +} from '../dto/performance.dto' +import { V2ProjectParamsDto } from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/performance') +export class PerformanceV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get('summary') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Performance summary', + description: + 'Aggregated frontend/network/backend timings for the selected period, the previous period, and the change between them. Values are in seconds.', + }) + async getSummary( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2PerfSummaryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/performance/summary') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getPerformanceSummary(pid, query) + } + + @Get('timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Performance timeseries', + description: + 'Performance timings grouped by time bucket. With measure=quantiles, returns p50/p75/p95 of the total load time instead of individual timings. Values are in seconds.', + }) + async getTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2PerfTimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, measure: ${query.measure}`, + 'GET /v2/projects/:pid/performance/timeseries', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getPerformanceTimeseries(pid, query) + } + + @Get('breakdown') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Performance breakdown', + description: + 'Performance timings grouped by a single dimension (country, page, browser, ...), paginated and sortable. Values are in seconds.', + }) + async getBreakdown( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2PerfBreakdownDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${query.dimension}, measure: ${query.measure}`, + 'GET /v2/projects/:pid/performance/breakdown', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getBreakdown( + pid, + query, + 'performance', + (query.measure || 'median') as PerfMeasure, + ) + } +} diff --git a/backend/apps/cloud/src/analytics/v2/controllers/profiles-v2.controller.ts b/backend/apps/cloud/src/analytics/v2/controllers/profiles-v2.controller.ts new file mode 100644 index 000000000..7b4e29f96 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/controllers/profiles-v2.controller.ts @@ -0,0 +1,129 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2ProfileParamsDto, + V2ProfilesQueryDto, + V2TimezoneQueryDto, +} from '../dto/entities.dto' +import { V2ListQueryDto, V2ProjectParamsDto } from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/profiles') +export class ProfilesV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get() + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Profiles', + description: + 'Paginated list of user profiles (anonymous and identified visitors).', + }) + async getProfiles( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2ProfilesQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/profiles') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getProfilesList(pid, query) + } + + @Get(':profileId') + @Auth(true, true) + @ApiOperation({ + summary: 'Profile details', + description: + 'Details for a single profile: attributes, top pages and activity calendar.', + }) + async getProfile( + @Param() { pid, profileId }: V2ProfileParamsDto, + @Query() query: V2TimezoneQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, profileId: ${profileId}`, + 'GET /v2/projects/:pid/profiles/:profileId', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getProfileDetails( + pid, + profileId, + query.timezone, + ) + } + + @Get(':profileId/sessions') + @Auth(true, true) + @ApiOperation({ + summary: 'Profile sessions', + description: 'Paginated sessions recorded for a specific profile.', + }) + async getProfileSessions( + @Param() { pid, profileId }: V2ProfileParamsDto, + @Query() query: V2ListQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, profileId: ${profileId}`, + 'GET /v2/projects/:pid/profiles/:profileId/sessions', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getProfileSessions(pid, profileId, query) + } +} diff --git a/backend/apps/cloud/src/analytics/v2/controllers/project-v2.controller.ts b/backend/apps/cloud/src/analytics/v2/controllers/project-v2.controller.ts new file mode 100644 index 000000000..bb3a81ff2 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/controllers/project-v2.controller.ts @@ -0,0 +1,188 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2DimensionsQueryDto, + V2DimensionValuesParamsDto, + V2DimensionValuesQueryDto, + V2FunnelQueryDto, + V2FunnelSessionsQueryDto, +} from '../dto/project.dto' +import { V2ProjectParamsDto } from '../dto/v2-base.dto' +import { SeoV2Service } from '../seo-v2.service' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid') +export class ProjectV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly seoV2Service: SeoV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get('funnel') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Funnel analysis', + description: + 'Funnel step conversion for a saved funnel (funnelId) or an ad-hoc list of steps, with per-step breakdowns and time-to-convert stats.', + }) + async getFunnel( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2FunnelQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/funnel') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getFunnel(pid, query) + } + + @Get('funnel/sessions') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Funnel sessions', + description: + 'Paginated sessions that reached (or dropped off at) a specific funnel step.', + }) + async getFunnelSessions( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2FunnelSessionsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, step: ${query.step}`, + 'GET /v2/projects/:pid/funnel/sessions', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getFunnelSessions(pid, query) + } + + @Get('live-visitors') + @Auth(true, true) + @ApiOperation({ + summary: 'Live visitors', + description: + 'Current online visitor count and the list of active visitors (last 5 minutes).', + }) + async getLiveVisitors( + @Param() { pid }: V2ProjectParamsDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/live-visitors') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getLiveVisitors(pid) + } + + @Get('dimensions') + @Auth(true, true) + @ApiOperation({ + summary: 'Dimension & metric discovery', + description: + 'Machine-readable list of the dimensions and metrics available for a data type — useful for building custom dashboards.', + }) + async getDimensions( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2DimensionsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/dimensions') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + // SEO is served by the Search Console API rather than ClickHouse, so it + // keeps its own registry outside the sqlExpr-based one. + if (query.type === 'seo') { + return this.seoV2Service.getDimensions() + } + + return this.analyticsV2Service.getDimensions(query.type || 'traffic') + } + + @Get('dimensions/:dimension/values') + @Auth(true, true) + @ApiOperation({ + summary: 'Dimension values', + description: + 'Distinct recorded values for a dimension — useful for filter autocompletion. browser_version and os_version return { name, version } pairs.', + }) + async getDimensionValues( + @Param() { pid, dimension }: V2DimensionValuesParamsDto, + @Query() query: V2DimensionValuesQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${dimension}`, + 'GET /v2/projects/:pid/dimensions/:dimension/values', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getDimensionValues( + pid, + dimension, + query.type || 'traffic', + ) + } +} diff --git a/backend/apps/cloud/src/analytics/v2/controllers/seo-v2.controller.ts b/backend/apps/cloud/src/analytics/v2/controllers/seo-v2.controller.ts new file mode 100644 index 000000000..85ee82f0c --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/controllers/seo-v2.controller.ts @@ -0,0 +1,202 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2SeoBreakdownDto, + V2SeoRangeDto, + V2SeoSummaryDto, + V2SeoTimeseriesDto, +} from '../dto/seo.dto' +import { V2ProjectParamsDto } from '../dto/v2-base.dto' +import { SeoV2Service } from '../seo-v2.service' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/seo') +export class SeoV2Controller { + constructor( + private readonly seoV2Service: SeoV2Service, + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get('status') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Search Console connection status', + description: + 'Whether Google Search Console is connected for this project and which property is linked. The other seo endpoints return 409 until both are true.', + }) + async getStatus( + @Param() { pid }: V2ProjectParamsDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/seo/status') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getStatus(pid) + } + + @Get('summary') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'SEO summary', + description: + 'Clicks, impressions, CTR and average position for the selected period, the previous period of the same length, and the change between them.', + }) + async getSummary( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SeoSummaryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/seo/summary') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getSummary(pid, query) + } + + @Get('timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'SEO timeseries', + description: + 'Selected SEO metrics grouped by time bucket, as rows of objects with ISO-8601 timestamps.', + }) + async getTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SeoTimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/seo/timeseries') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getTimeseries(pid, query) + } + + @Get('breakdown') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'SEO breakdown', + description: + 'SEO metrics grouped by a single dimension (query, page, country, device), paginated. Rows are always ordered by clicks descending.', + }) + async getBreakdown( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SeoBreakdownDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${query.dimension}`, + 'GET /v2/projects/:pid/seo/breakdown', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getBreakdown(pid, query) + } + + @Get('branded-traffic') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Branded vs non-branded traffic', + description: + "Clicks split by whether the search query contained one of the project's brand keywords. Skipped for ranges wider than 31 days.", + }) + async getBrandedTraffic( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SeoRangeDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/seo/branded-traffic') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getBrandedTraffic(pid, query) + } + + @Get('positions') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Search position analytics', + description: + 'Impressions bucketed by search position, and the daily count of ranking queries per position bucket. Skipped for ranges wider than 31 days.', + }) + async getPositions( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SeoRangeDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/seo/positions') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getPositions(pid, query) + } +} diff --git a/backend/apps/cloud/src/analytics/v2/controllers/sessions-v2.controller.ts b/backend/apps/cloud/src/analytics/v2/controllers/sessions-v2.controller.ts new file mode 100644 index 000000000..9870e3744 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/controllers/sessions-v2.controller.ts @@ -0,0 +1,99 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2SessionParamsDto, + V2SessionsQueryDto, + V2TimezoneQueryDto, +} from '../dto/entities.dto' +import { V2ProjectParamsDto } from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/sessions') +export class SessionsV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get() + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Sessions', + description: + 'Paginated list of individual sessions for the selected period.', + }) + async getSessions( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SessionsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/sessions') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getSessionsList(pid, query) + } + + @Get(':psid') + @Auth(true, true) + @ApiOperation({ + summary: 'Session details', + description: + 'Details for a single session: the page/event flow, session attributes and an activity chart.', + }) + async getSession( + @Param() { pid, psid }: V2SessionParamsDto, + @Query() query: V2TimezoneQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, psid: ${psid}`, + 'GET /v2/projects/:pid/sessions/:psid', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getSessionDetails(pid, psid, query.timezone) + } +} diff --git a/backend/apps/cloud/src/analytics/v2/controllers/traffic-v2.controller.ts b/backend/apps/cloud/src/analytics/v2/controllers/traffic-v2.controller.ts new file mode 100644 index 000000000..60c5b40db --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/controllers/traffic-v2.controller.ts @@ -0,0 +1,299 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2CustomEventsTimeseriesDto, + V2CustomMetricsQueryDto, + V2EventMetadataQueryDto, + V2PropertyMetadataQueryDto, +} from '../dto/project.dto' +import { + V2BreakdownDto, + V2ListQueryDto, + V2ProjectParamsDto, + V2SummaryDto, + V2TimeseriesDto, +} from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/traffic') +export class TrafficV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get('summary') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Traffic summary', + description: + 'Key traffic metrics for the selected period, the previous period of the same length, and the change between them.', + }) + async getSummary( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SummaryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/traffic/summary') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficSummary(pid, query) + } + + @Get('timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Traffic timeseries', + description: + 'Selected traffic metrics grouped by time bucket, as rows of objects with ISO-8601 timestamps.', + }) + async getTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2TimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/traffic/timeseries') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficTimeseries(pid, query) + } + + @Get('breakdown') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Traffic breakdown', + description: + 'Selected traffic metrics grouped by a single dimension (country, page, browser, ...), paginated and sortable.', + }) + async getBreakdown( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2BreakdownDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${query.dimension}`, + 'GET /v2/projects/:pid/traffic/breakdown', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getBreakdown(pid, query, 'traffic') + } + + @Get('custom-events') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Custom events', + description: + 'Counts of custom events recorded in the selected period, sorted by count.', + }) + async getCustomEvents( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2ListQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}`, + 'GET /v2/projects/:pid/traffic/custom-events', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficCustomEvents(pid, query) + } + + @Get('custom-metrics') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Custom metrics', + description: + 'Sum / average of numeric custom event metadata values for the selected period and the previous one (project view custom metrics).', + }) + async getCustomMetrics( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2CustomMetricsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}`, + 'GET /v2/projects/:pid/traffic/custom-metrics', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficCustomMetrics(pid, query) + } + + @Get('custom-events/timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Custom events timeseries', + description: + 'Occurrence counts for the selected custom events grouped by time bucket.', + }) + async getCustomEventsTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2CustomEventsTimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, events: ${query.events}`, + 'GET /v2/projects/:pid/traffic/custom-events/timeseries', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficCustomEventsTimeseries(pid, query) + } + + @Get('custom-events/metadata') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Custom event metadata', + description: + 'Aggregated metadata key/value counts for a specific custom event.', + }) + async getCustomEventMetadata( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2EventMetadataQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, event: ${query.event}`, + 'GET /v2/projects/:pid/traffic/custom-events/metadata', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficCustomEventMetadata(pid, query) + } + + @Get('page-properties/metadata') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Page property values', + description: 'Aggregated value counts for a specific page property (tag).', + }) + async getPagePropertyMetadata( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2PropertyMetadataQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, property: ${query.property}`, + 'GET /v2/projects/:pid/traffic/page-properties/metadata', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficPagePropertyMetadata(pid, query) + } + + @Get('page-properties') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Page properties', + description: + 'Counts of pageview metadata (page property) keys recorded in the selected period.', + }) + async getPageProperties( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2ListQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}`, + 'GET /v2/projects/:pid/traffic/page-properties', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficPageProperties(pid, query) + } +} diff --git a/backend/apps/cloud/src/analytics/v2/dto/entities.dto.ts b/backend/apps/cloud/src/analytics/v2/dto/entities.dto.ts new file mode 100644 index 000000000..b8579e767 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/dto/entities.dto.ts @@ -0,0 +1,149 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Transform, Type } from 'class-transformer' +import { + IsBoolean, + IsEnum, + IsIn, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Max, + Min, +} from 'class-validator' + +import { TimeBucketType } from '../../dto/getData.dto' +import { V2BaseQueryDto, V2ProjectParamsDto } from './v2-base.dto' + +export const V2_MAX_ENTITY_LIMIT = 150 + +class V2EntityListDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + default: 30, + maximum: V2_MAX_ENTITY_LIMIT, + description: 'Number of rows to return', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(V2_MAX_ENTITY_LIMIT) + limit?: number + + @ApiProperty({ + required: false, + default: 0, + description: 'Number of rows to skip (pagination)', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number +} + +export class V2SessionsQueryDto extends V2EntityListDto { + @ApiProperty({ + required: false, + enum: ['traffic', 'performance', 'error'], + default: 'traffic', + description: + 'Event scope for the returned sessions: sessions with traffic events (default), performance events, or errors in the selected range.', + }) + @IsOptional() + @IsIn(['traffic', 'performance', 'error'], { + message: 'The provided event_type is incorrect', + }) + event_type?: 'traffic' | 'performance' | 'error' +} + +export class V2ProfilesQueryDto extends V2EntityListDto { + @ApiProperty({ + required: false, + enum: ['all', 'anonymous', 'identified'], + default: 'all', + description: 'Filter profiles by type', + }) + @IsOptional() + @IsIn(['all', 'anonymous', 'identified'], { + message: 'The provided profile_type is incorrect', + }) + profile_type?: 'all' | 'anonymous' | 'identified' +} + +export class V2ErrorsQueryDto extends V2EntityListDto { + @ApiProperty({ + required: false, + default: false, + description: 'Include resolved error groups in the list', + }) + @IsOptional() + @Transform(({ value }) => { + if (value === true || value === 'true') return true + if (value === false || value === 'false') return false + return value + }) + @IsBoolean() + show_resolved?: boolean +} + +export class V2ErrorDetailQueryDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + enum: TimeBucketType, + description: + 'Time bucket for the error occurrence chart. Defaults to the lowest bucket allowed for the requested range.', + }) + @IsOptional() + @IsEnum(TimeBucketType, { message: 'The provided timeBucket is incorrect' }) + timeBucket?: TimeBucketType +} + +export class V2ErrorSessionsQueryDto extends V2BaseQueryDto { + @ApiProperty({ required: false, default: 10, maximum: 50 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(50) + limit?: number + + @ApiProperty({ required: false, default: 0 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number +} + +export class V2TimezoneQueryDto { + @ApiProperty({ + required: false, + description: 'IANA timezone used for displayed dates', + }) + @IsOptional() + @IsString() + timezone?: string +} + +export class V2SessionParamsDto extends V2ProjectParamsDto { + @ApiProperty({ description: 'The session identifier' }) + @IsNotEmpty() + @IsString() + psid: string +} + +export class V2ProfileParamsDto extends V2ProjectParamsDto { + @ApiProperty({ description: 'The profile identifier' }) + @IsNotEmpty() + @IsString() + profileId: string +} + +export class V2ErrorParamsDto extends V2ProjectParamsDto { + @ApiProperty({ description: 'The error group identifier (eid)' }) + @IsNotEmpty() + @IsString() + eid: string +} diff --git a/backend/apps/cloud/src/analytics/v2/dto/performance.dto.ts b/backend/apps/cloud/src/analytics/v2/dto/performance.dto.ts new file mode 100644 index 000000000..acb7c5dd7 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/dto/performance.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsIn, IsOptional } from 'class-validator' + +import { V2BreakdownDto, V2SummaryDto, V2TimeseriesDto } from './v2-base.dto' + +const MEASURE_VALUES = ['median', 'average', 'p75', 'p95', 'quantiles'] + +const MeasureApiProperty = () => + ApiProperty({ + required: false, + enum: MEASURE_VALUES, + default: 'median', + description: + "Aggregate function applied to performance timings. 'quantiles' is only meaningful on the timeseries endpoint (returns p50/p75/p95 of the total load time); other endpoints treat it as 'median'.", + }) + +export class V2PerfTimeseriesDto extends V2TimeseriesDto { + @MeasureApiProperty() + @IsOptional() + @IsIn(MEASURE_VALUES, { message: 'The provided measure is incorrect' }) + measure?: string +} + +export class V2PerfSummaryDto extends V2SummaryDto { + @MeasureApiProperty() + @IsOptional() + @IsIn(MEASURE_VALUES, { message: 'The provided measure is incorrect' }) + measure?: string +} + +export class V2PerfBreakdownDto extends V2BreakdownDto { + @MeasureApiProperty() + @IsOptional() + @IsIn(MEASURE_VALUES, { message: 'The provided measure is incorrect' }) + measure?: string +} diff --git a/backend/apps/cloud/src/analytics/v2/dto/project.dto.ts b/backend/apps/cloud/src/analytics/v2/dto/project.dto.ts new file mode 100644 index 000000000..e3cbc8dc5 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/dto/project.dto.ts @@ -0,0 +1,158 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Transform, Type } from 'class-transformer' +import { + IsBoolean, + IsEnum, + IsIn, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Max, + Min, +} from 'class-validator' + +import { TimeBucketType } from '../../dto/getData.dto' +import { V2_MAX_ENTITY_LIMIT } from './entities.dto' +import { V2BaseQueryDto, V2ProjectParamsDto } from './v2-base.dto' + +export class V2FunnelQueryDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + description: + 'JSON array of page paths / custom event names defining the funnel steps. Mutually exclusive with funnelId.', + example: '["/","/pricing","/signup"]', + }) + @IsOptional() + @IsString() + steps?: string + + @ApiProperty({ + required: false, + description: 'ID of a saved funnel. Mutually exclusive with steps.', + }) + @IsOptional() + @IsString() + funnelId?: string +} + +export class V2FunnelSessionsQueryDto extends V2FunnelQueryDto { + @ApiProperty({ + description: + 'Funnel step number the sessions must have reached (1-indexed)', + example: 2, + }) + @Type(() => Number) + @IsInt() + @Min(1) + step: number + + @ApiProperty({ + required: false, + default: false, + description: + 'Return sessions that reached the requested step but did not continue to the next one', + }) + @IsOptional() + @Transform(({ value }) => value === true || value === 'true') + @IsBoolean() + dropoff?: boolean + + @ApiProperty({ required: false, default: 30, maximum: V2_MAX_ENTITY_LIMIT }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(V2_MAX_ENTITY_LIMIT) + limit?: number + + @ApiProperty({ required: false, default: 0 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number +} + +export class V2DimensionsQueryDto { + @ApiProperty({ + required: false, + enum: ['traffic', 'performance', 'captcha', 'errors', 'seo'], + default: 'traffic', + description: 'Data type to list dimensions and metrics for', + }) + @IsOptional() + @IsIn(['traffic', 'performance', 'captcha', 'errors', 'seo'], { + message: 'The provided type is incorrect', + }) + type?: 'traffic' | 'performance' | 'captcha' | 'errors' | 'seo' +} + +export class V2DimensionValuesParamsDto extends V2ProjectParamsDto { + @ApiProperty({ description: "Dimension name, e.g. 'browser'" }) + @IsNotEmpty() + @IsString() + dimension: string +} + +export class V2DimensionValuesQueryDto { + @ApiProperty({ + required: false, + enum: ['traffic', 'errors'], + default: 'traffic', + description: 'Data type to list dimension values for', + }) + @IsOptional() + @IsIn(['traffic', 'errors'], { message: 'The provided type is incorrect' }) + type?: 'traffic' | 'errors' +} + +export class V2CustomMetricsQueryDto extends V2BaseQueryDto { + @ApiProperty({ + description: + 'JSON array of custom metric definitions (project view custom events)', + }) + @IsNotEmpty() + @IsString() + metrics: string +} + +export class V2EventMetadataQueryDto extends V2BaseQueryDto { + @ApiProperty({ description: 'Custom event name to return metadata for' }) + @IsNotEmpty() + @IsString() + event: string + + @ApiProperty({ required: false, enum: TimeBucketType }) + @IsOptional() + @IsEnum(TimeBucketType, { message: 'The provided timeBucket is incorrect' }) + timeBucket?: TimeBucketType +} + +export class V2PropertyMetadataQueryDto extends V2BaseQueryDto { + @ApiProperty({ description: 'Page property (tag) name to return values for' }) + @IsNotEmpty() + @IsString() + property: string + + @ApiProperty({ required: false, enum: TimeBucketType }) + @IsOptional() + @IsEnum(TimeBucketType, { message: 'The provided timeBucket is incorrect' }) + timeBucket?: TimeBucketType +} + +export class V2CustomEventsTimeseriesDto extends V2BaseQueryDto { + @ApiProperty({ + description: + 'Custom event names to chart: a comma-separated list or a JSON array', + example: 'signup,purchase', + }) + @IsNotEmpty() + @IsString() + events: string + + @ApiProperty({ required: false, enum: TimeBucketType }) + @IsOptional() + @IsEnum(TimeBucketType, { message: 'The provided timeBucket is incorrect' }) + timeBucket?: TimeBucketType +} diff --git a/backend/apps/cloud/src/analytics/v2/dto/seo.dto.ts b/backend/apps/cloud/src/analytics/v2/dto/seo.dto.ts new file mode 100644 index 000000000..f8153b33a --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/dto/seo.dto.ts @@ -0,0 +1,115 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { + IsIn, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Max, + Min, +} from 'class-validator' + +import { TimeBucketType } from '../../dto/getData.dto' +import { SEO_SORT } from '../registry/seo' +import { V2BaseQueryDto } from './v2-base.dto' + +const V2_SEO_MAX_BREAKDOWN_LIMIT = 100 + +/** + * Search Console reports nothing below hourly granularity, so `minute` is + * excluded from the buckets a caller may ask for. + */ +export const V2_SEO_TIME_BUCKETS = [ + TimeBucketType.HOUR, + TimeBucketType.DAY, + TimeBucketType.MONTH, + TimeBucketType.YEAR, +] as const + +export type V2SeoTimeBucket = (typeof V2_SEO_TIME_BUCKETS)[number] + +export class V2SeoSummaryDto extends V2BaseQueryDto {} + +export class V2SeoRangeDto extends V2BaseQueryDto {} + +export class V2SeoTimeseriesDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + enum: V2_SEO_TIME_BUCKETS, + description: + "Time bucket for grouping. One of: hour, day, month, year. Defaults to 'day', or 'hour' for intraday periods.", + }) + @IsOptional() + @IsIn(V2_SEO_TIME_BUCKETS as readonly string[], { + message: `The provided timeBucket is incorrect. Search Console supports: ${V2_SEO_TIME_BUCKETS.join(', ')}`, + }) + timeBucket?: V2SeoTimeBucket + + @ApiProperty({ + required: false, + description: + 'Comma-separated metric names to return. Defaults to clicks, impressions, ctr and position.', + example: 'clicks,impressions', + }) + @IsOptional() + @IsString() + metrics?: string +} + +export class V2SeoBreakdownDto extends V2BaseQueryDto { + @ApiProperty({ + description: + 'Dimension to break down by. One of: query, page, country, device', + example: 'query', + }) + @IsNotEmpty() + @IsString() + dimension: string + + @ApiProperty({ + required: false, + description: + 'Comma-separated metric names to compute per dimension value. Defaults to clicks, impressions, ctr and position.', + example: 'clicks,impressions', + }) + @IsOptional() + @IsString() + metrics?: string + + @ApiProperty({ + required: false, + default: 30, + maximum: V2_SEO_MAX_BREAKDOWN_LIMIT, + description: 'Number of rows to return', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(V2_SEO_MAX_BREAKDOWN_LIMIT) + limit?: number + + @ApiProperty({ + required: false, + default: 0, + description: 'Number of rows to skip (pagination)', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number + + @ApiProperty({ + required: false, + enum: [SEO_SORT], + default: SEO_SORT, + description: `Search Console orders rows by clicks descending and offers no sort control, so '${SEO_SORT}' is the only accepted value.`, + }) + @IsOptional() + @IsIn([SEO_SORT], { + message: `Search Console always orders rows by clicks descending; omit 'sort' or pass '${SEO_SORT}'`, + }) + sort?: string +} diff --git a/backend/apps/cloud/src/analytics/v2/dto/v2-base.dto.ts b/backend/apps/cloud/src/analytics/v2/dto/v2-base.dto.ts new file mode 100644 index 000000000..15e675fc1 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/dto/v2-base.dto.ts @@ -0,0 +1,197 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { + IsEnum, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Matches, + Max, + Min, +} from 'class-validator' + +import { PID_REGEX } from '../../../common/constants' +import { DEFAULT_TIMEZONE } from '../../../user/entities/user.entity' +import { ValidatePeriod } from '../../decorators/validate-period.decorator' +import { ChartRenderMode, TimeBucketType } from '../../dto/getData.dto' + +export const V2_DEFAULT_PERIOD = '7d' + +export class V2ProjectParamsDto { + @ApiProperty({ + example: 'aUn1quEid-3', + description: 'The project ID', + }) + @IsNotEmpty() + @Matches(PID_REGEX, { message: 'The provided Project ID (pid) is incorrect' }) + pid: string +} + +export class V2BaseQueryDto { + @ApiProperty({ + required: false, + description: + "Predefined reporting period. One of: 1h, today, yesterday, 1d, 7d, 4w, 3M, 12M, 24M, all. Defaults to '7d'. Mutually exclusive with from/to.", + example: '7d', + }) + @IsOptional() + @ValidatePeriod() + period?: string + + @ApiProperty({ + required: false, + description: + 'Custom range start (ISO 8601 date or datetime). Must be used together with `to`.', + example: '2026-06-01', + }) + @IsOptional() + @IsString() + from?: string + + @ApiProperty({ + required: false, + description: + 'Custom range end (ISO 8601 date or datetime). Must be used together with `from`.', + example: '2026-06-30', + }) + @IsOptional() + @IsString() + to?: string + + @ApiProperty({ + required: false, + description: 'IANA timezone used for bucketing and date boundaries', + default: DEFAULT_TIMEZONE, + example: 'Europe/Kyiv', + }) + @IsOptional() + @IsString() + timezone?: string + + @ApiProperty({ + required: false, + description: + 'JSON array of filter objects: [{ "dimension": "country", "operator": "is" | "is_not" | "contains" | "contains_not", "value": "US" | ["US", "DE"] | null, "key": "" }]', + example: '[{"dimension":"country","operator":"is","value":"US"}]', + }) + @IsOptional() + @IsString() + filters?: string +} + +export class V2TimeseriesDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + enum: TimeBucketType, + description: + 'Time bucket for grouping. Defaults to the lowest bucket allowed for the requested range.', + }) + @IsOptional() + @IsEnum(TimeBucketType, { message: 'The provided timeBucket is incorrect' }) + timeBucket?: TimeBucketType + + @ApiProperty({ + required: false, + description: + 'Comma-separated metric names to return. Defaults depend on the data type.', + example: 'visitors,pageviews', + }) + @IsOptional() + @IsString() + metrics?: string + + @ApiProperty({ + required: false, + enum: ChartRenderMode, + default: ChartRenderMode.PERIODICAL, + description: 'Return periodical (per-bucket) or cumulative values', + }) + @IsOptional() + @IsEnum(ChartRenderMode, { message: 'The provided mode is incorrect' }) + mode?: ChartRenderMode +} + +const V2_MAX_BREAKDOWN_LIMIT = 100 + +export class V2BreakdownDto extends V2BaseQueryDto { + @ApiProperty({ + description: "Dimension to break down by, e.g. 'country', 'page'", + example: 'country', + }) + @IsNotEmpty() + @IsString() + dimension: string + + @ApiProperty({ + required: false, + description: + 'Comma-separated metric names to compute per dimension value. Defaults depend on the data type.', + example: 'visitors,pageviews', + }) + @IsOptional() + @IsString() + metrics?: string + + @ApiProperty({ + required: false, + default: 30, + maximum: V2_MAX_BREAKDOWN_LIMIT, + description: 'Number of rows to return', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(V2_MAX_BREAKDOWN_LIMIT) + limit?: number + + @ApiProperty({ + required: false, + default: 0, + description: 'Number of rows to skip (pagination)', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number + + @ApiProperty({ + required: false, + description: + "Sorting as 'field:direction', where field is 'value' or a selected metric and direction is 'asc' or 'desc'. Defaults to the first selected metric, descending.", + example: 'visitors:desc', + }) + @IsOptional() + @IsString() + sort?: string +} + +export class V2SummaryDto extends V2BaseQueryDto {} + +export class V2ListQueryDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + default: 30, + maximum: V2_MAX_BREAKDOWN_LIMIT, + description: 'Number of rows to return', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(V2_MAX_BREAKDOWN_LIMIT) + limit?: number + + @ApiProperty({ + required: false, + default: 0, + description: 'Number of rows to skip (pagination)', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number +} diff --git a/backend/apps/cloud/src/analytics/v2/mappers/entity.mapper.ts b/backend/apps/cloud/src/analytics/v2/mappers/entity.mapper.ts new file mode 100644 index 000000000..c63c303b9 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/mappers/entity.mapper.ts @@ -0,0 +1,47 @@ +const KEY_RENAMES: Record = { + cc: 'country', + rg: 'region', + rgc: 'region_code', + ct: 'city', + lc: 'locale', + dv: 'device', + br: 'browser', + brv: 'browser_version', + os: 'os', + osv: 'os_version', + ref: 'referrer', + so: 'utm_source', + me: 'utm_medium', + ca: 'utm_campaign', + te: 'utm_term', + co: 'utm_content', + isp: 'isp', + og: 'organization', + ut: 'user_type', + ctp: 'connection_type', + sdur: 'duration', +} + +export const renameEntityKeys = >( + entity: T, +): Record => { + const result: Record = {} + + for (const [key, value] of Object.entries(entity)) { + result[KEY_RENAMES[key] ?? key] = value + } + + return result +} + +export const renameEntityList = ( + entities: unknown, +): Record[] => { + if (!Array.isArray(entities)) { + return [] + } + + return entities.map((entity) => + renameEntityKeys(entity as Record), + ) +} diff --git a/backend/apps/cloud/src/analytics/v2/mappers/envelope.ts b/backend/apps/cloud/src/analytics/v2/mappers/envelope.ts new file mode 100644 index 000000000..ca058667a --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/mappers/envelope.ts @@ -0,0 +1,9 @@ +export interface V2Envelope { + data: T + meta: Record +} + +export const envelope = ( + data: T, + meta: Record = {}, +): V2Envelope => ({ data, meta }) diff --git a/backend/apps/cloud/src/analytics/v2/mappers/seo.mapper.ts b/backend/apps/cloud/src/analytics/v2/mappers/seo.mapper.ts new file mode 100644 index 000000000..efd1ab46c --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/mappers/seo.mapper.ts @@ -0,0 +1,221 @@ +import countries from 'i18n-iso-countries' +import dayjs from 'dayjs' + +import { TimeBucketType } from '../../dto/getData.dto' +import { V2SeoGscDimension } from '../registry/seo' +import { toIsoTimestamp, TimeseriesRow } from './timeseries.mapper' + +export interface SeoMetricValues { + clicks: number + impressions: number + ctr: number + position: number +} + +export type SeoDateSeriesEntry = { date: string } & SeoMetricValues + +export interface SeoSummaryData { + current: SeoMetricValues + previous: SeoMetricValues + change: SeoMetricValues +} + +const round = (value: unknown, precision: number): number => { + const number = Number(value) + + if (!Number.isFinite(number)) { + return 0 + } + + const factor = 10 ** precision + + return Math.round(number * factor) / factor +} + +const toInteger = (value: unknown): number => Math.round(Number(value) || 0) + +/** + * Search Console reports countries as lowercase ISO 3166-1 alpha-3, while the + * rest of the v2 surface (and the `cc` column behind the traffic `country` + * dimension) uses uppercase alpha-2. Normalise so a `country` value means the + * same thing whichever data type produced it. + */ +export const gscCountryToAlpha2 = (country: string): string => { + const alpha2 = countries.alpha3ToAlpha2(String(country || '').toUpperCase()) + + return alpha2 || String(country || '').toUpperCase() +} + +const EMPTY_METRICS: SeoMetricValues = { + clicks: 0, + impressions: 0, + ctr: 0, + position: 0, +} + +const mapSeoMetricValues = ( + values: Partial | null | undefined, +): SeoMetricValues => ({ + clicks: toInteger(values?.clicks), + impressions: toInteger(values?.impressions), + ctr: round(values?.ctr, 2), + position: round(values?.position, 2), +}) + +export const mapSeoSummary = ( + current: Partial | null | undefined, + previous: Partial | null | undefined, +): SeoSummaryData => { + const mappedCurrent = mapSeoMetricValues(current) + const mappedPrevious = previous ? mapSeoMetricValues(previous) : EMPTY_METRICS + + return { + current: mappedCurrent, + previous: mappedPrevious, + change: { + clicks: mappedCurrent.clicks - mappedPrevious.clicks, + impressions: mappedCurrent.impressions - mappedPrevious.impressions, + ctr: round(mappedCurrent.ctr - mappedPrevious.ctr, 2), + position: round(mappedCurrent.position - mappedPrevious.position, 2), + }, + } +} + +/** + * A raw row as returned by one of the GSCService per-dimension getters. They + * disagree on the label field, and getKeywords calls its click count `count`. + */ +export interface GscBreakdownRow extends Partial { + name?: string + page?: string + country?: string + device?: string + count?: number +} + +const readRowValue = ( + dimension: V2SeoGscDimension, + row: GscBreakdownRow, +): string => { + if (dimension === 'query') return row.name ?? '' + if (dimension === 'page') return row.page ?? '' + if (dimension === 'country') return gscCountryToAlpha2(row.country ?? '') + + return row.device ?? '' +} + +export const mapSeoBreakdownRows = ( + dimension: V2SeoGscDimension, + rows: GscBreakdownRow[], + metrics: string[], +): Record[] => + rows.map((row) => { + const values = mapSeoMetricValues({ + clicks: row.clicks ?? row.count, + impressions: row.impressions, + ctr: row.ctr, + position: row.position, + }) + + const mapped: Record = { + value: readRowValue(dimension, row), + } + + for (const metric of metrics) { + mapped[metric] = values[metric as keyof SeoMetricValues] + } + + return mapped + }) + +const startOfBucket = (date: string, bucket: TimeBucketType): string => { + const parsed = dayjs(date) + + if (bucket === TimeBucketType.MONTH) { + return parsed.startOf('month').format('YYYY-MM-DD') + } + + if (bucket === TimeBucketType.YEAR) { + return parsed.startOf('year').format('YYYY-MM-DD') + } + + return date +} + +/** + * Search Console only serves hourly and daily rows, so wider buckets are rolled + * up here. Clicks and impressions sum; ctr is recomputed from the totals and + * position is averaged weighted by impressions, since neither is additive. + */ +export const bucketSeoDateSeries = ( + series: SeoDateSeriesEntry[], + bucket: TimeBucketType, +): SeoDateSeriesEntry[] => { + if (bucket !== TimeBucketType.MONTH && bucket !== TimeBucketType.YEAR) { + return series + } + + const buckets = new Map< + string, + { clicks: number; impressions: number; weightedPosition: number } + >() + const orderedKeys: string[] = [] + + for (const entry of series) { + if (!entry.date) continue + + const key = startOfBucket(entry.date, bucket) + const existing = buckets.get(key) + + if (existing) { + existing.clicks += entry.clicks + existing.impressions += entry.impressions + existing.weightedPosition += entry.position * entry.impressions + } else { + orderedKeys.push(key) + buckets.set(key, { + clicks: entry.clicks, + impressions: entry.impressions, + weightedPosition: entry.position * entry.impressions, + }) + } + } + + return orderedKeys.map((key) => { + const entry = buckets.get(key) + + return { + date: key, + clicks: Math.round(entry.clicks), + impressions: Math.round(entry.impressions), + ctr: + entry.impressions > 0 + ? round((entry.clicks / entry.impressions) * 100, 2) + : 0, + position: + entry.impressions > 0 + ? round(entry.weightedPosition / entry.impressions, 2) + : 0, + } + }) +} + +export const mapSeoTimeseries = ( + series: SeoDateSeriesEntry[], + metrics: string[], + timezone: string, +): TimeseriesRow[] => + series + .filter((entry) => Boolean(entry.date)) + .map((entry) => { + const values = mapSeoMetricValues(entry) + const row: TimeseriesRow = { + timestamp: toIsoTimestamp(entry.date, timezone), + } + + for (const metric of metrics) { + row[metric] = values[metric as keyof SeoMetricValues] + } + + return row + }) diff --git a/backend/apps/cloud/src/analytics/v2/mappers/summary.mapper.ts b/backend/apps/cloud/src/analytics/v2/mappers/summary.mapper.ts new file mode 100644 index 000000000..b2b2219f6 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/mappers/summary.mapper.ts @@ -0,0 +1,63 @@ +interface V1SummaryPeriod { + all: number + unique: number + users: number + bounceRate: number + sdur: number | null +} + +export interface V1TrafficSummary { + current: V1SummaryPeriod + previous: V1SummaryPeriod + change: number + uniqueChange: number + usersChange: number + bounceRateChange: number + sdurChange: number | null +} + +export interface TrafficSummaryPeriod { + visitors: number + pageviews: number + users: number + bounce_rate: number + session_duration: number | null +} + +export interface TrafficSummaryData { + current: TrafficSummaryPeriod + previous: TrafficSummaryPeriod + change: TrafficSummaryPeriod +} + +const toNumber = (value: unknown): number => Number(value) || 0 + +const toNullableNumber = (value: unknown): number | null => { + if (value === null || typeof value === 'undefined') { + return null + } + + return Number(value) || 0 +} + +const mapPeriod = (period: V1SummaryPeriod): TrafficSummaryPeriod => ({ + visitors: toNumber(period.unique), + pageviews: toNumber(period.all), + users: toNumber(period.users), + bounce_rate: toNumber(period.bounceRate), + session_duration: toNullableNumber(period.sdur), +}) + +export const mapTrafficSummary = ( + v1Summary: V1TrafficSummary, +): TrafficSummaryData => ({ + current: mapPeriod(v1Summary.current), + previous: mapPeriod(v1Summary.previous), + change: { + visitors: toNumber(v1Summary.uniqueChange), + pageviews: toNumber(v1Summary.change), + users: toNumber(v1Summary.usersChange), + bounce_rate: toNumber(v1Summary.bounceRateChange), + session_duration: toNullableNumber(v1Summary.sdurChange), + }, +}) diff --git a/backend/apps/cloud/src/analytics/v2/mappers/timeseries.mapper.ts b/backend/apps/cloud/src/analytics/v2/mappers/timeseries.mapper.ts new file mode 100644 index 000000000..b0f80e876 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/mappers/timeseries.mapper.ts @@ -0,0 +1,59 @@ +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +import dayjsTimezone from 'dayjs/plugin/timezone' + +import { DEFAULT_TIMEZONE } from '../../../user/entities/user.entity' + +dayjs.extend(utc) +dayjs.extend(dayjsTimezone) + +export type TimeseriesRow = { timestamp: string } & Record< + string, + string | number | null +> + +export const toIsoTimestamp = (value: string, timezone: string): string => { + const zone = timezone || DEFAULT_TIMEZONE + + return dayjs.tz(value, zone).format() +} + +export const zipTimeseries = ( + x: string[], + series: Record, + timezone: string, +): TimeseriesRow[] => { + const names = Object.keys(series).filter((name) => + Array.isArray(series[name]), + ) + + return x.map((label, index) => { + const row: TimeseriesRow = { timestamp: toIsoTimestamp(label, timezone) } + + for (const name of names) { + const value = series[name][index] + row[name] = typeof value === 'undefined' ? null : value + } + + return row + }) +} + +export const computeBounceRateSeries = ( + bounces: number[] | undefined, + uniques: number[] | undefined, +): number[] | undefined => { + if (!Array.isArray(bounces) || !Array.isArray(uniques)) { + return undefined + } + + return bounces.map((bounceCount, index) => { + const uniqueCount = Number(uniques[index]) || 0 + + if (uniqueCount <= 0) { + return 0 + } + + return Math.round((Number(bounceCount) / uniqueCount) * 100 * 100) / 100 + }) +} diff --git a/backend/apps/cloud/src/analytics/v2/query/breakdown-query.builder.ts b/backend/apps/cloud/src/analytics/v2/query/breakdown-query.builder.ts new file mode 100644 index 000000000..645f15f9a --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/query/breakdown-query.builder.ts @@ -0,0 +1,106 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { getCaptchaColumnExpression } from '../../analytics.service' +import { + captchaExtraWhere, + V2DataType, + V2DimensionDef, + V2MetricCtx, + V2MetricDef, +} from '../registry' + +export interface BreakdownSort { + field: string + direction: 'asc' | 'desc' +} + +export interface BreakdownQueryOptions { + dataType: V2DataType + dimension: V2DimensionDef + metrics: V2MetricDef[] + subQuery: string + ctx: V2MetricCtx + sort: BreakdownSort +} + +export const parseSortParam = ( + sort: string | undefined, + dimension: V2DimensionDef, + metrics: V2MetricDef[], +): BreakdownSort => { + if (!sort) { + return { field: metrics[0].api, direction: 'desc' } + } + + const [field, direction = 'desc'] = sort.split(':') + + if (direction !== 'asc' && direction !== 'desc') { + throw new UnprocessableEntityException( + `Invalid sort direction '${direction}'; use 'asc' or 'desc'`, + ) + } + + const sortable = ['value', ...metrics.map((metric) => metric.api)] + + if (!sortable.includes(field)) { + throw new UnprocessableEntityException( + `Cannot sort '${dimension.api}' breakdown by '${field}'. Sortable fields: ${sortable.join(', ')}`, + ) + } + + return { field, direction } +} + +export const buildBreakdownQuery = (opts: BreakdownQueryOptions): string => { + const { dataType, dimension, metrics, subQuery, ctx, sort } = opts + + const dimensionSql = + dataType === 'captcha' + ? getCaptchaColumnExpression(dimension.column) + : dimension.column + + const dimensionCols = [ + `${dimensionSql} AS value`, + ...(dimension.extraFields ?? []).map( + (field) => `${field.column} AS ${field.api}`, + ), + ] + + const metricCols = metrics.map((metric) => { + const expr = metric.sqlExpr(ctx) + + if (!expr) { + throw new UnprocessableEntityException( + `The '${metric.api}' metric is not supported on breakdowns. Request it on the timeseries or summary endpoint instead`, + ) + } + + return `${expr} AS ${metric.api}` + }) + + const groupBy = ['value', ...(dimension.extraFields ?? []).map((f) => f.api)] + + const whereParts: string[] = [] + + if (dimension.excludeNull) { + whereParts.push(`AND ${dimension.column} IS NOT NULL`) + } + + if (dataType === 'captcha') { + const guard = captchaExtraWhere(dimension) + if (guard) { + whereParts.push(guard) + } + } + + return ` + SELECT + ${[...dimensionCols, ...metricCols].join(',\n ')}, + count() OVER () AS __total + ${subQuery} + ${whereParts.join(' ')} + GROUP BY ${groupBy.join(', ')} + ORDER BY ${sort.field} ${sort.direction} + LIMIT {v2_limit:UInt32} OFFSET {v2_offset:UInt32} + ` +} diff --git a/backend/apps/cloud/src/analytics/v2/query/filters.translator.ts b/backend/apps/cloud/src/analytics/v2/query/filters.translator.ts new file mode 100644 index 000000000..833cb0900 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/query/filters.translator.ts @@ -0,0 +1,223 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { + findDimension, + V2_KEYED_FILTER_DIMENSIONS, + V2DataType, +} from '../registry' + +const V2_FILTER_OPERATORS = [ + 'is', + 'is_not', + 'contains', + 'contains_not', +] as const + +type V2FilterOperator = (typeof V2_FILTER_OPERATORS)[number] + +export interface V2Filter { + dimension: string + operator: V2FilterOperator + value: string | null | (string | null)[] + key?: string +} + +interface V1Filter { + column: string + filter: string | null | (string | null)[] + isExclusive: boolean + isContains: boolean +} + +const parseFiltersJson = (filters: string): unknown => { + try { + return JSON.parse(filters) + } catch { + throw new UnprocessableEntityException( + 'The provided filters are not a valid JSON array', + ) + } +} + +const resolveColumn = ( + filter: V2Filter, + type: V2DataType, + lenient: boolean, +): string | null => { + const { dimension, key } = filter + + const keyedFamily = Object.hasOwn(V2_KEYED_FILTER_DIMENSIONS, dimension) + ? V2_KEYED_FILTER_DIMENSIONS[dimension] + : undefined + + if (keyedFamily) { + if (!keyedFamily.types.includes(type)) { + if (lenient) { + return null + } + + throw new UnprocessableEntityException( + `The '${dimension}' filter is not supported for ${type} data`, + ) + } + + return key ? `${keyedFamily.v1Prefix}${key}` : keyedFamily.v1KeyColumn + } + + const def = findDimension(dimension, type) + + if (!def) { + if (lenient) { + return null + } + + throw new UnprocessableEntityException( + `Unknown ${type} filter dimension '${dimension}'`, + ) + } + + return def.column +} + +export const parseV2Filters = (filters: string | undefined): V2Filter[] => { + if (!filters || filters === '""') { + return [] + } + + const parsed = parseFiltersJson(filters) + + if (!Array.isArray(parsed)) { + throw new UnprocessableEntityException( + 'The filters parameter must be a JSON array of filter objects', + ) + } + + return parsed.map((raw, index) => { + if (!raw || typeof raw !== 'object') { + throw new UnprocessableEntityException( + `Filter at index ${index} must be an object`, + ) + } + + const { dimension, operator, value, key } = raw as Record + + if (typeof dimension !== 'string' || !dimension) { + throw new UnprocessableEntityException( + `Filter at index ${index} is missing a 'dimension'`, + ) + } + + if (!V2_FILTER_OPERATORS.includes(operator as V2FilterOperator)) { + throw new UnprocessableEntityException( + `Filter at index ${index} has an invalid operator. Supported operators: ${V2_FILTER_OPERATORS.join(', ')}`, + ) + } + + const isValidValue = (v: unknown) => v === null || typeof v === 'string' + + if (Array.isArray(value)) { + if (!value.every(isValidValue)) { + throw new UnprocessableEntityException( + `Filter at index ${index} has an invalid 'value'; array values must be strings or null`, + ) + } + } else if (!isValidValue(value)) { + throw new UnprocessableEntityException( + `Filter at index ${index} has an invalid 'value'; it must be a string, null or an array`, + ) + } + + if (key !== undefined && (typeof key !== 'string' || !key)) { + throw new UnprocessableEntityException( + `Filter at index ${index} has an invalid 'key'; it must be a non-empty string`, + ) + } + + if ( + key !== undefined && + !Object.hasOwn(V2_KEYED_FILTER_DIMENSIONS, dimension) + ) { + throw new UnprocessableEntityException( + `Filter at index ${index} has a 'key', but the '${dimension}' dimension does not support keys. Keyed dimensions: ${Object.keys(V2_KEYED_FILTER_DIMENSIONS).join(', ')}`, + ) + } + + const filter: V2Filter = { + dimension, + operator: operator as V2FilterOperator, + value: value as V2Filter['value'], + } + + if (typeof key === 'string' && key) { + filter.key = key + } + + return filter + }) +} + +export const toV1FiltersJson = ( + filters: V2Filter[], + type: V2DataType, + options: { lenient?: boolean } = {}, +): string => { + if (filters.length === 0) { + return '' + } + + const v1Filters: V1Filter[] = [] + + for (const filter of filters) { + const column = resolveColumn(filter, type, Boolean(options.lenient)) + + if (column === null) { + continue + } + + v1Filters.push({ + column, + filter: filter.value, + isExclusive: + filter.operator === 'is_not' || filter.operator === 'contains_not', + isContains: + filter.operator === 'contains' || filter.operator === 'contains_not', + }) + } + + return JSON.stringify(v1Filters) +} + +export const normalizeFiltersToV1Json = ( + filters: string | undefined, + type: V2DataType, + options: { lenient?: boolean } = {}, +): string => { + if (!filters || filters === '""') { + return '' + } + + let parsed: unknown + try { + parsed = JSON.parse(filters) + } catch { + return filters + } + + if (!Array.isArray(parsed) || parsed.length === 0) { + return '' + } + + const isLegacyV1 = parsed.every( + (filter) => + filter && + typeof filter === 'object' && + 'column' in filter && + !('dimension' in filter), + ) + + if (isLegacyV1) { + return filters + } + + return toV1FiltersJson(parseV2Filters(filters), type, options) +} diff --git a/backend/apps/cloud/src/analytics/v2/registry/dimensions.ts b/backend/apps/cloud/src/analytics/v2/registry/dimensions.ts new file mode 100644 index 000000000..22858d453 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/registry/dimensions.ts @@ -0,0 +1,264 @@ +import { + CAPTCHA_EVENT_SQL, + CAPTCHA_DIFFICULTY_SQL, + CAPTCHA_REASON_SQL, + CAPTCHA_SOLVE_MS_SQL, +} from '../../analytics.service' +import { V2DimensionDef } from './types' + +const GEO_TYPES: V2DimensionDef['types'] = ['traffic', 'performance', 'errors'] + +export const V2_DIMENSIONS: V2DimensionDef[] = [ + { + api: 'country', + column: 'cc', + types: ['traffic', 'performance', 'captcha', 'errors'], + description: 'Country (ISO 3166-1 alpha-2 code)', + }, + { + api: 'region', + column: 'rg', + types: GEO_TYPES, + excludeNull: true, + extraFields: [ + { api: 'country', column: 'cc' }, + { api: 'region_code', column: 'rgc' }, + ], + description: 'Region / subdivision', + }, + { + api: 'city', + column: 'ct', + types: GEO_TYPES, + excludeNull: true, + extraFields: [{ api: 'country', column: 'cc' }], + description: 'City', + }, + { + api: 'page', + column: 'pg', + types: GEO_TYPES, + description: 'Page path', + }, + { + api: 'host', + column: 'host', + types: GEO_TYPES, + description: 'Hostname the event was recorded on', + }, + { + api: 'locale', + column: 'lc', + types: ['traffic', 'errors'], + description: 'Browser locale (e.g. en-GB)', + }, + { + api: 'browser', + column: 'br', + types: ['traffic', 'performance', 'captcha', 'errors'], + description: 'Browser name', + }, + { + api: 'browser_version', + column: 'brv', + types: GEO_TYPES, + extraFields: [{ api: 'browser', column: 'br' }], + description: 'Browser version', + }, + { + api: 'os', + column: 'os', + types: ['traffic', 'captcha', 'errors'], + description: 'Operating system name', + }, + { + api: 'os_version', + column: 'osv', + types: ['traffic', 'errors'], + extraFields: [{ api: 'os', column: 'os' }], + description: 'Operating system version', + }, + { + api: 'device', + column: 'dv', + types: ['traffic', 'performance', 'captcha', 'errors'], + description: 'Device type (desktop, mobile, tablet, ...)', + }, + { + api: 'referrer', + column: 'ref', + types: ['traffic'], + description: 'Full referrer URL', + }, + { + api: 'referrer_name', + column: 'refn', + types: ['traffic'], + filterOnly: true, + description: + 'Canonical referrer (root domain), filter-only; use the referrer dimension for breakdowns', + }, + { + api: 'utm_source', + column: 'so', + types: ['traffic'], + excludeNull: true, + description: 'UTM source', + }, + { + api: 'utm_medium', + column: 'me', + types: ['traffic'], + excludeNull: true, + description: 'UTM medium', + }, + { + api: 'utm_campaign', + column: 'ca', + types: ['traffic'], + excludeNull: true, + description: 'UTM campaign', + }, + { + api: 'utm_term', + column: 'te', + types: ['traffic'], + excludeNull: true, + description: 'UTM term', + }, + { + api: 'utm_content', + column: 'co', + types: ['traffic'], + excludeNull: true, + description: 'UTM content', + }, + { + api: 'isp', + column: 'isp', + types: GEO_TYPES, + excludeNull: true, + description: 'Internet service provider', + }, + { + api: 'organization', + column: 'og', + types: GEO_TYPES, + excludeNull: true, + description: 'Network organization', + }, + { + api: 'user_type', + column: 'ut', + types: GEO_TYPES, + excludeNull: true, + description: 'Connection user type (residential, hosting, ...)', + }, + { + api: 'connection_type', + column: 'ctp', + types: GEO_TYPES, + excludeNull: true, + description: 'Connection type (Cable/DSL, Cellular, ...)', + }, + { + api: 'entry_page', + column: 'entryPage', + types: ['traffic', 'errors'], + virtual: 'entry_page', + description: 'First page of the session', + }, + { + api: 'exit_page', + column: 'exitPage', + types: ['traffic', 'errors'], + virtual: 'exit_page', + description: 'Last page of the session', + }, + { + api: 'event', + column: 'ev', + types: ['traffic'], + filterOnly: true, + description: + 'Custom event name (filter-only; use /traffic/custom-events for a breakdown)', + }, + { + api: 'error_name', + column: 'name', + types: ['errors'], + filterOnly: true, + description: 'Error name', + }, + { + api: 'error_message', + column: 'message', + types: ['errors'], + filterOnly: true, + description: 'Error message', + }, + { + api: 'error_filename', + column: 'filename', + types: ['errors'], + filterOnly: true, + description: 'Source file the error originated from', + }, + { + api: 'captcha_event', + column: 'captcha_event', + types: ['captcha'], + description: 'Captcha lifecycle event (generate, pass, verify_fail, ...)', + }, + { + api: 'captcha_difficulty', + column: 'captcha_difficulty', + types: ['captcha'], + extraWhere: `AND ${CAPTCHA_EVENT_SQL} = 'generate' AND toUInt8OrZero(${CAPTCHA_DIFFICULTY_SQL}) > 0`, + description: 'Captcha difficulty level', + }, + { + api: 'captcha_reason', + column: 'captcha_reason', + types: ['captcha'], + extraWhere: `AND ${CAPTCHA_EVENT_SQL} = 'generate' AND ${CAPTCHA_REASON_SQL} NOT IN ('', 'baseline', 'manual')`, + description: 'Reason for elevated captcha difficulty', + }, + { + api: 'solve_time', + column: 'solve_time', + types: ['captcha'], + extraWhere: `AND ${CAPTCHA_EVENT_SQL} = 'pass' AND ${CAPTCHA_SOLVE_MS_SQL} > 0`, + description: 'Captcha solve-time bucket', + }, +] + +const CAPTCHA_PASS_GUARD = `AND ${CAPTCHA_EVENT_SQL} = 'pass'` + +export const captchaExtraWhere = (dim: V2DimensionDef): string | undefined => { + if (dim.extraWhere) { + return dim.extraWhere + } + + if (['cc', 'br', 'os', 'dv'].includes(dim.column)) { + return CAPTCHA_PASS_GUARD + } + + return undefined +} + +export const V2_KEYED_FILTER_DIMENSIONS: Record< + string, + { v1Prefix: string; v1KeyColumn: string; types: V2DimensionDef['types'] } +> = { + event_metadata: { + v1Prefix: 'ev:key:', + v1KeyColumn: 'ev:key', + types: ['traffic'], + }, + page_property: { + v1Prefix: 'tag:key:', + v1KeyColumn: 'tag:key', + types: ['traffic', 'errors'], + }, +} diff --git a/backend/apps/cloud/src/analytics/v2/registry/index.ts b/backend/apps/cloud/src/analytics/v2/registry/index.ts new file mode 100644 index 000000000..7fd13aa32 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/registry/index.ts @@ -0,0 +1,101 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { DataType } from '../../analytics.service' +import { V2_DIMENSIONS } from './dimensions' +import { V2_METRICS } from './metrics' +import { V2DataType, V2DimensionDef, V2MetricDef } from './types' + +export * from './types' +export { + V2_DIMENSIONS, + V2_KEYED_FILTER_DIMENSIONS, + captchaExtraWhere, +} from './dimensions' + +export const V2_TO_V1_DATATYPE: Record = { + traffic: DataType.ANALYTICS, + performance: DataType.PERFORMANCE, + captcha: DataType.CAPTCHA, + errors: DataType.ERRORS, +} + +export const listDimensions = (type: V2DataType): V2DimensionDef[] => + V2_DIMENSIONS.filter((dimension) => dimension.types.includes(type)) + +export const listMetrics = (type: V2DataType): V2MetricDef[] => + V2_METRICS.filter((metric) => metric.types.includes(type)) + +export const findDimension = ( + api: string, + type: V2DataType, +): V2DimensionDef | undefined => + V2_DIMENSIONS.find( + (dimension) => dimension.api === api && dimension.types.includes(type), + ) + +export const getBreakdownDimension = ( + api: string, + type: V2DataType, +): V2DimensionDef => { + const dimension = findDimension(api, type) + + if (!dimension) { + throw new UnprocessableEntityException( + `Unknown ${type} dimension '${api}'. Supported dimensions: ${listDimensions( + type, + ) + .filter((d) => !d.filterOnly) + .map((d) => d.api) + .join(', ')}`, + ) + } + + if (dimension.filterOnly) { + throw new UnprocessableEntityException( + `The '${api}' dimension can only be used in filters, not as a breakdown dimension`, + ) + } + + return dimension +} + +export const getMetric = (api: string, type: V2DataType): V2MetricDef => { + const metric = V2_METRICS.find((m) => m.api === api && m.types.includes(type)) + + if (!metric) { + throw new UnprocessableEntityException( + `Unknown ${type} metric '${api}'. Supported metrics: ${listMetrics(type) + .map((m) => m.api) + .join(', ')}`, + ) + } + + return metric +} + +export const getDefaultMetrics = (type: V2DataType): V2MetricDef[] => + listMetrics(type).filter((metric) => metric.isDefault) + +export const parseMetricsParam = ( + metrics: string | undefined, + type: V2DataType, +): V2MetricDef[] => { + if (!metrics) { + return getDefaultMetrics(type) + } + + const names = [ + ...new Set( + metrics + .split(',') + .map((name) => name.trim()) + .filter(Boolean), + ), + ] + + if (names.length === 0) { + return getDefaultMetrics(type) + } + + return names.map((name) => getMetric(name, type)) +} diff --git a/backend/apps/cloud/src/analytics/v2/registry/metrics.ts b/backend/apps/cloud/src/analytics/v2/registry/metrics.ts new file mode 100644 index 000000000..84b715f91 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/registry/metrics.ts @@ -0,0 +1,187 @@ +import { V2MetricDef } from './types' + +const MEASURES_MAP_V2: Record = { + average: 'avg', + median: 'median', + p95: 'quantileExact(0.95)', + p75: 'quantileExact(0.75)', +} + +const perfExpr = + (column: string) => + ({ measure }: { measure?: string }): string => { + const processedMeasure = + !measure || measure === 'quantiles' ? 'median' : measure + const fn = MEASURES_MAP_V2[processedMeasure] || 'median' + return `round(divide(${fn}(${column}), 1000), 2)` + } + +export const V2_METRICS: V2MetricDef[] = [ + { + api: 'visitors', + types: ['traffic'], + sqlExpr: ({ customEVFilterApplied }) => + customEVFilterApplied ? 'count(*)' : 'count(DISTINCT psid)', + isDefault: true, + format: 'integer', + description: 'Unique sessions', + }, + { + api: 'pageviews', + types: ['traffic'], + sqlExpr: () => 'count(*)', + isDefault: true, + format: 'integer', + description: 'Pageview (or matched custom event) count', + }, + { + api: 'users', + types: ['traffic'], + sqlExpr: () => 'count(DISTINCT profileId)', + format: 'integer', + description: 'Unique users (identified + anonymous profiles)', + }, + { + api: 'session_duration', + types: ['traffic'], + sqlExpr: () => null, + requiresSessionsJoin: true, + format: 'seconds', + description: + 'Average session duration in seconds (timeseries and summary only)', + }, + { + api: 'bounce_rate', + types: ['traffic'], + sqlExpr: () => null, + requiresSessionsJoin: true, + format: 'percent', + description: + 'Percentage of single-pageview sessions (timeseries and summary only)', + }, + { + api: 'concurrency', + types: ['traffic'], + sqlExpr: () => null, + requiresSessionsJoin: true, + format: 'integer', + description: + 'Concurrent live visitors over time (timeseries only; ignores filters)', + }, + { + api: 'load_time', + types: ['performance'], + sqlExpr: perfExpr('pageLoad'), + isDefault: true, + format: 'seconds', + description: 'Full page load time', + }, + { + api: 'ttfb', + types: ['performance'], + sqlExpr: perfExpr('ttfb'), + format: 'seconds', + description: 'Time to first byte', + }, + { + api: 'dns', + types: ['performance'], + sqlExpr: perfExpr('dns'), + format: 'seconds', + description: 'DNS resolution time', + }, + { + api: 'tls', + types: ['performance'], + sqlExpr: perfExpr('tls'), + format: 'seconds', + description: 'TLS setup time', + }, + { + api: 'connection', + types: ['performance'], + sqlExpr: perfExpr('conn'), + format: 'seconds', + description: 'Connection establishment time', + }, + { + api: 'response', + types: ['performance'], + sqlExpr: perfExpr('response'), + format: 'seconds', + description: 'Response time', + }, + { + api: 'render', + types: ['performance'], + sqlExpr: perfExpr('render'), + format: 'seconds', + description: 'Browser render time', + }, + { + api: 'dom_load', + types: ['performance'], + sqlExpr: perfExpr('domLoad'), + format: 'seconds', + description: 'DOM content load time', + }, + { + api: 'occurrences', + types: ['errors'], + sqlExpr: () => 'count(*)', + isDefault: true, + format: 'integer', + description: 'Error occurrences', + }, + { + api: 'affected_users', + types: ['errors'], + sqlExpr: () => 'uniqExact(profileId)', + format: 'integer', + description: 'Distinct users affected', + }, + { + api: 'events', + types: ['captcha'], + sqlExpr: () => 'count(*)', + isDefault: true, + format: 'integer', + description: 'Captcha events matching the dimension scope', + }, + { + api: 'generated', + types: ['captcha'], + sqlExpr: () => null, + format: 'integer', + description: 'Captchas generated (timeseries and summary only)', + }, + { + api: 'passed', + types: ['captcha'], + sqlExpr: () => null, + format: 'integer', + description: 'Captchas passed (timeseries and summary only)', + }, + { + api: 'failed', + types: ['captcha'], + sqlExpr: () => null, + format: 'integer', + description: 'Captchas failed verification (timeseries and summary only)', + }, + { + api: 'validation_failed', + types: ['captcha'], + sqlExpr: () => null, + format: 'integer', + description: + 'Captchas that failed validation (timeseries and summary only)', + }, + { + api: 'replayed', + types: ['captcha'], + sqlExpr: () => null, + format: 'integer', + description: 'Replayed captcha attempts (timeseries and summary only)', + }, +] diff --git a/backend/apps/cloud/src/analytics/v2/registry/seo.ts b/backend/apps/cloud/src/analytics/v2/registry/seo.ts new file mode 100644 index 000000000..7128783ee --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/registry/seo.ts @@ -0,0 +1,193 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { V2Filter } from '../query/filters.translator' + +/** + * SEO data is served by the Google Search Console API rather than ClickHouse, + * so it cannot join the column/sqlExpr registry in ./dimensions and ./metrics. + * These definitions mirror that registry's shape and lookup semantics to keep + * /v2/projects/:pid/seo/* consistent with the rest of the v2 surface. + */ + +export type V2SeoGscDimension = 'query' | 'page' | 'country' | 'device' + +export interface V2SeoDimensionDef { + api: string + gsc: V2SeoGscDimension + description: string +} + +export interface V2SeoMetricDef { + api: string + isDefault?: boolean + format: 'integer' | 'percent' | 'position' + description: string +} + +export const V2_SEO_DIMENSIONS: V2SeoDimensionDef[] = [ + { + api: 'query', + gsc: 'query', + description: 'Search query the site was surfaced for', + }, + { + api: 'page', + gsc: 'page', + description: 'Landing page URL that appeared in search results', + }, + { + api: 'country', + gsc: 'country', + description: 'Country the search was made from (ISO 3166-1 alpha-2 code)', + }, + { + api: 'device', + gsc: 'device', + description: 'Device category the search was made on', + }, +] + +const V2_SEO_METRICS: V2SeoMetricDef[] = [ + { + api: 'clicks', + isDefault: true, + format: 'integer', + description: 'Clicks through to the site from search results', + }, + { + api: 'impressions', + isDefault: true, + format: 'integer', + description: 'Times a link to the site appeared in search results', + }, + { + api: 'ctr', + isDefault: true, + format: 'percent', + description: 'Click-through rate, as a percentage of impressions', + }, + { + api: 'position', + isDefault: true, + format: 'position', + description: 'Average position in search results', + }, +] + +export const listSeoDimensions = (): V2SeoDimensionDef[] => V2_SEO_DIMENSIONS + +export const listSeoMetrics = (): V2SeoMetricDef[] => V2_SEO_METRICS + +const findSeoDimension = (api: string): V2SeoDimensionDef | undefined => + V2_SEO_DIMENSIONS.find((dimension) => dimension.api === api) + +export const getSeoBreakdownDimension = (api: string): V2SeoDimensionDef => { + const dimension = findSeoDimension(api) + + if (!dimension) { + throw new UnprocessableEntityException( + `Unknown seo dimension '${api}'. Supported dimensions: ${V2_SEO_DIMENSIONS.map( + (d) => d.api, + ).join(', ')}`, + ) + } + + return dimension +} + +export const getSeoMetric = (api: string): V2SeoMetricDef => { + const metric = V2_SEO_METRICS.find((m) => m.api === api) + + if (!metric) { + throw new UnprocessableEntityException( + `Unknown seo metric '${api}'. Supported metrics: ${V2_SEO_METRICS.map( + (m) => m.api, + ).join(', ')}`, + ) + } + + return metric +} + +export const getDefaultSeoMetrics = (): V2SeoMetricDef[] => + V2_SEO_METRICS.filter((metric) => metric.isDefault) + +export const parseSeoMetricsParam = ( + metrics: string | undefined, +): V2SeoMetricDef[] => { + if (!metrics) { + return getDefaultSeoMetrics() + } + + const names = [ + ...new Set( + metrics + .split(',') + .map((name) => name.trim()) + .filter(Boolean), + ), + ] + + if (names.length === 0) { + return getDefaultSeoMetrics() + } + + return names.map((name) => getSeoMetric(name)) +} + +/** + * Search Console orders rows by clicks descending and exposes no sort control, + * so this is the only ordering the seo breakdown can honour. It is accepted + * (rather than rejected outright) so callers can be explicit about it. + */ +export const SEO_SORT = 'clicks:desc' + +/** + * GSCService.parseFilters keys filters by their v1 URL names, where the keyword + * dimension is 'keywords'. v2 calls it 'query', after Search Console's own + * naming, so translate on the way down — the same v2 -> v1 hop the ClickHouse + * data types make through toV1FiltersJson. + */ +export const toGscFiltersJson = (filters: V2Filter[]): string | undefined => { + if (filters.length === 0) { + return undefined + } + + return JSON.stringify( + filters.map((filter) => ({ + ...filter, + dimension: filter.dimension === 'query' ? 'keywords' : filter.dimension, + })), + ) +} + +/** + * Search Console expresses filters as dimensionFilterGroups of scalar + * comparisons, so the array / null values the rest of v2 accepts have no + * equivalent here. Reject them rather than silently dropping the filter. + */ +export const validateSeoFilters = (filters: V2Filter[]): V2Filter[] => { + for (const filter of filters) { + if (!findSeoDimension(filter.dimension)) { + throw new UnprocessableEntityException( + `Unknown seo filter dimension '${filter.dimension}'. Supported dimensions: ${V2_SEO_DIMENSIONS.map( + (d) => d.api, + ).join(', ')}`, + ) + } + + if (Array.isArray(filter.value)) { + throw new UnprocessableEntityException( + `The seo filter on '${filter.dimension}' cannot take an array of values; Search Console only supports a single value per filter`, + ) + } + + if (filter.value === null) { + throw new UnprocessableEntityException( + `The seo filter on '${filter.dimension}' cannot take a null value; Search Console does not report unset dimension values`, + ) + } + } + + return filters +} diff --git a/backend/apps/cloud/src/analytics/v2/registry/types.ts b/backend/apps/cloud/src/analytics/v2/registry/types.ts new file mode 100644 index 000000000..49ee973b5 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/registry/types.ts @@ -0,0 +1,35 @@ +import { PerfMeasure } from '../../interfaces' + +export type V2DataType = 'traffic' | 'performance' | 'captcha' | 'errors' + +interface V2DimensionExtraField { + api: string + column: string +} + +export interface V2DimensionDef { + api: string + column: string + types: V2DataType[] + extraFields?: V2DimensionExtraField[] + excludeNull?: boolean + filterOnly?: boolean + virtual?: 'entry_page' | 'exit_page' + extraWhere?: string + description: string +} + +export interface V2MetricCtx { + customEVFilterApplied: boolean + measure?: PerfMeasure +} + +export interface V2MetricDef { + api: string + types: V2DataType[] + sqlExpr: (ctx: V2MetricCtx) => string | null + requiresSessionsJoin?: boolean + isDefault?: boolean + format?: 'integer' | 'seconds' | 'percent' + description: string +} diff --git a/backend/apps/cloud/src/analytics/v2/seo-v2.service.ts b/backend/apps/cloud/src/analytics/v2/seo-v2.service.ts new file mode 100644 index 000000000..a8261e0a3 --- /dev/null +++ b/backend/apps/cloud/src/analytics/v2/seo-v2.service.ts @@ -0,0 +1,512 @@ +import { + BadRequestException, + ConflictException, + Injectable, + UnprocessableEntityException, +} from '@nestjs/common' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' + +import { + BrandedTrafficAnalytics, + GSC_ALL_TIME_DAYS, + GSCContext, + GSCService, + MAX_BRANDED_TRAFFIC_RANGE_DAYS, + MAX_FILTER_EXPRESSION_LENGTH, + MAX_FILTERS, + MAX_POSITION_ANALYTICS_RANGE_DAYS, + OPTIONAL_ANALYTICS_TIMEOUT_MS, + PositionAnalytics, +} from '../../project/gsc.service' +import { AnalyticsService } from '../analytics.service' +import { TimeBucketType } from '../dto/getData.dto' +import { + V2SeoBreakdownDto, + V2SeoRangeDto, + V2SeoSummaryDto, + V2SeoTimeseriesDto, + V2_SEO_TIME_BUCKETS, +} from './dto/seo.dto' +import { V2_DEFAULT_PERIOD, V2BaseQueryDto } from './dto/v2-base.dto' +import { envelope, V2Envelope } from './mappers/envelope' +import { + bucketSeoDateSeries, + mapSeoBreakdownRows, + mapSeoSummary, + mapSeoTimeseries, + SeoDateSeriesEntry, + SeoSummaryData, +} from './mappers/seo.mapper' +import { toIsoTimestamp, TimeseriesRow } from './mappers/timeseries.mapper' +import { parseV2Filters, V2Filter } from './query/filters.translator' +import { + getSeoBreakdownDimension, + listSeoDimensions, + listSeoMetrics, + parseSeoMetricsParam, + SEO_SORT, + toGscFiltersJson, + validateSeoFilters, +} from './registry/seo' + +dayjs.extend(utc) + +const DEFAULT_SEO_BREAKDOWN_LIMIT = 30 + +const INTRADAY_PERIODS = ['1h', 'today', 'yesterday', '1d'] + +interface SeoTimeframe { + period: string | null + timeBucket: TimeBucketType + safeTimezone: string + groupFrom: string + groupTo: string + rangeDays: number +} + +const SKIPPED = Symbol('skipped') + +/** + * Runs an expensive Search Console rollup under a deadline. A timeout aborts + * the in-flight paging and reports the panel as skipped; a genuine upstream + * failure still propagates, so a broken panel reads as broken rather than as + * "too much data". + */ +const resolveOptional = async ( + load: (signal: AbortSignal) => Promise, +): Promise => { + const controller = new AbortController() + let timeout: ReturnType | undefined + + const loading = load(controller.signal) + + // Aborting rejects `loading` after the race has already settled on SKIPPED; + // this keeps that late rejection from surfacing as an unhandled rejection + // without stopping Promise.race from seeing a real, pre-deadline failure. + loading.catch(() => {}) + + try { + return await Promise.race([ + loading, + new Promise((resolve) => { + timeout = setTimeout(() => { + controller.abort() + resolve(SKIPPED) + }, OPTIONAL_ANALYTICS_TIMEOUT_MS) + }), + ]) + } finally { + if (timeout) { + clearTimeout(timeout) + } + } +} + +@Injectable() +export class SeoV2Service { + constructor( + private readonly gscService: GSCService, + private readonly analyticsService: AnalyticsService, + ) {} + + /** + * Search Console has to be connected and a property linked before any of the + * data endpoints can answer. Both are project configuration rather than a bad + * request, hence 409 rather than 400. + * + * Call this only after the request itself has been validated: a malformed + * filter is a 422 whether or not the project happens to be connected, and + * answering 409 first would send callers debugging their query down the + * wrong path. + */ + private async requireContext(pid: string): Promise { + const { connected, property } = + await this.gscService.getConnectionState(pid) + + if (!connected) { + throw new ConflictException( + 'Google Search Console is not connected for this project', + ) + } + + if (!property) { + throw new ConflictException( + 'No Google Search Console property is linked to this project', + ) + } + + return this.gscService.getGSCContext(pid) + } + + private prepareFilters(filters: string | undefined): { + appliedFilters: V2Filter[] + gscFilters: string | undefined + } { + const appliedFilters = validateSeoFilters(parseV2Filters(filters)) + + if (appliedFilters.length > MAX_FILTERS) { + throw new UnprocessableEntityException( + `Too many filters; at most ${MAX_FILTERS} may be applied at once`, + ) + } + + for (const filter of appliedFilters) { + if (String(filter.value).length > MAX_FILTER_EXPRESSION_LENGTH) { + throw new UnprocessableEntityException( + `The seo filter on '${filter.dimension}' has a value longer than ${MAX_FILTER_EXPRESSION_LENGTH} characters`, + ) + } + } + + // GSCService.parseFilters already understands the v2 filter shape, so the + // validated filters are handed straight back to it, mirroring how the + // ClickHouse data types translate v2 -> v1 JSON -> getFiltersQuery. + return { appliedFilters, gscFilters: toGscFiltersJson(appliedFilters) } + } + + private resolveTimeframe( + dto: V2BaseQueryDto, + requestedBucket?: TimeBucketType, + ): SeoTimeframe { + const { from, to, timezone } = dto + const safeTimezone = this.analyticsService.getSafeTimezone(timezone) + + if (Boolean(from) !== Boolean(to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(from && to) + const period = hasCustomRange + ? null + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + const timeBucket = + requestedBucket ?? + (period && INTRADAY_PERIODS.includes(period) + ? TimeBucketType.HOUR + : TimeBucketType.DAY) + + // getGroupFromTo validates the bucket against the range, which neither an + // open-ended 'all' nor a caller-chosen custom range can satisfy. + const groupTimeBucket = + hasCustomRange || period === 'all' ? null : timeBucket + const diff = period === 'all' ? GSC_ALL_TIME_DAYS : undefined + + const { groupFrom, groupTo } = this.analyticsService.getGroupFromTo( + hasCustomRange ? from : undefined, + hasCustomRange ? to : undefined, + groupTimeBucket, + period ?? undefined, + safeTimezone, + diff, + ) + + const rangeDays = + Math.abs( + dayjs(groupTo) + .startOf('day') + .diff(dayjs(groupFrom).startOf('day'), 'day'), + ) + 1 + + return { period, timeBucket, safeTimezone, groupFrom, groupTo, rangeDays } + } + + private baseMeta( + pid: string, + timeframe: SeoTimeframe, + appliedFilters: V2Filter[], + ): Record { + return { + pid, + period: timeframe.period, + from: toIsoTimestamp(timeframe.groupFrom, timeframe.safeTimezone), + to: toIsoTimestamp(timeframe.groupTo, timeframe.safeTimezone), + timezone: timeframe.safeTimezone, + appliedFilters, + } + } + + async getStatus( + pid: string, + ): Promise> { + const state = await this.gscService.getConnectionState(pid) + + return envelope(state, { pid }) + } + + /** + * Mirrors AnalyticsV2Service.getDimensions so `?type=seo` answers in the same + * shape as every other data type on the discovery endpoint. + */ + getDimensions(): V2Envelope<{ + dimensions: Record[] + metrics: Record[] + }> { + const dimensions = listSeoDimensions().map((dimension) => ({ + name: dimension.api, + description: dimension.description, + filterOnly: false, + extraFields: [] as string[], + })) + + const metrics = listSeoMetrics().map((metric) => ({ + name: metric.api, + description: metric.description, + format: metric.format, + default: Boolean(metric.isDefault), + })) + + return envelope({ dimensions, metrics }, { type: 'seo' }) + } + + async getSummary( + pid: string, + dto: V2SeoSummaryDto, + ): Promise> { + const { appliedFilters, gscFilters } = this.prepareFilters(dto.filters) + const timeframe = this.resolveTimeframe(dto) + const ctx = await this.requireContext(pid) + const { groupFrom, groupTo } = timeframe + + const fromDate = dayjs(groupFrom) + const durationMs = dayjs(groupTo).diff(fromDate) + const previousTo = fromDate + .subtract(1, 'millisecond') + .format('YYYY-MM-DD HH:mm:ss') + const previousFrom = fromDate + .subtract(durationMs, 'millisecond') + .format('YYYY-MM-DD HH:mm:ss') + + const [current, previous] = await Promise.all([ + this.gscService.getSummary(pid, groupFrom, groupTo, gscFilters, ctx), + this.gscService + .getSummary(pid, previousFrom, previousTo, gscFilters, ctx) + .catch(() => null), + ]) + + return envelope(mapSeoSummary(current, previous), { + ...this.baseMeta(pid, timeframe, appliedFilters), + metrics: listSeoMetrics().map((metric) => metric.api), + }) + } + + async getTimeseries( + pid: string, + dto: V2SeoTimeseriesDto, + ): Promise> { + const { appliedFilters, gscFilters } = this.prepareFilters(dto.filters) + const metrics = parseSeoMetricsParam(dto.metrics).map( + (metric) => metric.api, + ) + const timeframe = this.resolveTimeframe( + dto, + dto.timeBucket as TimeBucketType | undefined, + ) + const ctx = await this.requireContext(pid) + + // Search Console only serves hourly and daily rows; wider buckets are + // rolled up from the daily series rather than requested upstream. + const series = (await this.gscService.getDateSeries( + pid, + timeframe.groupFrom, + timeframe.groupTo, + timeframe.timeBucket === TimeBucketType.HOUR ? 'hour' : 'day', + gscFilters, + ctx, + )) as SeoDateSeriesEntry[] + + const bucketed = bucketSeoDateSeries(series, timeframe.timeBucket) + + return envelope( + mapSeoTimeseries(bucketed, metrics, timeframe.safeTimezone), + { + ...this.baseMeta(pid, timeframe, appliedFilters), + metrics, + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: [...V2_SEO_TIME_BUCKETS], + }, + ) + } + + async getBreakdown( + pid: string, + dto: V2SeoBreakdownDto, + ): Promise[]>> { + const dimension = getSeoBreakdownDimension(dto.dimension) + const { appliedFilters, gscFilters } = this.prepareFilters(dto.filters) + const metrics = parseSeoMetricsParam(dto.metrics).map( + (metric) => metric.api, + ) + const timeframe = this.resolveTimeframe(dto) + const limit = dto.limit ?? DEFAULT_SEO_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + const ctx = await this.requireContext(pid) + const { groupFrom, groupTo } = timeframe + + const rows = await this.fetchBreakdownRows( + dimension.gsc, + pid, + groupFrom, + groupTo, + limit, + offset, + gscFilters, + ctx, + ) + + return envelope(mapSeoBreakdownRows(dimension.gsc, rows, metrics), { + ...this.baseMeta(pid, timeframe, appliedFilters), + dimension: dimension.api, + metrics, + limit, + offset, + sort: SEO_SORT, + }) + } + + private fetchBreakdownRows( + dimension: 'query' | 'page' | 'country' | 'device', + pid: string, + from: string, + to: string, + limit: number, + offset: number, + filters: string | undefined, + ctx: GSCContext, + ) { + if (dimension === 'query') { + return this.gscService.getKeywords( + pid, + from, + to, + limit, + offset, + filters, + undefined, + ctx, + ) + } + + if (dimension === 'page') { + return this.gscService.getTopPages( + pid, + from, + to, + limit, + offset, + filters, + undefined, + ctx, + ) + } + + if (dimension === 'country') { + return this.gscService.getTopCountries( + pid, + from, + to, + limit, + offset, + filters, + ctx, + ) + } + + return this.gscService.getTopDevices( + pid, + from, + to, + limit, + offset, + filters, + ctx, + ) + } + + async getBrandedTraffic( + pid: string, + dto: V2SeoRangeDto, + ): Promise> { + const { appliedFilters, gscFilters } = this.prepareFilters(dto.filters) + const timeframe = this.resolveTimeframe(dto) + const ctx = await this.requireContext(pid) + const meta = this.baseMeta(pid, timeframe, appliedFilters) + + if (timeframe.rangeDays > MAX_BRANDED_TRAFFIC_RANGE_DAYS) { + return envelope(null, { + ...meta, + skipped: true, + skippedReason: 'range_too_large', + maxRangeDays: MAX_BRANDED_TRAFFIC_RANGE_DAYS, + }) + } + + const result = await resolveOptional((signal) => + this.gscService.getBrandedTraffic( + pid, + timeframe.groupFrom, + timeframe.groupTo, + gscFilters, + ctx, + signal, + ), + ) + + if (result === SKIPPED) { + return envelope(null, { + ...meta, + skipped: true, + skippedReason: 'timeout', + timeoutMs: OPTIONAL_ANALYTICS_TIMEOUT_MS, + }) + } + + return envelope(result, { ...meta, skipped: false }) + } + + async getPositions( + pid: string, + dto: V2SeoRangeDto, + ): Promise> { + const { appliedFilters, gscFilters } = this.prepareFilters(dto.filters) + const timeframe = this.resolveTimeframe(dto) + const ctx = await this.requireContext(pid) + const meta = this.baseMeta(pid, timeframe, appliedFilters) + + if (timeframe.rangeDays > MAX_POSITION_ANALYTICS_RANGE_DAYS) { + return envelope(null, { + ...meta, + skipped: true, + skippedReason: 'range_too_large', + maxRangeDays: MAX_POSITION_ANALYTICS_RANGE_DAYS, + }) + } + + const result = await resolveOptional((signal) => + this.gscService.getPositionAnalytics( + pid, + timeframe.groupFrom, + timeframe.groupTo, + gscFilters, + ctx, + signal, + ), + ) + + if (result === SKIPPED) { + return envelope(null, { + ...meta, + skipped: true, + skippedReason: 'timeout', + timeoutMs: OPTIONAL_ANALYTICS_TIMEOUT_MS, + }) + } + + return envelope(result, { ...meta, skipped: false }) + } +} diff --git a/backend/apps/cloud/src/common/constants.ts b/backend/apps/cloud/src/common/constants.ts index c76a3f69a..6d77523fe 100644 --- a/backend/apps/cloud/src/common/constants.ts +++ b/backend/apps/cloud/src/common/constants.ts @@ -106,6 +106,40 @@ const TRAFFIC_COLUMNS = [ const TRAFFIC_METAKEY_COLUMNS = ['tag:key', 'tag:value'] const ALL_COLUMNS = [...TRAFFIC_COLUMNS, 'ev', 'entryPage', 'exitPage'] + +const V2_VIEW_FILTER_DIMENSIONS = [ + 'country', + 'region', + 'city', + 'page', + 'host', + 'locale', + 'browser', + 'browser_version', + 'os', + 'os_version', + 'device', + 'referrer', + 'referrer_name', + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_term', + 'utm_content', + 'isp', + 'organization', + 'user_type', + 'connection_type', + 'entry_page', + 'exit_page', + 'event', + 'error_name', + 'error_message', + 'error_filename', + 'event_metadata', + 'page_property', +] + const CAPTCHA_COLUMNS = [ 'cc', 'br', @@ -228,6 +262,7 @@ export { redisUserUsageinfoCacheTimeout, TRAFFIC_COLUMNS, TRAFFIC_METAKEY_COLUMNS, + V2_VIEW_FILTER_DIMENSIONS, CAPTCHA_COLUMNS, ERROR_COLUMNS, PERFORMANCE_COLUMNS, diff --git a/backend/apps/cloud/src/experiment/experiment.controller.ts b/backend/apps/cloud/src/experiment/experiment.controller.ts index 1d77f79af..10b78d9d7 100644 --- a/backend/apps/cloud/src/experiment/experiment.controller.ts +++ b/backend/apps/cloud/src/experiment/experiment.controller.ts @@ -43,6 +43,7 @@ import { DataType, getLowestPossibleTimeBucket, } from '../analytics/analytics.service' +import { normalizeFiltersToV1Json } from '../analytics/v2/query/filters.translator' import { TimeBucketType } from '../analytics/dto/getData.dto' import { Auth } from '../auth/decorators' import { CurrentUserId } from '../auth/decorators/current-user-id.decorator' @@ -1043,7 +1044,10 @@ export class ExperimentController { } const [filtersQuery, filtersParams, appliedFilters] = - this.analyticsService.getFiltersQuery(filters || '[]', DataType.ANALYTICS) + this.analyticsService.getFiltersQuery( + normalizeFiltersToV1Json(filters, 'traffic'), + DataType.ANALYTICS, + ) const isSegmented = Array.isArray(appliedFilters) && appliedFilters.length > 0 diff --git a/backend/apps/cloud/src/goal/goal.controller.ts b/backend/apps/cloud/src/goal/goal.controller.ts index 8722ad880..8e8632a4b 100644 --- a/backend/apps/cloud/src/goal/goal.controller.ts +++ b/backend/apps/cloud/src/goal/goal.controller.ts @@ -33,6 +33,7 @@ import { DataType, getLowestPossibleTimeBucket, } from '../analytics/analytics.service' +import { normalizeFiltersToV1Json } from '../analytics/v2/query/filters.translator' import { Auth } from '../auth/decorators' import { CurrentUserId } from '../auth/decorators/current-user-id.decorator' import { @@ -84,10 +85,20 @@ const clampPagination = (take?: number, skip?: number) => { const GOAL_CONDITION_COLUMNS: Record = { page: 'pg', - pg: 'pg', event: 'event_name', - event_name: 'event_name', host: 'host', + country: 'cc', + device: 'dv', + browser: 'br', + os: 'os', + referrer: 'ref', + utm_source: 'so', + utm_medium: 'me', + utm_campaign: 'ca', + utm_term: 'te', + utm_content: 'co', + pg: 'pg', + event_name: 'event_name', ref: 'ref', so: 'so', me: 'me', @@ -97,7 +108,6 @@ const GOAL_CONDITION_COLUMNS: Record = { cc: 'cc', dv: 'dv', br: 'br', - os: 'os', } type GoalBreakdownKey = @@ -875,7 +885,7 @@ export class GoalController { ) const [filtersQuery, filtersParams] = this.analyticsService.getFiltersQuery( - filters || '[]', + normalizeFiltersToV1Json(filters, 'traffic'), DataType.ANALYTICS, ) const { query: conversionsSubquery, params: goalParams } = @@ -1190,7 +1200,7 @@ export class GoalController { const [selector, groupBy] = this.getGroupSubquery(resolvedTimeBucket) const [filtersQuery, filtersParams] = this.analyticsService.getFiltersQuery( - filters || '[]', + normalizeFiltersToV1Json(filters, 'traffic'), DataType.ANALYTICS, ) const { query: conversionsSubquery, params: goalParams } = diff --git a/backend/apps/cloud/src/main.ts b/backend/apps/cloud/src/main.ts index 56da45680..d06d45286 100644 --- a/backend/apps/cloud/src/main.ts +++ b/backend/apps/cloud/src/main.ts @@ -46,6 +46,7 @@ async function bootstrap() { .setTitle('Swetrix API') .setVersion(process.env.npm_package_version) .addBearerAuth() + .addApiKey({ type: 'apiKey', name: 'X-Api-Key', in: 'header' }, 'apiKey') .build() const document = SwaggerModule.createDocument(app, config) SwaggerModule.setup('api', app, document) diff --git a/backend/apps/cloud/src/project/dto/create-project-view.dto.ts b/backend/apps/cloud/src/project/dto/create-project-view.dto.ts index 8d89ebbbd..0c773d5b6 100644 --- a/backend/apps/cloud/src/project/dto/create-project-view.dto.ts +++ b/backend/apps/cloud/src/project/dto/create-project-view.dto.ts @@ -1,5 +1,6 @@ import { ArrayMaxSize, + IsArray, IsEnum, IsNotEmpty, IsOptional, @@ -12,7 +13,6 @@ import { import { ApiProperty } from '@nestjs/swagger' import { ProjectViewType } from '../entity/project-view.entity' import { ProjectViewCustomEventMetaValueType } from '../entity/project-view-custom-event.entity' -import { TRAFFIC_COLUMNS } from '../../common/constants' export class ProjectViewCustomEventDto { @ApiProperty() @@ -48,14 +48,16 @@ export class ProjectViewCustomEventDto { } export interface Filter { - column: keyof typeof TRAFFIC_COLUMNS - filter: string - isExclusive: boolean - isContains?: boolean + dimension: string + operator: 'is' | 'is_not' | 'contains' | 'contains_not' + value: string | null | (string | null)[] + key?: string } export const MAX_METRICS_IN_VIEW = 3 +const MAX_FILTERS_IN_VIEW = 100 + export class CreateProjectViewDto { @ApiProperty() @MaxLength(20) @@ -71,11 +73,13 @@ export class CreateProjectViewDto { @ApiProperty({ description: - 'An array of properties to filter [{ column, filter, isExclusive, isContains }]', + 'An array of properties to filter [{ dimension, operator, value, key? }]', required: false, isArray: true, }) @IsOptional() + @IsArray() + @ArrayMaxSize(MAX_FILTERS_IN_VIEW) filters?: Filter[] @ApiProperty({ diff --git a/backend/apps/cloud/src/project/gsc.service.ts b/backend/apps/cloud/src/project/gsc.service.ts index 001967bc2..851c2996b 100644 --- a/backend/apps/cloud/src/project/gsc.service.ts +++ b/backend/apps/cloud/src/project/gsc.service.ts @@ -39,11 +39,17 @@ const ENCRYPTION_KEY = deriveKey('gsc-token') const MAX_ROW_LIMIT = 25000 const MAX_BRANDED_PAGES = 10 -const MAX_FILTER_EXPRESSION_LENGTH = 500 -const MAX_FILTERS = 20 -const MAX_BRANDED_TRAFFIC_RANGE_DAYS = 31 -const MAX_POSITION_ANALYTICS_RANGE_DAYS = 31 -const OPTIONAL_ANALYTICS_TIMEOUT_MS = 12000 +export const MAX_FILTER_EXPRESSION_LENGTH = 500 +export const MAX_FILTERS = 20 +export const MAX_BRANDED_TRAFFIC_RANGE_DAYS = 31 +export const MAX_POSITION_ANALYTICS_RANGE_DAYS = 31 +export const OPTIONAL_ANALYTICS_TIMEOUT_MS = 12000 + +/** + * Search Console keeps roughly 16 months of history, so an 'all' period is + * resolved against a fixed window rather than the project's first event. + */ +export const GSC_ALL_TIME_DAYS = 480 const IMPRESSION_POSITION_BUCKETS = [ { key: 'pos1To3', label: '1-3' }, @@ -67,13 +73,8 @@ type OrganicPositionSeriesEntry = { date: string } & Record< OrganicPositionBucketKey, number > -type BrandedTrafficAnalytics = { branded: number; nonBranded: number } -type SkippedBrandedAnalytics = { - skipped: true - branded: null - nonBranded: null -} -type PositionAnalytics = { +export type BrandedTrafficAnalytics = { branded: number; nonBranded: number } +export type PositionAnalytics = { impressionsByPosition: { key: ImpressionPositionBucketKey label: string @@ -82,23 +83,6 @@ type PositionAnalytics = { }[] organicPositions: OrganicPositionSeriesEntry[] } -type SkippedPositionAnalytics = { - skipped: true - impressionsByPosition: null - organicPositions: null -} - -const SKIPPED_BRANDED_ANALYTICS: SkippedBrandedAnalytics = { - skipped: true, - branded: null, - nonBranded: null, -} - -const SKIPPED_POSITION_ANALYTICS: SkippedPositionAnalytics = { - skipped: true, - impressionsByPosition: null, - organicPositions: null, -} const initialImpressionPositionBuckets = (): Record< ImpressionPositionBucketKey, @@ -140,32 +124,6 @@ const getOrganicPositionBucketKey = ( return 'pos51Plus' } -const resolveOptionalAnalytics = async ( - load: (signal: AbortSignal) => Promise, - fallback: T, -): Promise => { - const controller = new AbortController() - let timeout: ReturnType | undefined - - try { - return await Promise.race([ - load(controller.signal), - new Promise((resolve) => { - timeout = setTimeout(() => { - controller.abort() - resolve(fallback) - }, OPTIONAL_ANALYTICS_TIMEOUT_MS) - }), - ]) - } catch { - return fallback - } finally { - if (timeout) { - clearTimeout(timeout) - } - } -} - type GSCRow = { keys: string[] clicks?: number @@ -174,7 +132,7 @@ type GSCRow = { position?: number } -type GSCContext = { +export type GSCContext = { sc: ReturnType siteUrl: string } @@ -398,6 +356,23 @@ export class GSCService { return { connected, email: project?.gscAccountEmail || null } } + /** + * Whether the project can currently be queried: an account must be connected + * and one of its properties linked. Unlike getStatus this needs no account + * email, so it is safe to expose to anyone with project read access. + */ + async getConnectionState( + pid: string, + ): Promise<{ connected: boolean; property: string | null }> { + const tokens = await this.getStoredTokens(pid) + const connected = !_isEmpty(tokens?.refresh_token || tokens?.access_token) + + return { + connected, + property: connected ? tokens.property_uri || null : null, + } + } + private async getAuthedClientForPid(pid: string) { const tokens = await this.getStoredTokens(pid) if (_isEmpty(tokens)) { @@ -426,7 +401,7 @@ export class GSCService { return oauth2Client } - private async getGSCContext(pid: string): Promise { + async getGSCContext(pid: string): Promise { const tokens = await this.getStoredTokens(pid) if (_isEmpty(tokens) || _isEmpty(tokens.property_uri)) { throw new BadRequestException( @@ -529,31 +504,33 @@ export class GSCService { throw new BadRequestException('Invalid filters parameter') } - const { - column, - filter: value, - isExclusive, - isContains, - } = filter as { + const raw = filter as { + dimension?: unknown + operator?: unknown + value?: unknown column?: unknown filter?: unknown isExclusive?: unknown isContains?: unknown } - if (typeof column !== 'string' || typeof value !== 'string') { - throw new BadRequestException('Invalid filters parameter') + const isV2 = typeof raw.dimension === 'string' + const filterDimension = isV2 ? raw.dimension : raw.column + const value = isV2 ? raw.value : raw.filter + + if (typeof filterDimension !== 'string' || typeof value !== 'string') { + continue } if (value.length > MAX_FILTER_EXPRESSION_LENGTH) continue let dimension: string | undefined let expression = value - if (column === 'pg') { + if (filterDimension === 'page' || filterDimension === 'pg') { dimension = 'page' - } else if (column === 'keywords') { + } else if (filterDimension === 'keywords') { dimension = 'query' - } else if (column === 'country' || column === 'cc') { + } else if (filterDimension === 'country' || filterDimension === 'cc') { dimension = 'country' if (expression.length === 2) { const alpha3 = countries.alpha2ToAlpha3(expression.toUpperCase()) @@ -562,7 +539,7 @@ export class GSCService { } else { expression = expression.toLowerCase() } - } else if (column === 'device' || column === 'dv') { + } else if (filterDimension === 'device' || filterDimension === 'dv') { dimension = 'device' expression = expression.toUpperCase() } else { @@ -570,9 +547,17 @@ export class GSCService { } let operator = 'equals' - if (isExclusive) { - operator = isContains ? 'notContains' : 'notEquals' - } else if (isContains) { + if (isV2) { + if (raw.operator === 'is_not') { + operator = 'notEquals' + } else if (raw.operator === 'contains') { + operator = 'contains' + } else if (raw.operator === 'contains_not') { + operator = 'notContains' + } + } else if (raw.isExclusive) { + operator = raw.isContains ? 'notContains' : 'notEquals' + } else if (raw.isContains) { operator = 'contains' } @@ -1175,97 +1160,4 @@ export class GSCService { ) } } - - async getDashboard( - pid: string, - from: string, - to: string, - timeBucket?: string, - filtersStr?: string, - ) { - const connected = await this.isConnected(pid) - if (!connected) { - return { notConnected: true } - } - - const tokens = await this.getStoredTokens(pid) - if (_isEmpty(tokens.property_uri)) { - return { notConnected: true, noProperty: true } - } - - let ctx: GSCContext - try { - ctx = await this.getGSCContext(pid) - } catch { - return { notConnected: true } - } - - const fromDate = dayjs(from) - const toDate = dayjs(to) - const durationMs = toDate.diff(fromDate) - const rangeDays = - Math.abs(toDate.startOf('day').diff(fromDate.startOf('day'), 'day')) + 1 - const shouldLoadBrandedTraffic = rangeDays <= MAX_BRANDED_TRAFFIC_RANGE_DAYS - const shouldLoadPositionAnalytics = - rangeDays <= MAX_POSITION_ANALYTICS_RANGE_DAYS - const prevTo = fromDate - .subtract(1, 'millisecond') - .format('YYYY-MM-DD HH:mm:ss') - const prevFrom = fromDate - .subtract(durationMs, 'millisecond') - .format('YYYY-MM-DD HH:mm:ss') - - const [ - summary, - previousSummary, - dateSeries, - topPages, - topQueries, - topCountries, - topDevices, - brandedTraffic, - positionAnalytics, - ] = await Promise.all([ - this.getSummary(pid, from, to, filtersStr, ctx), - this.getSummary(pid, prevFrom, prevTo, filtersStr, ctx).catch(() => null), - this.getDateSeries(pid, from, to, timeBucket, filtersStr, ctx), - this.getTopPages(pid, from, to, 50, 0, filtersStr, undefined, ctx), - this.getKeywords(pid, from, to, 50, 0, filtersStr, undefined, ctx), - this.getTopCountries(pid, from, to, 50, 0, filtersStr, ctx), - this.getTopDevices(pid, from, to, 50, 0, filtersStr, ctx), - shouldLoadBrandedTraffic - ? resolveOptionalAnalytics< - BrandedTrafficAnalytics | SkippedBrandedAnalytics - >( - (signal) => - this.getBrandedTraffic(pid, from, to, filtersStr, ctx, signal), - SKIPPED_BRANDED_ANALYTICS, - ) - : Promise.resolve(SKIPPED_BRANDED_ANALYTICS), - shouldLoadPositionAnalytics - ? resolveOptionalAnalytics< - PositionAnalytics | SkippedPositionAnalytics - >( - (signal) => - this.getPositionAnalytics(pid, from, to, filtersStr, ctx, signal), - SKIPPED_POSITION_ANALYTICS, - ) - : Promise.resolve(SKIPPED_POSITION_ANALYTICS), - ]) - - return { - notConnected: false, - summary, - previousSummary, - dateSeries, - topPages, - topQueries, - topCountries, - topDevices, - brandedTraffic, - positionAnalyticsSkipped: 'skipped' in positionAnalytics, - impressionsByPosition: positionAnalytics.impressionsByPosition, - organicPositions: positionAnalytics.organicPositions, - } - } } diff --git a/backend/apps/cloud/src/project/project.service.ts b/backend/apps/cloud/src/project/project.service.ts index 9e2c0106e..d2d4f11fc 100644 --- a/backend/apps/cloud/src/project/project.service.ts +++ b/backend/apps/cloud/src/project/project.service.ts @@ -60,8 +60,7 @@ import { redisUserUsageinfoCacheTimeout, TRAFFIC_COLUMNS, EMAIL_ACTION_ENCRYPTION_KEY, - ALL_COLUMNS, - TRAFFIC_METAKEY_COLUMNS, + V2_VIEW_FILTER_DIMENSIONS, } from '../common/constants' import { clickhouse } from '../common/integrations/clickhouse' import { IUsageInfoRedis } from '../user/interfaces' @@ -1549,11 +1548,8 @@ export class ProjectService { return [] } - return _filter( - filters, - ({ column }) => - _includes(ALL_COLUMNS, column as string) || - _includes(TRAFFIC_METAKEY_COLUMNS, column as string), + return _filter(filters, ({ dimension }) => + _includes(V2_VIEW_FILTER_DIMENSIONS, dimension), ) } diff --git a/backend/apps/cloud/tsconfig.app.json b/backend/apps/cloud/tsconfig.app.json index 3818f9d2d..536502cbc 100644 --- a/backend/apps/cloud/tsconfig.app.json +++ b/backend/apps/cloud/tsconfig.app.json @@ -7,7 +7,7 @@ "esModuleInterop": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "target": "es2017", + "target": "es2022", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", diff --git a/backend/apps/community/src/analytics/analytics.controller.ts b/backend/apps/community/src/analytics/analytics.controller.ts index 9b16c8137..61bc50b40 100644 --- a/backend/apps/community/src/analytics/analytics.controller.ts +++ b/backend/apps/community/src/analytics/analytics.controller.ts @@ -35,19 +35,19 @@ import { DataType, getLowestPossibleTimeBucket, } from './analytics.service' +import { normalizeFiltersToV1Json } from './v2/query/filters.translator' import { VALID_PERIODS } from './decorators/validate-period.decorator' import { CurrentUserId } from '../auth/decorators/current-user-id.decorator' import { DEFAULT_TIMEZONE } from '../user/entities/user.entity' import { AuthenticationGuard } from '../auth/guards/authentication.guard' import { PageviewsDto } from './dto/pageviews.dto' import { EventsDto } from './dto/events.dto' -import { GetDataDto, ChartRenderMode, TimeBucketType } from './dto/getData.dto' +import { GetDataDto, ChartRenderMode } from './dto/getData.dto' import { GetFiltersDto } from './dto/get-filters.dto' import { DataDeletionDto } from './dto/data-deletion.dto' import { GetVersionFiltersDto } from './dto/get-version-filters.dto' import { GetCustomEventMetadata } from './dto/get-custom-event-meta.dto' import { GetPagePropertyMetaDto } from './dto/get-page-property-meta.dto' -import { GetUserFlowDto } from './dto/getUserFlow.dto' import { GetJourneysDto } from './dto/get-journeys.dto' import { GetJourneySessionsDto } from './dto/get-journey-sessions.dto' import { GetFunnelsDto } from './dto/getFunnels.dto' @@ -57,13 +57,7 @@ import { clickhouse } from '../common/integrations/clickhouse' import { checkRateLimit, getIPDetails, getIPFromHeaders } from '../common/utils' import { GetCustomEventsDto } from './dto/get-custom-events.dto' import { GetBotStatsDto } from './dto/get-bot-stats.dto' -import { - IFunnel, - IGetFunnel, - IPageProperty, - IUserFlow, - PerfMeasure, -} from './interfaces' +import { IFunnel, IGetFunnel, IPageProperty, PerfMeasure } from './interfaces' import { GetSessionsDto } from './dto/get-sessions.dto' import { GetSessionDto } from './dto/get-session.dto' import { GetProfilesDto } from './dto/get-profiles.dto' @@ -85,7 +79,7 @@ import { GetHeartbeatStatsDto } from './dto/get-heartbeat-stats' import { LiveVisitorsDto } from './dto/live-visitors.dto' import { NoscriptDto } from './dto/noscript.dto' import { GetKeywordsDto } from './dto/get-keywords.dto' -import { GSCService } from '../project/gsc.service' +import { GSC_ALL_TIME_DAYS, GSCService } from '../project/gsc.service' import { ExperimentService } from '../experiment/experiment.service' import { getExperimentVariant } from '../feature-flag/evaluation' @@ -100,9 +94,6 @@ const BOT_RESPONSE = { message: 'Bot traffic detected, request is ignored' } const ONLINE_VISITORS_WINDOW_MINUTES = 5 // minutes -// Maximum range for "all time" GSC queries (~16 months, GSC's data retention) -const GSC_ALL_TIME_DAYS = 480 - // Performance object validator: none of the values cannot be bigger than 1000 * 60 * 5 (5 minutes) and are >= 0 const MAX_PERFORMANCE_VALUE = 1000 * 60 * 5 const isPerformanceValid = (perf: any) => { @@ -888,57 +879,6 @@ export class AnalyticsController { return { chart, appliedFilters: parsedFilters } } - @Get('user-flow') - @Auth(true, true) - async getUserFlow( - @Query() data: GetUserFlowDto, - @CurrentUserId() uid: string, - @Headers() headers: { 'x-password'?: string }, - ): Promise { - const { pid, period, from, to, timezone = DEFAULT_TIMEZONE, filters } = data - - await this.analyticsService.checkProjectAccess( - pid, - uid, - headers['x-password'], - ) - - this.logger.log( - `pid: ${pid}, period: ${period}`, - 'GET /analytics/user-flow', - ) - - let diff - - if (period === 'all') { - const res = await this.analyticsService.calculateTimeBucketForAllTime( - pid, - 'pageview', - ) - - diff = res.diff - } - - const safeTimezone = this.analyticsService.getSafeTimezone(timezone) - const { groupFrom, groupTo } = this.analyticsService.getGroupFromTo( - from, - to, - null, - period, - safeTimezone, - diff, - ) - - const [filtersQuery, filtersParams, parsedFilters] = - this.analyticsService.getFiltersQuery(filters, DataType.ANALYTICS, true) - - const params = { pid, groupFrom, groupTo, ...filtersParams } - - const flow = await this.analyticsService.getUserFlow(params, filtersQuery) - - return { ...flow, appliedFilters: parsedFilters } - } - @Get('journeys') @Auth(true, true) async getJourneys( @@ -984,7 +924,11 @@ export class AnalyticsController { ) const [filtersQuery, filtersParams, parsedFilters] = - this.analyticsService.getFiltersQuery(filters, DataType.ANALYTICS, true) + this.analyticsService.getFiltersQuery( + normalizeFiltersToV1Json(filters, 'traffic'), + DataType.ANALYTICS, + true, + ) const params = { pid, groupFrom, groupTo, ...filtersParams } @@ -1058,7 +1002,7 @@ export class AnalyticsController { ) const [filtersQuery, filtersParams] = this.analyticsService.getFiltersQuery( - filters || '[]', + normalizeFiltersToV1Json(filters, 'traffic'), DataType.ANALYTICS, ) const params = { pid, groupFrom, groupTo, ...filtersParams } @@ -1281,7 +1225,7 @@ export class AnalyticsController { return this.analyticsService.getDataDeletionPreview( body.pid, - body.filters || '[]', + normalizeFiltersToV1Json(body.filters, 'traffic'), body.from || null, body.to || null, ) @@ -1303,7 +1247,7 @@ export class AnalyticsController { await this.analyticsService.deleteData( body.pid, - body.filters || '[]', + normalizeFiltersToV1Json(body.filters, 'traffic'), body.from || null, body.to || null, body.types || ['pageview', 'custom_event'], @@ -2917,133 +2861,4 @@ export class AnalyticsController { const keywords = await this.gscService.getKeywords(pid, groupFrom, groupTo) return { keywords } } - - @Get('gsc-dashboard') - @Auth(true, true) - async getGSCDashboard( - @Query() data: GetKeywordsDto, - @CurrentUserId() uid: string, - @Headers() headers: { 'x-password'?: string }, - ) { - const { - pid, - period, - from, - to, - timezone = DEFAULT_TIMEZONE, - timeBucket, - filters, - } = data - - await this.analyticsService.checkProjectAccess( - pid, - uid, - headers['x-password'], - ) - - const defaultTimeBucket = ['1h', 'today', 'yesterday', '1d'].includes( - period, - ) - ? TimeBucketType.HOUR - : TimeBucketType.DAY - const finalTimeBucket = (timeBucket || defaultTimeBucket) as TimeBucketType - let groupTimeBucket: TimeBucketType | null = finalTimeBucket - if (period === 'all' || period === 'custom') { - groupTimeBucket = null - } - - this.logger.log( - `pid: ${pid}, period: ${period}, timeBucket: ${finalTimeBucket}, filters: ${filters}`, - 'GET /analytics/gsc-dashboard', - ) - - const diff = period === 'all' ? GSC_ALL_TIME_DAYS : undefined - - const safeTimezone = this.analyticsService.getSafeTimezone(timezone) - const { groupFrom, groupTo } = this.analyticsService.getGroupFromTo( - from, - to, - groupTimeBucket, - period, - safeTimezone, - diff, - ) - - return this.gscService.getDashboard( - pid, - groupFrom, - groupTo, - finalTimeBucket, - filters, - ) - } - - @Get('gsc-details') - @Auth(true, true) - async getGSCDetails( - @Query() data: GetKeywordsDto, - @CurrentUserId() uid: string, - @Headers() headers: { 'x-password'?: string }, - ) { - const { - pid, - period, - from, - to, - timezone = DEFAULT_TIMEZONE, - page, - query, - } = data - - this.logger.log( - `pid: ${pid}, period: ${period}, page: ${page}, query: ${query}`, - 'GET /analytics/gsc-details', - ) - - await this.analyticsService.checkProjectAccess( - pid, - uid, - headers['x-password'], - ) - - const diff = period === 'all' ? GSC_ALL_TIME_DAYS : undefined - - const safeTimezone = this.analyticsService.getSafeTimezone(timezone) - const { groupFrom, groupTo } = this.analyticsService.getGroupFromTo( - from, - to, - null, - period, - safeTimezone, - diff, - ) - - if (page) { - const keywords = await this.gscService.getKeywords( - pid, - groupFrom, - groupTo, - 50, - 0, - undefined, - page, - ) - return { type: 'queries', data: keywords } - } - - if (query) { - const pages = await this.gscService.getTopPages( - pid, - groupFrom, - groupTo, - 50, - 0, - undefined, - query, - ) - return { type: 'pages', data: pages } - } - - return { type: 'none', data: [] } - } } diff --git a/backend/apps/community/src/analytics/analytics.module.ts b/backend/apps/community/src/analytics/analytics.module.ts index 29fab9058..413e1b5df 100644 --- a/backend/apps/community/src/analytics/analytics.module.ts +++ b/backend/apps/community/src/analytics/analytics.module.ts @@ -3,21 +3,47 @@ import { Module, forwardRef } from '@nestjs/common' import { AnalyticsService } from './analytics.service' import { AnalyticsController } from './analytics.controller' import { BotDetectionService } from './bot-detection.service' +import { AnalyticsReadGuard } from './protection/analytics-read.guard' +import { PublicProjectCacheInterceptor } from './protection/public-project-cache.interceptor' import { HeartbeatGateway } from './heartbeat.gateway' import { SaltService } from './salt.service' import { AppLoggerModule } from '../logger/logger.module' import { ProjectModule } from '../project/project.module' import { ExperimentModule } from '../experiment/experiment.module' +import { AnalyticsV2Service } from './v2/analytics-v2.service' +import { SeoV2Service } from './v2/seo-v2.service' +import { TrafficV2Controller } from './v2/controllers/traffic-v2.controller' +import { PerformanceV2Controller } from './v2/controllers/performance-v2.controller' +import { CaptchaV2Controller } from './v2/controllers/captcha-v2.controller' +import { ErrorsV2Controller } from './v2/controllers/errors-v2.controller' +import { SessionsV2Controller } from './v2/controllers/sessions-v2.controller' +import { ProfilesV2Controller } from './v2/controllers/profiles-v2.controller' +import { ProjectV2Controller } from './v2/controllers/project-v2.controller' +import { SeoV2Controller } from './v2/controllers/seo-v2.controller' @Module({ imports: [AppLoggerModule, ProjectModule, forwardRef(() => ExperimentModule)], providers: [ AnalyticsService, + AnalyticsV2Service, + SeoV2Service, BotDetectionService, SaltService, HeartbeatGateway, + AnalyticsReadGuard, + PublicProjectCacheInterceptor, ], exports: [AnalyticsService, BotDetectionService, SaltService], - controllers: [AnalyticsController], + controllers: [ + AnalyticsController, + TrafficV2Controller, + PerformanceV2Controller, + CaptchaV2Controller, + ErrorsV2Controller, + SessionsV2Controller, + ProfilesV2Controller, + ProjectV2Controller, + SeoV2Controller, + ], }) export class AnalyticsModule {} diff --git a/backend/apps/community/src/analytics/analytics.service.ts b/backend/apps/community/src/analytics/analytics.service.ts index 87fe7e583..e6c43c064 100644 --- a/backend/apps/community/src/analytics/analytics.service.ts +++ b/backend/apps/community/src/analytics/analytics.service.ts @@ -80,12 +80,8 @@ import { TrafficCHResponse, IGetGroupFromTo, GetFiltersQuery, - IUserFlowNode, - IUserFlowLink, - IUserFlow, IJourney, IJourneys, - IBuildUserFlow, IExtractChartData, IGenerateXAxis, IAggregatedMetadata, @@ -129,6 +125,12 @@ const LIVE_SESSION_THRESHOLD_SECONDS = 120 const MAX_CONCURRENCY_WINDOW_MINUTES = 366 * 24 * 60 const MAX_FILTERS = 100 const MAX_FILTER_VALUES = 100 +// Journeys keep only the first {steps} distinct pages of a session, but the +// dedupe happens after aggregation, so the cap has to sit far above {steps}: +// a session that reloads one page N times still needs its later, different +// pages to survive. 1000 raw pageviews is orders of magnitude beyond any real +// session while keeping a bot's session array bounded. +const MAX_JOURNEY_PAGEVIEWS_PER_SESSION = 1000 // Event types that can be targeted by the data-deletion tool. const DELETABLE_EVENT_TYPES = [...DATA_DELETION_EVENT_TYPES] @@ -313,10 +315,13 @@ const mapErrorColumn = (type: string): string => ERROR_COLUMN_MAP[type] || type const captchaMetaValue = (key: string, fallback: string) => `if(indexOf(\`meta.key\`, '${key}') = 0, '${fallback}', arrayElement(\`meta.value\`, indexOf(\`meta.key\`, '${key}')))` -const CAPTCHA_EVENT_SQL = captchaMetaValue('captcha_event', 'pass') -const CAPTCHA_DIFFICULTY_SQL = captchaMetaValue('captcha_difficulty', '0') -const CAPTCHA_REASON_SQL = captchaMetaValue('captcha_reason', '') -const CAPTCHA_SOLVE_MS_SQL = `toUInt32OrZero(${captchaMetaValue('solve_ms', '0')})` +export const CAPTCHA_EVENT_SQL = captchaMetaValue('captcha_event', 'pass') +export const CAPTCHA_DIFFICULTY_SQL = captchaMetaValue( + 'captcha_difficulty', + '0', +) +export const CAPTCHA_REASON_SQL = captchaMetaValue('captcha_reason', '') +export const CAPTCHA_SOLVE_MS_SQL = `toUInt32OrZero(${captchaMetaValue('solve_ms', '0')})` const CAPTCHA_SOLVE_TIME_SQL = `multiIf(${CAPTCHA_SOLVE_MS_SQL} = 0, 'unknown', ${CAPTCHA_SOLVE_MS_SQL} < 1000, '<1s', ${CAPTCHA_SOLVE_MS_SQL} < 3000, '1-3s', ${CAPTCHA_SOLVE_MS_SQL} < 10000, '3-10s', ${CAPTCHA_SOLVE_MS_SQL} < 30000, '10-30s', '30s+')` const CAPTCHA_META_COLUMN_MAP: Record = { @@ -326,7 +331,7 @@ const CAPTCHA_META_COLUMN_MAP: Record = { solve_time: CAPTCHA_SOLVE_TIME_SQL, } -const getCaptchaColumnExpression = (col: string): string => +export const getCaptchaColumnExpression = (col: string): string => CAPTCHA_META_COLUMN_MAP[col] || col const generateParamsQuery = ( @@ -944,100 +949,6 @@ export class AnalyticsService { } } - removeCyclicDependencies(links: IUserFlowLink[]): IUserFlowLink[] { - const visited = new Set() - - return links.filter((link) => { - const key = `${link.source}_${link.target}` - if (visited.has(key)) { - return false - } - visited.add(key) - return true - }) - } - - buildUserFlow(links: IUserFlowLink[]): IBuildUserFlow { - const nodes: IUserFlowNode[] = Array.from( - new Set( - links - .map((link: IUserFlowLink) => link.source) - .concat(links.map((link: IUserFlowLink) => link.target)), - ), - ).map((node: any) => ({ id: node })) - - return { nodes, links } - } - - async getUserFlow( - params: Record, - filtersQuery: string, - ): Promise { - const query = ` - WITH page_sequences AS ( - SELECT - psid, - pg, - created, - lagInFrame(pg) OVER (PARTITION BY psid ORDER BY created) AS prev_page - FROM events - WHERE - pid = {pid:FixedString(12)} - AND type = 'pageview' - AND created BETWEEN {groupFrom:String} AND {groupTo:String} - AND pg IS NOT NULL - ${filtersQuery} - ) - SELECT - prev_page AS source, - pg AS target, - count() AS value - FROM page_sequences - WHERE prev_page IS NOT NULL - AND prev_page != pg - GROUP BY - source, - target - ORDER BY value DESC - ` - - const { data } = await clickhouse - .query({ - query, - query_params: params, - }) - .then((res) => res.json()) - - if (_isEmpty(data)) { - const empty = { nodes: [], links: [] } - return { - ascending: empty, - descending: empty, - } - } - - const ascendingLinks: IUserFlowLink[] = [] - const descendingLinks: IUserFlowLink[] = [] - - this.removeCyclicDependencies(data).forEach((row: any) => { - const link: IUserFlowLink = { - source: row.source, - target: row.target, - value: row.value, - } - if (link.source < link.target) { - ascendingLinks.push(link) - } else { - descendingLinks.push(link) - } - }) - - return { - ascending: this.buildUserFlow(ascendingLinks), - descending: this.buildUserFlow(descendingLinks), - } - } - async getJourneys( params: Record, filtersQuery: string, @@ -1065,7 +976,12 @@ export class AnalyticsService { SELECT psid, arraySlice( - arrayCompact(arrayMap(x -> x.2, arraySort(groupArray((created, pg))))), + arrayCompact( + arrayMap( + x -> x.2, + groupArraySorted({maxPageviews:UInt32})((created, pg)) + ) + ), 1, {steps:UInt32} ) AS path @@ -1106,7 +1022,12 @@ export class AnalyticsService { const { data } = await clickhouse .query({ query, - query_params: { ...params, steps, journeys }, + query_params: { + ...params, + steps, + journeys, + maxPageviews: MAX_JOURNEY_PAGEVIEWS_PER_SESSION, + }, }) .then((res) => res.json<{ path: string[]; value: string; totalSessions: string }>(), diff --git a/backend/apps/community/src/analytics/dto/getUserFlow.dto.ts b/backend/apps/community/src/analytics/dto/getUserFlow.dto.ts deleted file mode 100644 index 832756966..000000000 --- a/backend/apps/community/src/analytics/dto/getUserFlow.dto.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger' -import { IsNotEmpty, IsOptional, Matches } from 'class-validator' -import { DEFAULT_TIMEZONE } from '../../user/entities/user.entity' -import { ValidatePeriod } from '../decorators/validate-period.decorator' -import { PID_REGEX } from '../../common/constants' - -export class GetUserFlowDto { - @ApiProperty({ - example: 'aUn1quEid-3', - required: true, - description: 'The project ID', - }) - @IsNotEmpty() - @Matches(PID_REGEX, { message: 'The provided Project ID (pid) is incorrect' }) - pid: string - - @ApiProperty({ required: false }) - @IsOptional() - @ValidatePeriod() - period?: string - - @ApiProperty() - from: string - - @ApiProperty() - to: string - - @ApiProperty({ - description: 'Timezone to display data in', - default: DEFAULT_TIMEZONE, - }) - timezone: string - - @ApiProperty({ - description: - 'A stringified array of properties to filter [{ column, filter, isExclusive, isContains }]', - }) - filters: string -} diff --git a/backend/apps/community/src/analytics/interfaces/index.ts b/backend/apps/community/src/analytics/interfaces/index.ts index b874c07d6..a0279566b 100644 --- a/backend/apps/community/src/analytics/interfaces/index.ts +++ b/backend/apps/community/src/analytics/interfaces/index.ts @@ -70,31 +70,11 @@ export interface GetFiltersQuery extends Array { 3: boolean } -export interface IUserFlowNode { - id: string -} - -export interface IUserFlowLink { - source: string - target: string - value: number -} - export interface IGenerateXAxis { x: string[] xShifted: string[] } -export interface IBuildUserFlow { - nodes: IUserFlowNode[] - links: IUserFlowLink[] -} - -export interface IUserFlow { - ascending: IBuildUserFlow - descending: IBuildUserFlow -} - export interface IJourney { path: string[] value: number diff --git a/backend/apps/community/src/analytics/protection/analytics-read.guard.ts b/backend/apps/community/src/analytics/protection/analytics-read.guard.ts new file mode 100644 index 000000000..6c4aec190 --- /dev/null +++ b/backend/apps/community/src/analytics/protection/analytics-read.guard.ts @@ -0,0 +1,105 @@ +import { + CanActivate, + ExecutionContext, + ForbiddenException, + Injectable, +} from '@nestjs/common' +import { isbot } from 'isbot' + +import { ProjectService } from '../../project/project.service' +import { checkRateLimit, getIPDetails } from '../../common/utils' +import { AppLoggerService } from '../../logger/logger.service' +import { getTrustworthyIp, getSinglePid } from './analytics-read.util' + +const READ_RATE_LIMIT = 300 +const AUTHED_READ_RATE_LIMIT = 3000 +const READ_RATE_WINDOW = 60 + +const BLOCK_DATACENTER_READS = + process.env.ANALYTICS_BLOCK_DATACENTER_READS === 'true' + +@Injectable() +export class AnalyticsReadGuard implements CanActivate { + constructor( + private readonly projectService: ProjectService, + private readonly logger: AppLoggerService, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest() + + if (req.method !== 'GET') { + return true + } + + if (req.headers['x-api-key']) { + return true + } + + const uid = req.user?.sub || req.user?.id || null + if (uid) { + if (AUTHED_READ_RATE_LIMIT > 0) { + await checkRateLimit( + String(uid), + 'analytics-read-user', + AUTHED_READ_RATE_LIMIT, + READ_RATE_WINDOW, + ) + } + return true + } + + let isPublicProject = false + try { + const pid = getSinglePid(req.query || {}, req.params || {}) + if (pid) { + const project = await this.projectService.getRedisProject(pid) + isPublicProject = Boolean(project?.public) + } + } catch (reason) { + this.logger.warn( + `AnalyticsReadGuard soft-failed: ${reason}`, + 'AnalyticsReadGuard', + ) + return true + } + + if (!isPublicProject) { + return true + } + + const userAgent = String(req.headers['user-agent'] || '') + if (userAgent && isbot(userAgent)) { + throw new ForbiddenException( + 'Automated traffic is not permitted for this resource', + ) + } + + const ip = getTrustworthyIp(req) + + if (BLOCK_DATACENTER_READS && ip) { + try { + if (getIPDetails(ip).isHosting) { + throw new ForbiddenException( + 'Automated traffic is not permitted for this resource', + ) + } + } catch (reason) { + if (reason instanceof ForbiddenException) { + throw reason + } + } + } + + if (READ_RATE_LIMIT && ip) { + await checkRateLimit( + ip, + 'analytics-read', + READ_RATE_LIMIT, + READ_RATE_WINDOW, + ) + } + + return true + } +} diff --git a/backend/apps/community/src/analytics/protection/analytics-read.util.ts b/backend/apps/community/src/analytics/protection/analytics-read.util.ts new file mode 100644 index 000000000..3c77aea97 --- /dev/null +++ b/backend/apps/community/src/analytics/protection/analytics-read.util.ts @@ -0,0 +1,79 @@ +import _isString from 'lodash/isString' + +import { PID_REGEX } from '../../common/constants' +import { getIPFromHeaders } from '../../common/utils' + +const TRUSTED_PROXY_IPS = new Set( + (process.env.TRUSTED_PROXY_IPS || '') + .split(',') + .map((entry) => entry.trim()) + .filter(Boolean), +) + +const firstHeaderValue = (value: unknown): string => { + if (_isString(value) && value) { + return value.trim() + } + if (Array.isArray(value) && _isString(value[0]) && value[0]) { + return value[0].trim() + } + return '' +} + +export const getTrustworthyIp = (req: { + headers: Record + ip?: string +}): string => { + const connectionIp = firstHeaderValue(req.headers['x-real-ip']) + + if (connectionIp && TRUSTED_PROXY_IPS.has(connectionIp)) { + return firstHeaderValue(req.headers['x-client-ip-address']) + } + + if (connectionIp) { + return connectionIp + } + + return getIPFromHeaders(req.headers) || req.ip || '' +} + +export const getSinglePid = ( + query: Record, + params?: Record, +): string | null => { + const paramPid = params?.pid + + if (_isString(paramPid) && PID_REGEX.test(paramPid)) { + return paramPid + } + + const { pid, pids } = query + + if (pid !== undefined && pids !== undefined) { + return null + } + + if (_isString(pid) && pid) { + return pid + } + + if (_isString(pids) && pids) { + try { + const parsed = JSON.parse(pids) + if ( + Array.isArray(parsed) && + parsed.length === 1 && + _isString(parsed[0]) + ) { + return parsed[0] + } + } catch { + return null + } + } + + return null +} + +export const getAnalyticsRoute = (path: string): string => + path.replace(/^\/(v1\/)?log\/?/, '').replace(/\/$/, '') diff --git a/backend/apps/community/src/analytics/protection/cacheable-analytics.decorator.ts b/backend/apps/community/src/analytics/protection/cacheable-analytics.decorator.ts new file mode 100644 index 000000000..8a69a7d25 --- /dev/null +++ b/backend/apps/community/src/analytics/protection/cacheable-analytics.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from '@nestjs/common' + +export const CACHEABLE_ANALYTICS_KEY = 'cacheable_analytics' + +export const CacheableAnalytics = () => + SetMetadata(CACHEABLE_ANALYTICS_KEY, true) diff --git a/backend/apps/community/src/analytics/protection/public-project-cache.interceptor.ts b/backend/apps/community/src/analytics/protection/public-project-cache.interceptor.ts new file mode 100644 index 000000000..431b281e7 --- /dev/null +++ b/backend/apps/community/src/analytics/protection/public-project-cache.interceptor.ts @@ -0,0 +1,116 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common' +import { Reflector } from '@nestjs/core' +import { Observable, of } from 'rxjs' +import { tap } from 'rxjs/operators' + +import { ProjectService } from '../../project/project.service' +import { AppLoggerService } from '../../logger/logger.service' +import { redis } from '../../common/constants' +import { getSinglePid, getAnalyticsRoute } from './analytics-read.util' +import { CACHEABLE_ANALYTICS_KEY } from './cacheable-analytics.decorator' + +const CACHE_TTL = Number(process.env.PUBLIC_PROJECT_CACHE_TTL) || 10 + +const CACHE_PREFIX = 'arc' + +const CACHEABLE_ROUTES = new Set([ + '', + 'chart', + 'birdseye', + 'performance', + 'performance/chart', + 'performance/birdseye', + 'sessions', + 'profiles', + 'errors', + 'error-overview', + 'funnel', + 'funnel-sessions', + 'custom-events', + 'keywords', +]) + +@Injectable() +export class PublicProjectCacheInterceptor implements NestInterceptor { + constructor( + private readonly projectService: ProjectService, + private readonly logger: AppLoggerService, + private readonly reflector: Reflector, + ) {} + + async intercept( + context: ExecutionContext, + next: CallHandler, + ): Promise> { + const req = context.switchToHttp().getRequest() + + if (req.method !== 'GET') { + return next.handle() + } + + const isCacheableV2 = this.reflector.get( + CACHEABLE_ANALYTICS_KEY, + context.getHandler(), + ) + + if (!isCacheableV2) { + const route = getAnalyticsRoute(req.path || '') + if (!CACHEABLE_ROUTES.has(route)) { + return next.handle() + } + } + + const pid = getSinglePid(req.query || {}, req.params || {}) + if (!pid) { + return next.handle() + } + + let isPublic = false + try { + const project = await this.projectService.getRedisProject(pid) + isPublic = Boolean(project?.public) + } catch { + return next.handle() + } + + if (!isPublic) { + return next.handle() + } + + const key = `${CACHE_PREFIX}:${req.originalUrl}` + + try { + const cached = await redis.get(key) + if (cached !== null) { + return of(JSON.parse(cached)) + } + } catch (reason) { + this.logger.warn( + `PublicProjectCacheInterceptor read failed: ${reason}`, + 'PublicProjectCacheInterceptor', + ) + return next.handle() + } + + return next.handle().pipe( + tap((body) => { + if (body === undefined || body === null) { + return + } + redis + .set(key, JSON.stringify(body), 'EX', CACHE_TTL) + .catch((reason) => { + this.logger.warn( + `PublicProjectCacheInterceptor write failed: ${reason}`, + 'PublicProjectCacheInterceptor', + ) + }) + }), + ) + } +} diff --git a/backend/apps/community/src/analytics/v2/__tests__/breakdown-query.builder.spec.ts b/backend/apps/community/src/analytics/v2/__tests__/breakdown-query.builder.spec.ts new file mode 100644 index 000000000..710fae981 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/__tests__/breakdown-query.builder.spec.ts @@ -0,0 +1,157 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { + buildBreakdownQuery, + parseSortParam, +} from '../query/breakdown-query.builder' +import { + getBreakdownDimension, + getMetric, + parseMetricsParam, +} from '../registry' + +const SUB_QUERY = `FROM events WHERE pid = {pid:FixedString(12)} AND type = 'pageview' AND created BETWEEN {groupFrom:String} AND {groupTo:String}` + +const normalize = (sql: string) => sql.replace(/\s+/g, ' ').trim() + +describe('parseSortParam', () => { + const dimension = getBreakdownDimension('country', 'traffic') + const metrics = parseMetricsParam('visitors,pageviews', 'traffic') + + it('defaults to first metric desc', () => { + expect(parseSortParam(undefined, dimension, metrics)).toEqual({ + field: 'visitors', + direction: 'desc', + }) + }) + + it('parses field:direction and validates both', () => { + expect(parseSortParam('pageviews:asc', dimension, metrics)).toEqual({ + field: 'pageviews', + direction: 'asc', + }) + expect(parseSortParam('value:asc', dimension, metrics)).toEqual({ + field: 'value', + direction: 'asc', + }) + expect(() => parseSortParam('visitors:up', dimension, metrics)).toThrow( + UnprocessableEntityException, + ) + expect(() => parseSortParam('users:desc', dimension, metrics)).toThrow( + UnprocessableEntityException, + ) + }) +}) + +describe('buildBreakdownQuery', () => { + it('builds a multi-metric traffic query with pagination and total', () => { + const query = normalize( + buildBreakdownQuery({ + dataType: 'traffic', + dimension: getBreakdownDimension('country', 'traffic'), + metrics: parseMetricsParam('visitors,pageviews', 'traffic'), + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false }, + sort: { field: 'visitors', direction: 'desc' }, + }), + ) + + expect(query).toContain('cc AS value') + expect(query).toContain('count(DISTINCT psid) AS visitors') + expect(query).toContain('count(*) AS pageviews') + expect(query).toContain('count() OVER () AS __total') + expect(query).toContain('GROUP BY value') + expect(query).toContain('ORDER BY visitors desc') + expect(query).toContain('LIMIT {v2_limit:UInt32} OFFSET {v2_offset:UInt32}') + }) + + it('degrades visitors to count(*) under custom-event filters (v1 parity)', () => { + const query = buildBreakdownQuery({ + dataType: 'traffic', + dimension: getBreakdownDimension('country', 'traffic'), + metrics: [getMetric('visitors', 'traffic')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: true }, + sort: { field: 'visitors', direction: 'desc' }, + }) + + expect(query).toContain('count(*) AS visitors') + expect(query).not.toContain('count(DISTINCT psid)') + }) + + it('includes extra fields for region and groups by them', () => { + const query = normalize( + buildBreakdownQuery({ + dataType: 'traffic', + dimension: getBreakdownDimension('region', 'traffic'), + metrics: [getMetric('visitors', 'traffic')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false }, + sort: { field: 'visitors', direction: 'desc' }, + }), + ) + + expect(query).toContain('rg AS value') + expect(query).toContain('cc AS country') + expect(query).toContain('rgc AS region_code') + expect(query).toContain('GROUP BY value, country, region_code') + expect(query).toContain('AND rg IS NOT NULL') + }) + + it('applies the measure to performance metrics', () => { + const query = buildBreakdownQuery({ + dataType: 'performance', + dimension: getBreakdownDimension('page', 'performance'), + metrics: [getMetric('load_time', 'performance')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false, measure: 'p95' }, + sort: { field: 'load_time', direction: 'desc' }, + }) + + expect(query).toContain( + 'round(divide(quantileExact(0.95)(pageLoad), 1000), 2) AS load_time', + ) + }) + + it('uses captcha meta-column expressions and per-dimension guards', () => { + const query = normalize( + buildBreakdownQuery({ + dataType: 'captcha', + dimension: getBreakdownDimension('solve_time', 'captcha'), + metrics: [getMetric('events', 'captcha')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false }, + sort: { field: 'events', direction: 'desc' }, + }), + ) + + expect(query).toContain('multiIf(') + expect(query).toContain("= 'pass'") + + const countryQuery = normalize( + buildBreakdownQuery({ + dataType: 'captcha', + dimension: getBreakdownDimension('country', 'captcha'), + metrics: [getMetric('events', 'captcha')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false }, + sort: { field: 'events', direction: 'desc' }, + }), + ) + + expect(countryQuery).toContain("= 'pass'") + }) + + it('rejects sessions-join metrics with a 422', () => { + expect(() => + buildBreakdownQuery({ + dataType: 'traffic', + dimension: getBreakdownDimension('country', 'traffic'), + metrics: [getMetric('bounce_rate', 'traffic')], + subQuery: SUB_QUERY, + ctx: { customEVFilterApplied: false }, + sort: { field: 'bounce_rate', direction: 'desc' }, + }), + ).toThrow(UnprocessableEntityException) + }) +}) diff --git a/backend/apps/community/src/analytics/v2/__tests__/filters.translator.spec.ts b/backend/apps/community/src/analytics/v2/__tests__/filters.translator.spec.ts new file mode 100644 index 000000000..7039e6cb6 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/__tests__/filters.translator.spec.ts @@ -0,0 +1,211 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { + normalizeFiltersToV1Json, + parseV2Filters, + toV1FiltersJson, +} from '../query/filters.translator' + +describe('parseV2Filters', () => { + it('returns [] for empty input', () => { + expect(parseV2Filters(undefined)).toEqual([]) + expect(parseV2Filters('')).toEqual([]) + expect(parseV2Filters('""')).toEqual([]) + }) + + it('parses valid filters', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'country', operator: 'is', value: ['US', 'DE'] }, + { dimension: 'page', operator: 'contains', value: '/blog' }, + ]), + ) + + expect(filters).toHaveLength(2) + expect(filters[0]).toEqual({ + dimension: 'country', + operator: 'is', + value: ['US', 'DE'], + }) + }) + + it('rejects invalid JSON, non-arrays, bad operators and values', () => { + expect(() => parseV2Filters('not json')).toThrow( + UnprocessableEntityException, + ) + expect(() => parseV2Filters('{"dimension":"country"}')).toThrow( + UnprocessableEntityException, + ) + expect(() => + parseV2Filters( + JSON.stringify([ + { dimension: 'country', operator: 'equals', value: 'US' }, + ]), + ), + ).toThrow(UnprocessableEntityException) + expect(() => + parseV2Filters( + JSON.stringify([{ dimension: 'country', operator: 'is', value: 42 }]), + ), + ).toThrow(UnprocessableEntityException) + }) + + it('accepts null values (filter for unknown)', () => { + const filters = parseV2Filters( + JSON.stringify([{ dimension: 'region', operator: 'is', value: null }]), + ) + + expect(filters[0].value).toBeNull() + }) +}) + +describe('toV1FiltersJson', () => { + it('translates operators to isExclusive/isContains and columns to v1 codes', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'country', operator: 'is', value: 'US' }, + { dimension: 'page', operator: 'contains', value: '/blog' }, + { dimension: 'browser', operator: 'is_not', value: 'Chrome' }, + { dimension: 'referrer', operator: 'contains_not', value: 'google' }, + ]), + ) + + const v1 = JSON.parse(toV1FiltersJson(filters, 'traffic')) + + expect(v1).toEqual([ + { column: 'cc', filter: 'US', isExclusive: false, isContains: false }, + { column: 'pg', filter: '/blog', isExclusive: false, isContains: true }, + { column: 'br', filter: 'Chrome', isExclusive: true, isContains: false }, + { + column: 'ref', + filter: 'google', + isExclusive: true, + isContains: true, + }, + ]) + }) + + it('translates keyed metadata filters to ev:key: / tag:key:', () => { + const filters = parseV2Filters( + JSON.stringify([ + { + dimension: 'event_metadata', + key: 'plan', + operator: 'is_not', + value: 'free', + }, + { + dimension: 'page_property', + key: 'author', + operator: 'is', + value: 'Andrii', + }, + { dimension: 'event_metadata', operator: 'is', value: 'plan' }, + ]), + ) + + const v1 = JSON.parse(toV1FiltersJson(filters, 'traffic')) + + expect(v1[0].column).toBe('ev:key:plan') + expect(v1[0].isExclusive).toBe(true) + expect(v1[1].column).toBe('tag:key:author') + expect(v1[2].column).toBe('ev:key') + }) + + it('maps errors-only dimensions through v1 public names', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'error_name', operator: 'is', value: 'TypeError' }, + ]), + ) + + const v1 = JSON.parse(toV1FiltersJson(filters, 'errors')) + + expect(v1[0].column).toBe('name') + }) + + it('rejects unknown dimensions strictly, drops them in lenient mode', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'error_name', operator: 'is', value: 'TypeError' }, + { dimension: 'country', operator: 'is', value: 'US' }, + ]), + ) + + expect(() => toV1FiltersJson(filters, 'traffic')).toThrow( + UnprocessableEntityException, + ) + + const lenient = JSON.parse( + toV1FiltersJson(filters, 'traffic', { lenient: true }), + ) + + expect(lenient).toHaveLength(1) + expect(lenient[0].column).toBe('cc') + }) + + it('keeps array values intact (v1 expands them internally)', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'country', operator: 'is', value: ['US', null] }, + ]), + ) + + const v1 = JSON.parse(toV1FiltersJson(filters, 'traffic')) + + expect(v1[0].filter).toEqual(['US', null]) + }) + + it('translates entry_page / exit_page / referrer_name / event filters', () => { + const filters = parseV2Filters( + JSON.stringify([ + { dimension: 'entry_page', operator: 'is', value: '/' }, + { dimension: 'exit_page', operator: 'is', value: '/bye' }, + { dimension: 'referrer_name', operator: 'is', value: 'google.com' }, + { dimension: 'event', operator: 'is', value: 'signup' }, + ]), + ) + + const v1 = JSON.parse(toV1FiltersJson(filters, 'traffic')) + + expect(v1.map((filter: { column: string }) => filter.column)).toEqual([ + 'entryPage', + 'exitPage', + 'refn', + 'ev', + ]) + }) +}) + +describe('normalizeFiltersToV1Json', () => { + it('returns "" for empty input', () => { + expect(normalizeFiltersToV1Json(undefined, 'traffic')).toBe('') + expect(normalizeFiltersToV1Json('', 'traffic')).toBe('') + expect(normalizeFiltersToV1Json('""', 'traffic')).toBe('') + expect(normalizeFiltersToV1Json('[]', 'traffic')).toBe('') + }) + + it('passes legacy v1 filters through untouched (public API back-compat)', () => { + const v1 = JSON.stringify([ + { column: 'cc', filter: 'US', isExclusive: false, isContains: false }, + ]) + + expect(normalizeFiltersToV1Json(v1, 'traffic')).toBe(v1) + }) + + it('translates v2 filters into the v1 shape', () => { + const v2 = JSON.stringify([ + { dimension: 'country', operator: 'is_not', value: 'US' }, + ]) + + const v1 = JSON.parse(normalizeFiltersToV1Json(v2, 'traffic')) + + expect(v1).toEqual([ + { column: 'cc', filter: 'US', isExclusive: true, isContains: false }, + ]) + }) + + it('returns malformed input unchanged for getFiltersQuery to handle', () => { + expect(normalizeFiltersToV1Json('not json', 'traffic')).toBe('not json') + }) +}) diff --git a/backend/apps/community/src/analytics/v2/__tests__/registry.spec.ts b/backend/apps/community/src/analytics/v2/__tests__/registry.spec.ts new file mode 100644 index 000000000..34ae77bfc --- /dev/null +++ b/backend/apps/community/src/analytics/v2/__tests__/registry.spec.ts @@ -0,0 +1,137 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { + getBreakdownDimension, + getDefaultMetrics, + getMetric, + listDimensions, + parseMetricsParam, + V2_DIMENSIONS, + V2_KEYED_FILTER_DIMENSIONS, +} from '../registry' + +const { + TRAFFIC_COLUMNS, + PERFORMANCE_COLUMNS, + ERROR_COLUMNS, + CAPTCHA_COLUMNS, + V2_VIEW_FILTER_DIMENSIONS, + // eslint-disable-next-line @typescript-eslint/no-require-imports +} = require('../../../common/constants') + +describe('registry <-> v1 column allowlist lockstep', () => { + const columnsFor = (type: 'traffic' | 'performance' | 'captcha' | 'errors') => + V2_DIMENSIONS.filter( + (dimension) => dimension.types.includes(type) && !dimension.virtual, + ).map((dimension) => dimension.column) + + it('covers every v1 traffic column', () => { + for (const column of TRAFFIC_COLUMNS) { + expect(columnsFor('traffic')).toContain(column) + } + }) + + it('covers every v1 performance column', () => { + for (const column of PERFORMANCE_COLUMNS) { + expect(columnsFor('performance')).toContain(column) + } + }) + + it('covers every v1 error column', () => { + for (const column of ERROR_COLUMNS) { + expect(columnsFor('errors')).toContain(column) + } + }) + + it('covers every v1 captcha column', () => { + for (const column of CAPTCHA_COLUMNS) { + expect(columnsFor('captcha')).toContain(column) + } + }) +}) + +describe('V2_VIEW_FILTER_DIMENSIONS <-> registry parity', () => { + const expected = [ + ...V2_DIMENSIONS.filter((dimension) => + dimension.types.some((type) => type !== 'captcha'), + ).map((dimension) => dimension.api), + ...Object.keys(V2_KEYED_FILTER_DIMENSIONS), + ] + + it('matches every non-captcha registry dimension exactly', () => { + expect([...V2_VIEW_FILTER_DIMENSIONS].sort()).toEqual([...expected].sort()) + }) +}) + +describe('dimension lookups', () => { + it('resolves valid breakdown dimensions', () => { + expect(getBreakdownDimension('country', 'traffic').column).toBe('cc') + expect(getBreakdownDimension('page', 'performance').column).toBe('pg') + }) + + it('rejects unknown and filter-only dimensions with 422', () => { + expect(() => getBreakdownDimension('nope', 'traffic')).toThrow( + UnprocessableEntityException, + ) + expect(() => getBreakdownDimension('referrer_name', 'traffic')).toThrow( + UnprocessableEntityException, + ) + expect(() => getBreakdownDimension('event', 'traffic')).toThrow( + UnprocessableEntityException, + ) + expect(() => getBreakdownDimension('referrer', 'performance')).toThrow( + UnprocessableEntityException, + ) + }) + + it('has unique api names per data type', () => { + for (const type of [ + 'traffic', + 'performance', + 'captcha', + 'errors', + ] as const) { + const names = listDimensions(type).map((dimension) => dimension.api) + expect(new Set(names).size).toBe(names.length) + } + }) +}) + +describe('metric lookups', () => { + it('resolves metrics and rejects unknown ones', () => { + expect(getMetric('visitors', 'traffic').api).toBe('visitors') + expect(() => getMetric('visitors', 'errors')).toThrow( + UnprocessableEntityException, + ) + }) + + it('provides sensible defaults per data type', () => { + expect(getDefaultMetrics('traffic').map((metric) => metric.api)).toEqual([ + 'visitors', + 'pageviews', + ]) + expect( + getDefaultMetrics('performance').map((metric) => metric.api), + ).toEqual(['load_time']) + expect(getDefaultMetrics('errors').map((metric) => metric.api)).toEqual([ + 'occurrences', + ]) + }) + + it('parses and dedupes the metrics CSV param', () => { + expect( + parseMetricsParam('visitors, pageviews,visitors', 'traffic').map( + (metric) => metric.api, + ), + ).toEqual(['visitors', 'pageviews']) + + expect(parseMetricsParam(undefined, 'traffic').map((m) => m.api)).toEqual([ + 'visitors', + 'pageviews', + ]) + + expect(() => parseMetricsParam('bogus', 'traffic')).toThrow( + UnprocessableEntityException, + ) + }) +}) diff --git a/backend/apps/community/src/analytics/v2/__tests__/seo.mapper.spec.ts b/backend/apps/community/src/analytics/v2/__tests__/seo.mapper.spec.ts new file mode 100644 index 000000000..d90e11b99 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/__tests__/seo.mapper.spec.ts @@ -0,0 +1,210 @@ +import { TimeBucketType } from '../../dto/getData.dto' +import { + bucketSeoDateSeries, + gscCountryToAlpha2, + mapSeoBreakdownRows, + mapSeoSummary, + mapSeoTimeseries, + SeoDateSeriesEntry, +} from '../mappers/seo.mapper' + +const day = ( + date: string, + clicks: number, + impressions: number, + position: number, +): SeoDateSeriesEntry => ({ + date, + clicks, + impressions, + position, + ctr: impressions > 0 ? (clicks / impressions) * 100 : 0, +}) + +describe('gscCountryToAlpha2', () => { + it('converts Search Console alpha-3 to the alpha-2 the rest of v2 uses', () => { + expect(gscCountryToAlpha2('usa')).toBe('US') + expect(gscCountryToAlpha2('deu')).toBe('DE') + expect(gscCountryToAlpha2('ukr')).toBe('UA') + }) + + it('passes through anything it cannot map rather than dropping the row', () => { + expect(gscCountryToAlpha2('zzz')).toBe('ZZZ') + expect(gscCountryToAlpha2('')).toBe('') + }) +}) + +describe('mapSeoBreakdownRows', () => { + it("reads getKeywords' `count` as clicks", () => { + expect( + mapSeoBreakdownRows( + 'query', + [ + { + name: 'swetrix', + count: 12, + impressions: 400, + ctr: 3, + position: 4.2, + }, + ], + ['clicks', 'impressions', 'ctr', 'position'], + ), + ).toEqual([ + { value: 'swetrix', clicks: 12, impressions: 400, ctr: 3, position: 4.2 }, + ]) + }) + + it('reads the label field each per-dimension getter returns', () => { + expect( + mapSeoBreakdownRows('page', [{ page: '/blog', clicks: 5 }], ['clicks']), + ).toEqual([{ value: '/blog', clicks: 5 }]) + + expect( + mapSeoBreakdownRows( + 'device', + [{ device: 'mobile', clicks: 3 }], + ['clicks'], + ), + ).toEqual([{ value: 'mobile', clicks: 3 }]) + }) + + it('normalises country rows to alpha-2', () => { + expect( + mapSeoBreakdownRows( + 'country', + [{ country: 'usa', clicks: 9 }], + ['clicks'], + ), + ).toEqual([{ value: 'US', clicks: 9 }]) + }) + + it('only emits the requested metrics', () => { + expect( + mapSeoBreakdownRows( + 'query', + [{ name: 'a', count: 1, impressions: 2, ctr: 50, position: 1 }], + ['clicks'], + ), + ).toEqual([{ value: 'a', clicks: 1 }]) + }) + + it('defaults missing values to zero rather than emitting NaN', () => { + expect( + mapSeoBreakdownRows('query', [{ name: 'a' }], ['clicks', 'position']), + ).toEqual([{ value: 'a', clicks: 0, position: 0 }]) + }) +}) + +describe('mapSeoSummary', () => { + it('reports the absolute change between periods, matching the other v2 summaries', () => { + expect( + mapSeoSummary( + { clicks: 120, impressions: 3000, ctr: 4, position: 8.1 }, + { clicks: 100, impressions: 2500, ctr: 4.5, position: 9.4 }, + ), + ).toEqual({ + current: { clicks: 120, impressions: 3000, ctr: 4, position: 8.1 }, + previous: { clicks: 100, impressions: 2500, ctr: 4.5, position: 9.4 }, + change: { clicks: 20, impressions: 500, ctr: -0.5, position: -1.3 }, + }) + }) + + it('treats a missing previous period as zeroes', () => { + const summary = mapSeoSummary( + { clicks: 10, impressions: 100, ctr: 10, position: 2 }, + null, + ) + + expect(summary.previous).toEqual({ + clicks: 0, + impressions: 0, + ctr: 0, + position: 0, + }) + expect(summary.change.clicks).toBe(10) + }) +}) + +describe('bucketSeoDateSeries', () => { + const series = [ + day('2026-01-05', 10, 100, 10), + day('2026-01-20', 30, 300, 2), + day('2026-02-10', 5, 50, 4), + ] + + it('leaves the native hour and day granularity untouched', () => { + expect(bucketSeoDateSeries(series, TimeBucketType.DAY)).toBe(series) + expect(bucketSeoDateSeries(series, TimeBucketType.HOUR)).toBe(series) + }) + + it('sums clicks and impressions into month buckets', () => { + const [january, february] = bucketSeoDateSeries( + series, + TimeBucketType.MONTH, + ) + + expect(january.date).toBe('2026-01-01') + expect(january.clicks).toBe(40) + expect(january.impressions).toBe(400) + expect(february.date).toBe('2026-02-01') + expect(february.clicks).toBe(5) + }) + + it('recomputes ctr from the bucket totals rather than averaging it', () => { + const [january] = bucketSeoDateSeries(series, TimeBucketType.MONTH) + // 40 clicks / 400 impressions, not the mean of the two daily CTRs. + expect(january.ctr).toBe(10) + }) + + it('averages position weighted by impressions', () => { + const [january] = bucketSeoDateSeries(series, TimeBucketType.MONTH) + // (10*100 + 2*300) / 400 = 4, whereas a plain mean would give 6. + expect(january.position).toBe(4) + }) + + it('rolls a whole year up into one bucket', () => { + const yearly = bucketSeoDateSeries(series, TimeBucketType.YEAR) + + expect(yearly).toHaveLength(1) + expect(yearly[0].date).toBe('2026-01-01') + expect(yearly[0].clicks).toBe(45) + }) + + it('reports zeroed rates for a bucket with no impressions', () => { + const [bucket] = bucketSeoDateSeries( + [day('2026-01-05', 0, 0, 0)], + TimeBucketType.MONTH, + ) + + expect(bucket.ctr).toBe(0) + expect(bucket.position).toBe(0) + }) +}) + +describe('mapSeoTimeseries', () => { + it('emits ISO timestamps in the requested timezone', () => { + const [row] = mapSeoTimeseries( + [day('2026-01-05', 10, 100, 3)], + ['clicks'], + 'Europe/Kyiv', + ) + + expect(row.timestamp).toBe('2026-01-05T00:00:00+02:00') + expect(row.clicks).toBe(10) + }) + + it('only emits the requested metrics', () => { + const [row] = mapSeoTimeseries( + [day('2026-01-05', 10, 100, 3)], + ['clicks', 'position'], + 'UTC', + ) + + expect(Object.keys(row).sort()).toEqual(['clicks', 'position', 'timestamp']) + }) + + it('drops rows Search Console returned without a date', () => { + expect(mapSeoTimeseries([day('', 1, 1, 1)], ['clicks'], 'UTC')).toEqual([]) + }) +}) diff --git a/backend/apps/community/src/analytics/v2/__tests__/seo.registry.spec.ts b/backend/apps/community/src/analytics/v2/__tests__/seo.registry.spec.ts new file mode 100644 index 000000000..145d9407b --- /dev/null +++ b/backend/apps/community/src/analytics/v2/__tests__/seo.registry.spec.ts @@ -0,0 +1,134 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { V2Filter } from '../query/filters.translator' +import { + getDefaultSeoMetrics, + getSeoBreakdownDimension, + getSeoMetric, + listSeoDimensions, + parseSeoMetricsParam, + toGscFiltersJson, + validateSeoFilters, + V2_SEO_DIMENSIONS, +} from '../registry/seo' + +const filter = (overrides: Partial = {}): V2Filter => + ({ + dimension: 'country', + operator: 'is', + value: 'US', + ...overrides, + }) as V2Filter + +describe('seo dimension lookups', () => { + it('resolves every declared dimension to its Search Console name', () => { + expect(getSeoBreakdownDimension('query').gsc).toBe('query') + expect(getSeoBreakdownDimension('page').gsc).toBe('page') + expect(getSeoBreakdownDimension('country').gsc).toBe('country') + expect(getSeoBreakdownDimension('device').gsc).toBe('device') + }) + + it('rejects unknown dimensions with 422', () => { + expect(() => getSeoBreakdownDimension('nope')).toThrow( + UnprocessableEntityException, + ) + // Valid for traffic, but Search Console reports nothing like it. + expect(() => getSeoBreakdownDimension('browser')).toThrow( + UnprocessableEntityException, + ) + }) + + it('has unique api names', () => { + const names = listSeoDimensions().map((dimension) => dimension.api) + expect(new Set(names).size).toBe(names.length) + }) + + it('only maps onto dimensions Search Console actually serves', () => { + for (const dimension of V2_SEO_DIMENSIONS) { + expect(['query', 'page', 'country', 'device']).toContain(dimension.gsc) + } + }) +}) + +describe('seo metric lookups', () => { + it('resolves metrics and rejects unknown ones', () => { + expect(getSeoMetric('clicks').api).toBe('clicks') + expect(() => getSeoMetric('visitors')).toThrow(UnprocessableEntityException) + }) + + it('returns all four metrics by default', () => { + expect(getDefaultSeoMetrics().map((metric) => metric.api)).toEqual([ + 'clicks', + 'impressions', + 'ctr', + 'position', + ]) + }) + + it('parses and dedupes the metrics CSV param', () => { + expect( + parseSeoMetricsParam('clicks, impressions,clicks').map( + (metric) => metric.api, + ), + ).toEqual(['clicks', 'impressions']) + + expect(parseSeoMetricsParam(undefined).map((metric) => metric.api)).toEqual( + ['clicks', 'impressions', 'ctr', 'position'], + ) + + expect(() => parseSeoMetricsParam('bogus')).toThrow( + UnprocessableEntityException, + ) + }) +}) + +describe('validateSeoFilters', () => { + it('accepts scalar filters on known dimensions', () => { + const filters = [filter(), filter({ dimension: 'query', value: 'swetrix' })] + expect(validateSeoFilters(filters)).toBe(filters) + }) + + it('rejects filters on dimensions Search Console cannot filter by', () => { + expect(() => + validateSeoFilters([filter({ dimension: 'browser' })]), + ).toThrow(UnprocessableEntityException) + }) + + it('rejects array values, which have no dimensionFilterGroups equivalent', () => { + expect(() => validateSeoFilters([filter({ value: ['US', 'DE'] })])).toThrow( + UnprocessableEntityException, + ) + }) + + it('rejects null values, since Search Console reports no unset values', () => { + expect(() => validateSeoFilters([filter({ value: null })])).toThrow( + UnprocessableEntityException, + ) + }) +}) + +describe('toGscFiltersJson', () => { + it('renames query -> keywords, the key GSCService.parseFilters expects', () => { + const json = toGscFiltersJson([ + filter({ dimension: 'query', value: 'swetrix' }), + ]) + + expect(JSON.parse(json)).toEqual([ + { dimension: 'keywords', operator: 'is', value: 'swetrix' }, + ]) + }) + + it('leaves dimensions that already share a name alone', () => { + const json = toGscFiltersJson([ + filter({ dimension: 'page', operator: 'contains', value: '/blog' }), + ]) + + expect(JSON.parse(json)).toEqual([ + { dimension: 'page', operator: 'contains', value: '/blog' }, + ]) + }) + + it('returns undefined for no filters so no filter group is sent', () => { + expect(toGscFiltersJson([])).toBeUndefined() + }) +}) diff --git a/backend/apps/community/src/analytics/v2/__tests__/timeseries.mapper.spec.ts b/backend/apps/community/src/analytics/v2/__tests__/timeseries.mapper.spec.ts new file mode 100644 index 000000000..1c97d0db3 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/__tests__/timeseries.mapper.spec.ts @@ -0,0 +1,72 @@ +import { + computeBounceRateSeries, + toIsoTimestamp, + zipTimeseries, +} from '../mappers/timeseries.mapper' + +describe('toIsoTimestamp', () => { + it('applies the timezone offset for day labels', () => { + expect(toIsoTimestamp('2026-07-01', 'Europe/Kyiv')).toBe( + '2026-07-01T00:00:00+03:00', + ) + }) + + it('handles hour labels and UTC', () => { + expect(toIsoTimestamp('2026-07-01 13:00:00', 'Etc/GMT')).toBe( + '2026-07-01T13:00:00Z', + ) + }) + + it('handles month and year labels', () => { + expect(toIsoTimestamp('2026-07', 'Etc/GMT')).toBe('2026-07-01T00:00:00Z') + expect(toIsoTimestamp('2026', 'Etc/GMT')).toBe('2026-01-01T00:00:00Z') + }) +}) + +describe('zipTimeseries', () => { + it('zips columnar series into self-describing rows', () => { + const rows = zipTimeseries( + ['2026-07-01', '2026-07-02'], + { + visitors: [10, 20], + pageviews: [30, 40], + skipped: undefined, + }, + 'Etc/GMT', + ) + + expect(rows).toEqual([ + { + timestamp: '2026-07-01T00:00:00Z', + visitors: 10, + pageviews: 30, + }, + { + timestamp: '2026-07-02T00:00:00Z', + visitors: 20, + pageviews: 40, + }, + ]) + }) + + it('fills missing values with null', () => { + const rows = zipTimeseries( + ['2026-07-01', '2026-07-02'], + { visitors: [10] }, + 'Etc/GMT', + ) + + expect(rows[1].visitors).toBeNull() + }) +}) + +describe('computeBounceRateSeries', () => { + it('computes per-bucket bounce rate as bounces / uniques * 100', () => { + expect(computeBounceRateSeries([1, 0, 3], [4, 0, 6])).toEqual([25, 0, 50]) + }) + + it('returns undefined when either input is missing', () => { + expect(computeBounceRateSeries(undefined, [1])).toBeUndefined() + expect(computeBounceRateSeries([1], undefined)).toBeUndefined() + }) +}) diff --git a/backend/apps/community/src/analytics/v2/analytics-v2.service.ts b/backend/apps/community/src/analytics/v2/analytics-v2.service.ts new file mode 100644 index 000000000..55606aeaa --- /dev/null +++ b/backend/apps/community/src/analytics/v2/analytics-v2.service.ts @@ -0,0 +1,1966 @@ +import { + BadRequestException, + Injectable, + InternalServerErrorException, + UnprocessableEntityException, +} from '@nestjs/common' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' + +import { clickhouse } from '../../common/integrations/clickhouse' +import { AppLoggerService } from '../../logger/logger.service' +import { MAX_METRICS_IN_VIEW } from '../../project/dto/create-project-view.dto' +import { IFunnel, PerfMeasure } from '../interfaces' +import { + AnalyticsService, + getLowestPossibleTimeBucket, +} from '../analytics.service' +import { ChartRenderMode, TimeBucketType } from '../dto/getData.dto' +import { GetCustomEventMetadata } from '../dto/get-custom-event-meta.dto' +import { GetPagePropertyMetaDto } from '../dto/get-page-property-meta.dto' +import { + findDimension, + getBreakdownDimension, + getMetric, + listDimensions, + listMetrics, + parseMetricsParam, + V2_TO_V1_DATATYPE, + V2DataType, + V2MetricDef, +} from './registry' +import { + parseV2Filters, + toV1FiltersJson, + V2Filter, +} from './query/filters.translator' +import { + buildBreakdownQuery, + parseSortParam, +} from './query/breakdown-query.builder' +import { + computeBounceRateSeries, + toIsoTimestamp, + zipTimeseries, + TimeseriesRow, +} from './mappers/timeseries.mapper' +import { renameEntityKeys, renameEntityList } from './mappers/entity.mapper' +import { mapTrafficSummary, V1TrafficSummary } from './mappers/summary.mapper' +import { envelope, V2Envelope } from './mappers/envelope' +import { + V2_DEFAULT_PERIOD, + V2BaseQueryDto, + V2BreakdownDto, + V2TimeseriesDto, +} from './dto/v2-base.dto' + +dayjs.extend(utc) + +type EventsAllTimeType = + | 'pageview' + | 'custom_event' + | 'performance' + | 'error' + | 'captcha' + +const ALL_TIME_EVENT_TYPES: Record = { + traffic: ['pageview', 'custom_event', 'error'], + performance: ['performance'], + captcha: ['captcha'], + errors: ['error'], +} + +const TRAFFIC_TIMESERIES_METRICS = [ + 'visitors', + 'pageviews', + 'session_duration', + 'bounce_rate', + 'concurrency', +] + +const PERFORMANCE_TIMESERIES_METRICS: Record = { + dns: 'dns', + tls: 'tls', + connection: 'conn', + response: 'response', + render: 'render', + dom_load: 'domLoad', + ttfb: 'ttfb', +} + +const CAPTCHA_TIMESERIES_METRICS: Record = { + generated: 'generated', + passed: 'passed', + failed: 'failed', + validation_failed: 'validationFailed', + replayed: 'replayed', +} + +const ONLINE_VISITORS_WINDOW_MINUTES = 5 + +export interface ResolvedTimeframe { + period: string | null + timeBucket: TimeBucketType | null + allowedTimeBuckets: TimeBucketType[] | null + safeTimezone: string + groupFrom: string + groupTo: string + groupFromUTC: string + groupToUTC: string +} + +interface PreparedFilters { + appliedFilters: V2Filter[] + v1FiltersJson: string + filtersQuery: string + filtersParams: Record + customEVFilterApplied: boolean +} + +const DEFAULT_BREAKDOWN_LIMIT = 30 + +@Injectable() +export class AnalyticsV2Service { + constructor( + private readonly analyticsService: AnalyticsService, + private readonly logger: AppLoggerService, + ) {} + + async assertReadAccess( + pid: string, + uid: string | null, + password?: string, + ): Promise { + await this.analyticsService.checkProjectAccess(pid, uid, password) + } + + async resolveTimeframe(opts: { + pid: string + dataType: V2DataType + period?: string + from?: string + to?: string + timeBucket?: TimeBucketType + timezone?: string + allTimeEventTypes?: readonly EventsAllTimeType[] + }): Promise { + const { pid, dataType, from, to, timezone } = opts + + const safeTimezone = this.analyticsService.getSafeTimezone(timezone) + + if (Boolean(from) !== Boolean(to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(from && to) + const period = hasCustomRange + ? null + : opts.period && opts.period !== 'custom' + ? opts.period + : V2_DEFAULT_PERIOD + + let timeBucket = + opts.timeBucket || + getLowestPossibleTimeBucket( + period ?? undefined, + hasCustomRange ? from : undefined, + hasCustomRange ? to : undefined, + ) + + let diff: number | undefined + let allowedTimeBuckets: TimeBucketType[] | null = null + + if (period === 'all') { + const allTime = await this.analyticsService.calculateTimeBucketForAllTime( + pid, + opts.allTimeEventTypes ?? ALL_TIME_EVENT_TYPES[dataType], + ) + + diff = allTime.diff + allowedTimeBuckets = allTime.timeBucket + timeBucket = + opts.timeBucket && allTime.timeBucket.includes(opts.timeBucket) + ? opts.timeBucket + : allTime.timeBucket[0] + } + + const { groupFrom, groupTo, groupFromUTC, groupToUTC } = + this.analyticsService.getGroupFromTo( + hasCustomRange ? from : undefined, + hasCustomRange ? to : undefined, + timeBucket, + period ?? undefined, + safeTimezone, + diff, + ) + + return { + period, + timeBucket, + allowedTimeBuckets, + safeTimezone, + groupFrom, + groupTo, + groupFromUTC, + groupToUTC, + } + } + + prepareFilters( + filters: string | undefined, + dataType: V2DataType, + ignoreEV?: boolean, + ): PreparedFilters { + const appliedFilters = parseV2Filters(filters) + const v1FiltersJson = toV1FiltersJson(appliedFilters, dataType) + + const [filtersQuery, filtersParams, , customEVFilterApplied] = + this.analyticsService.getFiltersQuery( + v1FiltersJson, + V2_TO_V1_DATATYPE[dataType], + ignoreEV, + ) + + return { + appliedFilters, + v1FiltersJson, + filtersQuery, + filtersParams, + customEVFilterApplied, + } + } + + buildSubQuery( + dataType: V2DataType, + filtersQuery: string, + customEVFilterApplied: boolean, + ): string { + if (dataType === 'traffic') { + return this.analyticsService.buildAnalyticsEventsSubQuery( + filtersQuery, + customEVFilterApplied, + ) + } + + if (dataType === 'captcha') { + return this.analyticsService.buildAnalyticsEventsSubQuery( + filtersQuery, + false, + true, + ) + } + + const eventType = dataType === 'performance' ? 'performance' : 'error' + + return `FROM events WHERE pid = {pid:FixedString(12)} AND type = '${eventType}' ${filtersQuery} AND created BETWEEN {groupFrom:String} AND {groupTo:String}` + } + + private baseMeta( + pid: string, + timeframe: ResolvedTimeframe, + appliedFilters: V2Filter[], + ): Record { + return { + pid, + period: timeframe.period, + from: toIsoTimestamp(timeframe.groupFrom, timeframe.safeTimezone), + to: toIsoTimestamp(timeframe.groupTo, timeframe.safeTimezone), + timezone: timeframe.safeTimezone, + appliedFilters, + } + } + + async getBreakdown( + pid: string, + dto: V2BreakdownDto, + dataType: V2DataType, + measure?: PerfMeasure, + ): Promise[]>> { + const dimension = getBreakdownDimension(dto.dimension, dataType) + const metrics = + dimension.virtual && !dto.metrics + ? [getMetric('visitors', dataType)] + : parseMetricsParam(dto.metrics, dataType) + const filters = this.prepareFilters( + dto.filters, + dataType, + dataType === 'performance' || dataType === 'errors', + ) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + const sort = parseSortParam(dto.sort, dimension, metrics) + const subQuery = this.buildSubQuery( + dataType, + filters.filtersQuery, + filters.customEVFilterApplied, + ) + + const meta: Record = { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + dimension: dimension.api, + metrics: metrics.map((metric) => metric.api), + limit, + offset, + sort: `${sort.field}:${sort.direction}`, + } + + if (dimension.virtual) { + const rows = await this.getVirtualPageBreakdown( + dimension.virtual, + dataType, + metrics, + filters, + subQuery, + { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + sort, + ) + + meta.total = rows.length + + return envelope(rows.slice(offset, offset + limit), meta) + } + + const query = buildBreakdownQuery({ + dataType, + dimension, + metrics, + subQuery, + ctx: { + customEVFilterApplied: filters.customEVFilterApplied, + measure, + }, + sort, + }) + + const { data } = await clickhouse + .query({ + query, + query_params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + v2_limit: limit, + v2_offset: offset, + }, + }) + .then((resultSet) => resultSet.json>()) + + let total = 0 + + const rows = (data || []).map((row) => { + const { __total, value, ...rest } = row + + total = Number(__total) || 0 + + const mapped: Record = { value } + + for (const field of dimension.extraFields ?? []) { + mapped[field.api] = rest[field.api] ?? null + } + + for (const metric of metrics) { + mapped[metric.api] = Number(rest[metric.api]) || 0 + } + + return mapped + }) + + meta.total = total + + return envelope(rows, meta) + } + + private async getVirtualPageBreakdown( + kind: 'entry_page' | 'exit_page', + dataType: V2DataType, + metrics: V2MetricDef[], + filters: PreparedFilters, + subQuery: string, + params: Record, + sort: { field: string; direction: 'asc' | 'desc' }, + ): Promise[]> { + if (dataType !== 'traffic') { + throw new UnprocessableEntityException( + `The '${kind}' dimension is only available for traffic breakdowns`, + ) + } + + if (filters.customEVFilterApplied) { + throw new UnprocessableEntityException( + `The '${kind}' dimension cannot be combined with custom event filters`, + ) + } + + const unsupported = metrics.filter((metric) => metric.api !== 'visitors') + + if (unsupported.length > 0) { + throw new UnprocessableEntityException( + `The '${kind}' dimension only supports the 'visitors' metric`, + ) + } + + const paramsData = { params } + + const rows = + kind === 'entry_page' + ? await this.analyticsService.getEntryPages(subQuery, paramsData) + : await this.analyticsService.getExitPages(subQuery, paramsData) + + const mapped = (rows || []).map((row) => ({ + value: row.name, + visitors: Number(row.count) || 0, + })) + + const directionFactor = sort.direction === 'asc' ? 1 : -1 + + mapped.sort((a, b) => { + if (sort.field === 'value') { + return String(a.value).localeCompare(String(b.value)) * directionFactor + } + + return (a.visitors - b.visitors) * directionFactor + }) + + return mapped + } + + async getTrafficTimeseries( + pid: string, + dto: V2TimeseriesDto, + ): Promise> { + const dataType: V2DataType = 'traffic' + const metrics = parseMetricsParam(dto.metrics, dataType) + + for (const metric of metrics) { + if (!TRAFFIC_TIMESERIES_METRICS.includes(metric.api)) { + throw new UnprocessableEntityException( + `The '${metric.api}' metric is not supported on the traffic timeseries. Supported metrics: ${TRAFFIC_TIMESERIES_METRICS.join(', ')}`, + ) + } + } + + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + }) + + const mode = dto.mode || ChartRenderMode.PERIODICAL + + const includeConcurrency = metrics.some( + (metric) => metric.api === 'concurrency', + ) + + const result = (await this.analyticsService.groupChartByTimeBucket( + timeframe.timeBucket, + timeframe.groupFrom, + timeframe.groupTo, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + filters.customEVFilterApplied, + mode, + includeConcurrency, + )) as { + chart: { + x: string[] + visits: number[] + uniques: number[] + sdur: number[] + bounces?: number[] + concurrency?: number[] + } + } + + const { chart } = result + + const availableSeries: Record = { + visitors: chart.uniques, + pageviews: chart.visits, + session_duration: chart.sdur, + bounce_rate: computeBounceRateSeries(chart.bounces, chart.uniques), + concurrency: chart.concurrency, + } + + const series: Record = {} + + for (const metric of metrics) { + series[metric.api] = availableSeries[metric.api] + } + + return envelope(zipTimeseries(chart.x, series, timeframe.safeTimezone), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: timeframe.allowedTimeBuckets, + metrics: metrics.map((metric) => metric.api), + mode, + }) + } + + async getTrafficSummary( + pid: string, + dto: V2BaseQueryDto, + ): Promise>> { + const dataType: V2DataType = 'traffic' + const appliedFilters = parseV2Filters(dto.filters) + const v1FiltersJson = toV1FiltersJson(appliedFilters, dataType) + + const hasCustomRange = Boolean(dto.from && dto.to) + + if (Boolean(dto.from) !== Boolean(dto.to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const period = hasCustomRange + ? undefined + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + const result = await this.analyticsService.getAnalyticsSummary( + [pid], + undefined, + period, + hasCustomRange ? dto.from : undefined, + hasCustomRange ? dto.to : undefined, + dto.timezone, + v1FiltersJson, + false, + ) + + const summary = result?.[pid] as V1TrafficSummary | undefined + + if (!summary) { + throw new InternalServerErrorException( + 'Failed to compute the traffic summary for this project', + ) + } + + return envelope(mapTrafficSummary(summary), { + pid, + period: period ?? null, + from: hasCustomRange ? dto.from : undefined, + to: hasCustomRange ? dto.to : undefined, + timezone: this.analyticsService.getSafeTimezone(dto.timezone), + appliedFilters, + }) + } + + async getTrafficCustomEvents( + pid: string, + dto: V2BaseQueryDto & { limit?: number; offset?: number }, + ): Promise> { + const dataType: V2DataType = 'traffic' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const customs = await this.analyticsService.getCustomEvents( + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + ) + + const rows = Object.entries(customs || {}) + .map(([event, count]) => ({ event, count: Number(count) || 0 })) + .sort((a, b) => b.count - a.count) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + return envelope(rows.slice(offset, offset + limit), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + total: rows.length, + limit, + offset, + }) + } + + async getTrafficCustomMetrics( + pid: string, + dto: V2BaseQueryDto & { metrics: string }, + ): Promise> { + const dataType: V2DataType = 'traffic' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const parsedMetrics = this.analyticsService.parseMetrics(dto.metrics) + + if (parsedMetrics.length > MAX_METRICS_IN_VIEW) { + throw new UnprocessableEntityException( + `The maximum number of metrics within one request is ${MAX_METRICS_IN_VIEW}`, + ) + } + + if (!parsedMetrics.length) { + return envelope([], this.baseMeta(pid, timeframe, filters.appliedFilters)) + } + + const meta = await this.analyticsService.getMetaResults( + pid, + parsedMetrics, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + timeframe.period ?? undefined, + dto.from, + dto.to, + ) + + return envelope( + meta ?? [], + this.baseMeta(pid, timeframe, filters.appliedFilters), + ) + } + + async getTrafficPageProperties( + pid: string, + dto: V2BaseQueryDto & { limit?: number; offset?: number }, + ): Promise> { + const dataType: V2DataType = 'traffic' + const filters = this.prepareFilters(dto.filters, dataType) + + if (filters.customEVFilterApplied) { + throw new UnprocessableEntityException( + 'Page properties cannot be combined with custom event filters', + ) + } + + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const properties = await this.analyticsService.getPageProperties( + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + ) + + const rows = Object.entries(properties || {}) + .map(([property, count]) => ({ property, count: Number(count) || 0 })) + .sort((a, b) => b.count - a.count) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + return envelope(rows.slice(offset, offset + limit), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + total: rows.length, + limit, + offset, + }) + } + + async getPerformanceTimeseries( + pid: string, + dto: V2TimeseriesDto & { measure?: string }, + ): Promise> { + const dataType: V2DataType = 'performance' + const measure = (dto.measure || 'median') as PerfMeasure + + this.analyticsService.checkIfPerfMeasureIsValid(measure) + + const filters = this.prepareFilters(dto.filters, dataType, true) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + }) + + const chart = (await this.analyticsService.getPerfChartData( + timeframe.timeBucket, + timeframe.groupFrom, + timeframe.groupTo, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + measure, + )) as { x: string[] } & Record + + const series: Record = {} + let metricNames: string[] + + if (measure === 'quantiles') { + metricNames = ['p50', 'p75', 'p95'] + + for (const name of metricNames) { + series[name] = chart[name] + } + } else { + metricNames = dto.metrics + ? [ + ...new Set( + dto.metrics + .split(',') + .map((name) => name.trim()) + .filter(Boolean), + ), + ] + : Object.keys(PERFORMANCE_TIMESERIES_METRICS) + + for (const name of metricNames) { + const v1Key = Object.hasOwn(PERFORMANCE_TIMESERIES_METRICS, name) + ? PERFORMANCE_TIMESERIES_METRICS[name] + : undefined + + if (!v1Key) { + throw new UnprocessableEntityException( + `The '${name}' metric is not supported on the performance timeseries. Supported metrics: ${Object.keys(PERFORMANCE_TIMESERIES_METRICS).join(', ')}`, + ) + } + + series[name] = chart[v1Key] + } + } + + return envelope(zipTimeseries(chart.x, series, timeframe.safeTimezone), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: timeframe.allowedTimeBuckets, + metrics: metricNames, + measure, + }) + } + + async getPerformanceSummary( + pid: string, + dto: V2BaseQueryDto & { measure?: string }, + ): Promise>> { + const dataType: V2DataType = 'performance' + let measure = (dto.measure || 'median') as PerfMeasure + + this.analyticsService.checkIfPerfMeasureIsValid(measure) + + if (measure === 'quantiles') { + measure = 'median' + } + + const appliedFilters = parseV2Filters(dto.filters) + const v1FiltersJson = toV1FiltersJson(appliedFilters, dataType) + + if (Boolean(dto.from) !== Boolean(dto.to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(dto.from && dto.to) + const period = hasCustomRange + ? undefined + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + const result = await this.analyticsService.getPerformanceSummary( + [pid], + period, + hasCustomRange ? dto.from : undefined, + hasCustomRange ? dto.to : undefined, + dto.timezone, + v1FiltersJson, + measure, + ) + + const summary = result?.[pid] as + | { + current: Record + previous: Record + frontendChange: number + networkChange: number + backendChange: number + } + | undefined + + if (!summary) { + throw new InternalServerErrorException( + 'Failed to compute the performance summary for this project', + ) + } + + return envelope( + { + current: summary.current, + previous: summary.previous, + change: { + frontend: summary.frontendChange, + network: summary.networkChange, + backend: summary.backendChange, + }, + }, + { + pid, + period: period ?? null, + from: hasCustomRange ? dto.from : undefined, + to: hasCustomRange ? dto.to : undefined, + timezone: this.analyticsService.getSafeTimezone(dto.timezone), + measure, + appliedFilters, + }, + ) + } + + async getCaptchaTimeseries( + pid: string, + dto: V2TimeseriesDto, + ): Promise> { + const dataType: V2DataType = 'captcha' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + }) + + const mode = dto.mode || ChartRenderMode.PERIODICAL + + const { xShifted } = this.analyticsService.generateXAxis( + timeframe.timeBucket, + timeframe.groupFrom, + timeframe.groupTo, + timeframe.safeTimezone, + ) + + const query = this.analyticsService.generateCaptchaAggregationQuery( + timeframe.timeBucket, + filters.filtersQuery, + mode, + ) + + const { data } = await clickhouse + .query({ + query, + query_params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + timezone: timeframe.safeTimezone, + }, + }) + .then((resultSet) => resultSet.json>()) + + const chart = this.analyticsService.extractCaptchaChartData( + data, + xShifted, + ) as Record + + const metricNames = dto.metrics + ? [ + ...new Set( + dto.metrics + .split(',') + .map((name) => name.trim()) + .filter(Boolean), + ), + ] + : Object.keys(CAPTCHA_TIMESERIES_METRICS) + + const series: Record = {} + + for (const name of metricNames) { + const v1Key = Object.hasOwn(CAPTCHA_TIMESERIES_METRICS, name) + ? CAPTCHA_TIMESERIES_METRICS[name] + : undefined + + if (!v1Key) { + throw new UnprocessableEntityException( + `The '${name}' metric is not supported on the captcha timeseries. Supported metrics: ${Object.keys(CAPTCHA_TIMESERIES_METRICS).join(', ')}`, + ) + } + + series[name] = chart[v1Key] + } + + return envelope(zipTimeseries(xShifted, series, timeframe.safeTimezone), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: timeframe.allowedTimeBuckets, + metrics: metricNames, + mode, + }) + } + + async getCaptchaSummary( + pid: string, + dto: V2BaseQueryDto, + ): Promise>> { + const dataType: V2DataType = 'captcha' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const baseParams = { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + } + + const currentPromise = this.analyticsService.getCaptchaSummary( + filters.filtersQuery, + { params: baseParams }, + ) + + let previousPromise: Promise = Promise.resolve(null) + + if (timeframe.period !== 'all') { + const periodSubtracted = dayjs + .utc(timeframe.groupFromUTC) + .subtract( + Math.abs( + dayjs + .utc(timeframe.groupFromUTC) + .diff(dayjs.utc(timeframe.groupToUTC), 'minutes'), + ), + 'minutes', + ) + .format('YYYY-MM-DD HH:mm:ss') + + previousPromise = this.analyticsService.getCaptchaSummary( + filters.filtersQuery, + { + params: { + ...baseParams, + groupFrom: periodSubtracted, + groupTo: timeframe.groupFromUTC, + }, + }, + ) + } + + const [current, previous] = await Promise.all([ + currentPromise, + previousPromise, + ]) + + return envelope( + { current, previous }, + this.baseMeta(pid, timeframe, filters.appliedFilters), + ) + } + + async getErrorsList( + pid: string, + dto: V2BaseQueryDto & { + limit?: number + offset?: number + show_resolved?: boolean + }, + ): Promise[]>> { + const dataType: V2DataType = 'errors' + const filters = this.prepareFilters(dto.filters, dataType, true) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + const errors = await this.analyticsService.getErrorsList( + JSON.stringify({ showResolved: Boolean(dto.show_resolved) }), + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + limit, + offset, + ) + + return envelope(renameEntityList(errors), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + limit, + offset, + showResolved: Boolean(dto.show_resolved), + }) + } + + async getErrorsTimeseries( + pid: string, + dto: V2TimeseriesDto, + ): Promise> { + const dataType: V2DataType = 'errors' + const filters = this.prepareFilters(dto.filters, dataType, true) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + }) + + const mode = dto.mode || ChartRenderMode.PERIODICAL + + const { x, format } = this.analyticsService.generateUTCXAxis( + timeframe.timeBucket, + timeframe.groupFromUTC, + timeframe.groupToUTC, + ) + + const query = this.analyticsService.generateErrorsAggregationQuery( + timeframe.timeBucket, + filters.filtersQuery, + mode, + ) + + const { data } = await clickhouse + .query({ + query, + query_params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }) + .then((resultSet) => resultSet.json>()) + + const { count, affectedUsers } = + this.analyticsService.extractErrorsChartData(data as never[], x) + + const labels = this.analyticsService.shiftToTimezone( + x, + timeframe.safeTimezone, + format, + ) + + const availableSeries: Record = { + occurrences: count, + affected_users: affectedUsers, + } + + const metricNames = dto.metrics + ? [ + ...new Set( + dto.metrics + .split(',') + .map((name) => name.trim()) + .filter(Boolean), + ), + ] + : Object.keys(availableSeries) + + const series: Record = {} + + for (const name of metricNames) { + if (!Object.hasOwn(availableSeries, name) || !availableSeries[name]) { + throw new UnprocessableEntityException( + `The '${name}' metric is not supported on the errors timeseries. Supported metrics: ${Object.keys(availableSeries).join(', ')}`, + ) + } + + series[name] = availableSeries[name] + } + + return envelope(zipTimeseries(labels, series, timeframe.safeTimezone), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: timeframe.allowedTimeBuckets, + metrics: metricNames, + mode, + }) + } + + async getErrorsOverview( + pid: string, + dto: V2BaseQueryDto & { show_resolved?: boolean }, + ): Promise> { + const dataType: V2DataType = 'errors' + const filters = this.prepareFilters(dto.filters, dataType, true) + + const sessionV1FiltersJson = toV1FiltersJson( + filters.appliedFilters, + 'traffic', + { lenient: true }, + ) + const [sessionFiltersQuery, sessionFiltersParams] = + this.analyticsService.getFiltersQuery( + sessionV1FiltersJson, + V2_TO_V1_DATATYPE.traffic, + true, + ) + + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const overview = await this.analyticsService.getErrorOverview( + pid, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + timeframe.groupFromUTC, + timeframe.groupToUTC, + timeframe.timeBucket, + Boolean(dto.show_resolved), + sessionFiltersQuery, + sessionFiltersParams, + ) + + return envelope(overview, { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + showResolved: Boolean(dto.show_resolved), + }) + } + + async getErrorDetails( + pid: string, + eid: string, + dto: V2BaseQueryDto & { timeBucket?: TimeBucketType }, + ): Promise> { + const dataType: V2DataType = 'errors' + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + }) + + const details = await this.analyticsService.getErrorDetails( + pid, + eid, + timeframe.safeTimezone, + timeframe.groupFromUTC, + timeframe.groupToUTC, + timeframe.timeBucket, + ) + + return envelope(details, { + ...this.baseMeta(pid, timeframe, []), + eid, + timeBucket: timeframe.timeBucket, + }) + } + + async getErrorSessions( + pid: string, + eid: string, + dto: V2BaseQueryDto & { limit?: number; offset?: number }, + ): Promise[]>> { + const dataType: V2DataType = 'errors' + const filters = this.prepareFilters(dto.filters, dataType, true) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const limit = dto.limit ?? 10 + const offset = dto.offset ?? 0 + + const result = (await this.analyticsService.getErrorAffectedSessions( + pid, + eid, + timeframe.groupFromUTC, + timeframe.groupToUTC, + limit, + offset, + filters.filtersQuery, + filters.filtersParams, + )) as { sessions?: unknown } | Record[] + + const sessions = Array.isArray(result) + ? result + : ((result?.sessions ?? []) as Record[]) + + return envelope(renameEntityList(sessions), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + eid, + limit, + offset, + }) + } + + async getSessionsList( + pid: string, + dto: V2BaseQueryDto & { + limit?: number + offset?: number + event_type?: 'traffic' | 'performance' | 'error' + }, + ): Promise[]>> { + const eventType = dto.event_type || 'traffic' + const dataType: V2DataType = + eventType === 'performance' + ? 'performance' + : eventType === 'error' + ? 'errors' + : 'traffic' + + const filters = this.prepareFilters( + dto.filters, + dataType, + eventType !== 'traffic', + ) + + const timeframe = await this.resolveTimeframe({ + pid, + dataType: 'traffic', + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + allTimeEventTypes: + eventType === 'traffic' + ? ['pageview', 'custom_event', 'error'] + : [eventType], + }) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + const sessions = await this.analyticsService.getSessionsList( + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + limit, + offset, + filters.customEVFilterApplied, + eventType, + ) + + return envelope(renameEntityList(sessions), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + eventType, + limit, + offset, + }) + } + + async getSessionDetails( + pid: string, + psid: string, + timezone?: string, + ): Promise>> { + const safeTimezone = this.analyticsService.getSafeTimezone(timezone) + + const result = (await this.analyticsService.getSessionDetails( + pid, + psid, + safeTimezone, + )) as Record + + const details = result?.details as Record | undefined + + return envelope( + { + ...result, + details: details ? renameEntityKeys(details) : details, + }, + { pid, psid, timezone: safeTimezone }, + ) + } + + async getProfilesList( + pid: string, + dto: V2BaseQueryDto & { + limit?: number + offset?: number + profile_type?: 'all' | 'anonymous' | 'identified' + }, + ): Promise[]>> { + const dataType: V2DataType = 'traffic' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + const profiles = await this.analyticsService.getProfilesList( + pid, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + limit, + offset, + dto.profile_type || 'all', + filters.customEVFilterApplied, + ) + + return envelope(renameEntityList(profiles), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + profileType: dto.profile_type || 'all', + limit, + offset, + }) + } + + async getProfileDetails( + pid: string, + profileId: string, + timezone?: string, + ): Promise>> { + const safeTimezone = this.analyticsService.getSafeTimezone(timezone) + + const [details, topPages, activityCalendar] = await Promise.all([ + this.analyticsService.getProfileDetails(pid, profileId, safeTimezone), + this.analyticsService.getProfileTopPages(pid, profileId), + this.analyticsService.getProfileActivityCalendar(pid, profileId), + ]) + + return envelope( + { + ...renameEntityKeys((details ?? {}) as Record), + topPages, + activityCalendar, + }, + { pid, profileId, timezone: safeTimezone }, + ) + } + + async getProfileSessions( + pid: string, + profileId: string, + dto: V2BaseQueryDto & { limit?: number; offset?: number }, + ): Promise[]>> { + const dataType: V2DataType = 'traffic' + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timezone: dto.timezone, + }) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + const sessions = await this.analyticsService.getProfileSessionsList( + pid, + profileId, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + limit, + offset, + filters.customEVFilterApplied, + ) + + return envelope(renameEntityList(sessions), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + profileId, + limit, + offset, + }) + } + + async getTrafficCustomEventMetadata( + pid: string, + dto: V2BaseQueryDto & { event: string; timeBucket?: TimeBucketType }, + ): Promise> { + const appliedFilters = parseV2Filters(dto.filters) + const v1FiltersJson = toV1FiltersJson(appliedFilters, 'traffic') + + if (Boolean(dto.from) !== Boolean(dto.to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(dto.from && dto.to) + const from = hasCustomRange ? dto.from : undefined + const to = hasCustomRange ? dto.to : undefined + const period = hasCustomRange + ? undefined + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + const { result } = await this.analyticsService.getCustomEventMetadata({ + pid, + period, + timeBucket: + dto.timeBucket || getLowestPossibleTimeBucket(period, from, to), + from, + to, + filters: v1FiltersJson, + timezone: dto.timezone, + event: dto.event, + } as GetCustomEventMetadata) + + return envelope(result, { + pid, + event: dto.event, + period: period ?? null, + timezone: this.analyticsService.getSafeTimezone(dto.timezone), + appliedFilters, + }) + } + + async getTrafficPagePropertyMetadata( + pid: string, + dto: V2BaseQueryDto & { property: string; timeBucket?: TimeBucketType }, + ): Promise> { + const appliedFilters = parseV2Filters(dto.filters) + const v1FiltersJson = toV1FiltersJson(appliedFilters, 'traffic') + + if (Boolean(dto.from) !== Boolean(dto.to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(dto.from && dto.to) + const from = hasCustomRange ? dto.from : undefined + const to = hasCustomRange ? dto.to : undefined + const period = hasCustomRange + ? undefined + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + const { result } = await this.analyticsService.getPagePropertyMeta({ + pid, + period, + timeBucket: + dto.timeBucket || getLowestPossibleTimeBucket(period, from, to), + from, + to, + filters: v1FiltersJson, + timezone: dto.timezone, + property: dto.property, + } as GetPagePropertyMetaDto) + + return envelope(result, { + pid, + property: dto.property, + period: period ?? null, + timezone: this.analyticsService.getSafeTimezone(dto.timezone), + appliedFilters, + }) + } + + async getTrafficCustomEventsTimeseries( + pid: string, + dto: V2BaseQueryDto & { events: string; timeBucket?: TimeBucketType }, + ): Promise> { + const dataType: V2DataType = 'traffic' + + let events: string[] + try { + const parsed = JSON.parse(dto.events) + events = Array.isArray(parsed) ? parsed.map(String) : [] + } catch { + events = dto.events + .split(',') + .map((event) => event.trim()) + .filter(Boolean) + } + + events = events.filter((event) => event && event !== '__proto__') + + if (events.length === 0) { + throw new UnprocessableEntityException( + 'At least one custom event name has to be provided', + ) + } + + const filters = this.prepareFilters(dto.filters, dataType) + const timeframe = await this.resolveTimeframe({ + pid, + dataType, + period: dto.period, + from: dto.from, + to: dto.to, + timeBucket: dto.timeBucket, + timezone: dto.timezone, + allTimeEventTypes: ['custom_event'], + }) + + const result = (await this.analyticsService.groupCustomEVByTimeBucket( + timeframe.timeBucket, + timeframe.groupFrom, + timeframe.groupTo, + filters.filtersQuery, + { + params: { + pid, + groupFrom: timeframe.groupFromUTC, + groupTo: timeframe.groupToUTC, + ...filters.filtersParams, + }, + }, + timeframe.safeTimezone, + events, + )) as { chart: { x: string[]; events: Record } } + + const series: Record = {} + + for (const event of events) { + series[event] = result.chart.events?.[event] ?? [] + } + + return envelope( + zipTimeseries(result.chart.x, series, timeframe.safeTimezone), + { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: timeframe.allowedTimeBuckets, + events, + }, + ) + } + + async getFunnel( + pid: string, + dto: V2BaseQueryDto & { steps?: string; funnelId?: string }, + ): Promise> { + const steps = await this.resolveFunnelSteps(pid, dto) + + const { params, filters, timeframe } = await this.resolveFunnelContext( + pid, + dto, + ) + + let funnel: IFunnel[] = [] + let totalPageviews = 0 + let timeToConvert: unknown + let stepDetails: Record>> = { + countries: {}, + devices: {}, + browsers: {}, + sources: {}, + campaigns: {}, + pages: {}, + profileTypes: {}, + } + + await Promise.all([ + (async () => { + funnel = await this.analyticsService.getFunnel( + steps, + params, + filters.filtersQuery, + ) + })(), + (async () => { + totalPageviews = await this.analyticsService.getTotalPageviews( + pid, + params.groupFrom as string, + params.groupTo as string, + filters.filtersQuery, + filters.filtersParams, + ) + })(), + (async () => { + try { + stepDetails = await this.analyticsService.getFunnelStepDetails( + steps, + params, + filters.filtersQuery, + ) + } catch (reason) { + this.logger.log(reason, 'AnalyticsV2Service -> getFunnelStepDetails') + } + })(), + (async () => { + timeToConvert = await this.analyticsService.getFunnelTimeToConvert( + steps, + params, + filters.filtersQuery, + ) + })(), + ]) + + for (let i = 0; i < funnel.length; i++) { + funnel[i].breakdowns = { + countries: stepDetails.countries[i + 1] || {}, + devices: stepDetails.devices[i + 1] || {}, + browsers: stepDetails.browsers[i + 1] || {}, + sources: stepDetails.sources[i + 1] || {}, + campaigns: stepDetails.campaigns[i + 1] || {}, + pages: stepDetails.pages[i + 1] || {}, + profileTypes: stepDetails.profileTypes[i + 1] || {}, + } + } + + return envelope( + { steps: funnel, totalPageviews, timeToConvert }, + this.baseMeta(pid, timeframe, filters.appliedFilters), + ) + } + + async getFunnelSessions( + pid: string, + dto: V2BaseQueryDto & { + steps?: string + funnelId?: string + step: number + dropoff?: boolean + limit?: number + offset?: number + }, + ): Promise[]>> { + const steps = await this.resolveFunnelSteps(pid, dto) + + if (dto.step < 1 || dto.step > steps.length) { + throw new BadRequestException( + 'Step must be between 1 and the number of funnel steps', + ) + } + + const { params, filters, timeframe } = await this.resolveFunnelContext( + pid, + dto, + ) + + const limit = dto.limit ?? DEFAULT_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + + const sessions = await this.analyticsService.getFunnelSessionsList( + steps, + params, + timeframe.safeTimezone, + dto.step, + limit, + offset, + filters.filtersQuery, + Boolean(dto.dropoff), + ) + + return envelope(renameEntityList(sessions), { + ...this.baseMeta(pid, timeframe, filters.appliedFilters), + step: dto.step, + dropoff: Boolean(dto.dropoff), + limit, + offset, + }) + } + + private async resolveFunnelSteps( + pid: string, + dto: { steps?: string; funnelId?: string }, + ): Promise { + if (dto.steps && dto.funnelId) { + throw new BadRequestException( + "The 'steps' and 'funnelId' parameters are mutually exclusive", + ) + } + + return this.analyticsService.getPagesArray(dto.steps, dto.funnelId, pid) + } + + private async resolveFunnelContext( + pid: string, + dto: V2BaseQueryDto, + ): Promise<{ + params: Record + filters: PreparedFilters + timeframe: ResolvedTimeframe + }> { + const filters = this.prepareFilters(dto.filters, 'traffic') + const safeTimezone = this.analyticsService.getSafeTimezone(dto.timezone) + + if (Boolean(dto.from) !== Boolean(dto.to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(dto.from && dto.to) + const period = hasCustomRange + ? null + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + let diff: number | undefined + + if (period === 'all') { + const [pageviewRes, customEventRes] = await Promise.all([ + this.analyticsService.calculateTimeBucketForAllTime(pid, 'pageview'), + this.analyticsService.calculateTimeBucketForAllTime( + pid, + 'custom_event', + ), + ]) + + diff = Math.max(pageviewRes.diff, customEventRes.diff) + } + + const { groupFrom, groupTo, groupFromUTC, groupToUTC } = + this.analyticsService.getGroupFromTo( + hasCustomRange ? dto.from : undefined, + hasCustomRange ? dto.to : undefined, + null, + period ?? undefined, + safeTimezone, + diff, + ) + + return { + params: { + pid, + groupFrom, + groupTo, + ...filters.filtersParams, + }, + filters, + timeframe: { + period, + timeBucket: null, + allowedTimeBuckets: null, + safeTimezone, + groupFrom, + groupTo, + groupFromUTC, + groupToUTC, + }, + } + } + + async getLiveVisitors( + pid: string, + ): Promise> { + const since = dayjs + .utc() + .subtract(ONLINE_VISITORS_WINDOW_MINUTES, 'minute') + .format('YYYY-MM-DD HH:mm:ss') + + const query = ` + SELECT + dv, + br, + os, + cc, + psidCasted AS psid + FROM ( + SELECT + any(dv) AS dv, + any(br) AS br, + any(os) AS os, + any(cc) AS cc, + toString(psid) AS psidCasted + FROM events + WHERE + pid = {pid:FixedString(12)} + AND type IN ('pageview', 'custom_event') + AND created >= {since:DateTime} + AND psid IS NOT NULL + AND psid != 0 + GROUP BY psid + ) + ` + + const [{ data }, count] = await Promise.all([ + clickhouse + .query({ query, query_params: { pid, since } }) + .then((resultSet) => resultSet.json>()), + this.analyticsService.getOnlineUserCount(pid), + ]) + + return envelope( + { count, visitors: renameEntityList(data) }, + { pid, windowMinutes: ONLINE_VISITORS_WINDOW_MINUTES }, + ) + } + + getDimensions(type: V2DataType): V2Envelope<{ + dimensions: Record[] + metrics: Record[] + }> { + const dimensions = listDimensions(type).map((dimension) => ({ + name: dimension.api, + description: dimension.description, + filterOnly: Boolean(dimension.filterOnly), + extraFields: (dimension.extraFields ?? []).map((field) => field.api), + })) + + const metrics = listMetrics(type).map((metric) => ({ + name: metric.api, + description: metric.description, + format: metric.format ?? 'integer', + default: Boolean(metric.isDefault), + })) + + return envelope({ dimensions, metrics }, { type }) + } + + async getDimensionValues( + pid: string, + dimensionApi: string, + type: 'traffic' | 'errors', + ): Promise> { + const dimension = findDimension( + dimensionApi, + type === 'errors' ? 'errors' : 'traffic', + ) + + if (!dimension) { + throw new UnprocessableEntityException( + `Unknown ${type} dimension '${dimensionApi}'`, + ) + } + + if (dimension.column === 'brv' || dimension.column === 'osv') { + const values = await this.analyticsService.getVersionFilters( + pid, + type, + dimension.column === 'brv' ? 'br' : 'os', + ) + + return envelope(values, { pid, dimension: dimensionApi, type }) + } + + const values = + type === 'errors' && !dimension.virtual + ? await this.analyticsService.getErrorsFilters(pid, dimension.column) + : await this.analyticsService.getFilters(pid, dimension.column) + + return envelope(values, { pid, dimension: dimensionApi, type }) + } + + validateMetric(api: string, dataType: V2DataType): V2MetricDef { + return getMetric(api, dataType) + } +} diff --git a/backend/apps/community/src/analytics/v2/controllers/captcha-v2.controller.ts b/backend/apps/community/src/analytics/v2/controllers/captcha-v2.controller.ts new file mode 100644 index 000000000..b2e0c313b --- /dev/null +++ b/backend/apps/community/src/analytics/v2/controllers/captcha-v2.controller.ts @@ -0,0 +1,125 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2BreakdownDto, + V2ProjectParamsDto, + V2SummaryDto, + V2TimeseriesDto, +} from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/captcha') +export class CaptchaV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get('summary') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Captcha summary', + description: + 'Captcha counters, pass rate, solve-time quantiles, and difficulty/solve-time distributions for the selected period, plus the previous period for comparison.', + }) + async getSummary( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SummaryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/captcha/summary') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getCaptchaSummary(pid, query) + } + + @Get('timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Captcha timeseries', + description: + 'Captcha counters (generated, passed, failed, validation_failed, replayed) grouped by time bucket.', + }) + async getTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2TimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/captcha/timeseries') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getCaptchaTimeseries(pid, query) + } + + @Get('breakdown') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Captcha breakdown', + description: + 'Captcha events grouped by a single dimension (country, browser, os, device, captcha_event, captcha_difficulty, captcha_reason, solve_time).', + }) + async getBreakdown( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2BreakdownDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${query.dimension}`, + 'GET /v2/projects/:pid/captcha/breakdown', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getBreakdown(pid, query, 'captcha') + } +} diff --git a/backend/apps/community/src/analytics/v2/controllers/errors-v2.controller.ts b/backend/apps/community/src/analytics/v2/controllers/errors-v2.controller.ts new file mode 100644 index 000000000..902f3bdd8 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/controllers/errors-v2.controller.ts @@ -0,0 +1,207 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2ErrorDetailQueryDto, + V2ErrorParamsDto, + V2ErrorSessionsQueryDto, + V2ErrorsQueryDto, +} from '../dto/entities.dto' +import { + V2BreakdownDto, + V2ProjectParamsDto, + V2TimeseriesDto, +} from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/errors') +export class ErrorsV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get() + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Error groups', + description: + 'Paginated list of error groups (grouped by error ID) with occurrence counts, affected users and status.', + }) + async getErrors( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2ErrorsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/errors') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getErrorsList(pid, query) + } + + @Get('overview') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Errors overview', + description: + 'Aggregated error statistics: occurrences, affected users and sessions for the selected period.', + }) + async getOverview( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2ErrorsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/errors/overview') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getErrorsOverview(pid, query) + } + + @Get('timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Errors timeseries', + description: 'Error occurrences and affected users grouped by time bucket.', + }) + async getTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2TimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/errors/timeseries') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getErrorsTimeseries(pid, query) + } + + @Get('breakdown') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Errors breakdown', + description: + 'Error occurrences grouped by a single dimension (page, browser, os, country, ...).', + }) + async getBreakdown( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2BreakdownDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${query.dimension}`, + 'GET /v2/projects/:pid/errors/breakdown', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getBreakdown(pid, query, 'errors') + } + + @Get(':eid') + @Auth(true, true) + @ApiOperation({ + summary: 'Error details', + description: + 'Details for a single error group: metadata, occurrence chart and affected user counts.', + }) + async getErrorDetails( + @Param() { pid, eid }: V2ErrorParamsDto, + @Query() query: V2ErrorDetailQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, eid: ${eid}`, + 'GET /v2/projects/:pid/errors/:eid', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getErrorDetails(pid, eid, query) + } + + @Get(':eid/sessions') + @Auth(true, true) + @ApiOperation({ + summary: 'Error sessions', + description: 'Paginated sessions affected by a specific error group.', + }) + async getErrorSessions( + @Param() { pid, eid }: V2ErrorParamsDto, + @Query() query: V2ErrorSessionsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, eid: ${eid}`, + 'GET /v2/projects/:pid/errors/:eid/sessions', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getErrorSessions(pid, eid, query) + } +} diff --git a/backend/apps/community/src/analytics/v2/controllers/performance-v2.controller.ts b/backend/apps/community/src/analytics/v2/controllers/performance-v2.controller.ts new file mode 100644 index 000000000..664f8d91e --- /dev/null +++ b/backend/apps/community/src/analytics/v2/controllers/performance-v2.controller.ts @@ -0,0 +1,134 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { PerfMeasure } from '../../interfaces' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2PerfBreakdownDto, + V2PerfSummaryDto, + V2PerfTimeseriesDto, +} from '../dto/performance.dto' +import { V2ProjectParamsDto } from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/performance') +export class PerformanceV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get('summary') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Performance summary', + description: + 'Aggregated frontend/network/backend timings for the selected period, the previous period, and the change between them. Values are in seconds.', + }) + async getSummary( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2PerfSummaryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/performance/summary') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getPerformanceSummary(pid, query) + } + + @Get('timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Performance timeseries', + description: + 'Performance timings grouped by time bucket. With measure=quantiles, returns p50/p75/p95 of the total load time instead of individual timings. Values are in seconds.', + }) + async getTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2PerfTimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, measure: ${query.measure}`, + 'GET /v2/projects/:pid/performance/timeseries', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getPerformanceTimeseries(pid, query) + } + + @Get('breakdown') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Performance breakdown', + description: + 'Performance timings grouped by a single dimension (country, page, browser, ...), paginated and sortable. Values are in seconds.', + }) + async getBreakdown( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2PerfBreakdownDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${query.dimension}, measure: ${query.measure}`, + 'GET /v2/projects/:pid/performance/breakdown', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getBreakdown( + pid, + query, + 'performance', + (query.measure || 'median') as PerfMeasure, + ) + } +} diff --git a/backend/apps/community/src/analytics/v2/controllers/profiles-v2.controller.ts b/backend/apps/community/src/analytics/v2/controllers/profiles-v2.controller.ts new file mode 100644 index 000000000..7b4e29f96 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/controllers/profiles-v2.controller.ts @@ -0,0 +1,129 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2ProfileParamsDto, + V2ProfilesQueryDto, + V2TimezoneQueryDto, +} from '../dto/entities.dto' +import { V2ListQueryDto, V2ProjectParamsDto } from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/profiles') +export class ProfilesV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get() + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Profiles', + description: + 'Paginated list of user profiles (anonymous and identified visitors).', + }) + async getProfiles( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2ProfilesQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/profiles') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getProfilesList(pid, query) + } + + @Get(':profileId') + @Auth(true, true) + @ApiOperation({ + summary: 'Profile details', + description: + 'Details for a single profile: attributes, top pages and activity calendar.', + }) + async getProfile( + @Param() { pid, profileId }: V2ProfileParamsDto, + @Query() query: V2TimezoneQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, profileId: ${profileId}`, + 'GET /v2/projects/:pid/profiles/:profileId', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getProfileDetails( + pid, + profileId, + query.timezone, + ) + } + + @Get(':profileId/sessions') + @Auth(true, true) + @ApiOperation({ + summary: 'Profile sessions', + description: 'Paginated sessions recorded for a specific profile.', + }) + async getProfileSessions( + @Param() { pid, profileId }: V2ProfileParamsDto, + @Query() query: V2ListQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, profileId: ${profileId}`, + 'GET /v2/projects/:pid/profiles/:profileId/sessions', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getProfileSessions(pid, profileId, query) + } +} diff --git a/backend/apps/community/src/analytics/v2/controllers/project-v2.controller.ts b/backend/apps/community/src/analytics/v2/controllers/project-v2.controller.ts new file mode 100644 index 000000000..bb3a81ff2 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/controllers/project-v2.controller.ts @@ -0,0 +1,188 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2DimensionsQueryDto, + V2DimensionValuesParamsDto, + V2DimensionValuesQueryDto, + V2FunnelQueryDto, + V2FunnelSessionsQueryDto, +} from '../dto/project.dto' +import { V2ProjectParamsDto } from '../dto/v2-base.dto' +import { SeoV2Service } from '../seo-v2.service' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid') +export class ProjectV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly seoV2Service: SeoV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get('funnel') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Funnel analysis', + description: + 'Funnel step conversion for a saved funnel (funnelId) or an ad-hoc list of steps, with per-step breakdowns and time-to-convert stats.', + }) + async getFunnel( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2FunnelQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/funnel') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getFunnel(pid, query) + } + + @Get('funnel/sessions') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Funnel sessions', + description: + 'Paginated sessions that reached (or dropped off at) a specific funnel step.', + }) + async getFunnelSessions( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2FunnelSessionsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, step: ${query.step}`, + 'GET /v2/projects/:pid/funnel/sessions', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getFunnelSessions(pid, query) + } + + @Get('live-visitors') + @Auth(true, true) + @ApiOperation({ + summary: 'Live visitors', + description: + 'Current online visitor count and the list of active visitors (last 5 minutes).', + }) + async getLiveVisitors( + @Param() { pid }: V2ProjectParamsDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/live-visitors') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getLiveVisitors(pid) + } + + @Get('dimensions') + @Auth(true, true) + @ApiOperation({ + summary: 'Dimension & metric discovery', + description: + 'Machine-readable list of the dimensions and metrics available for a data type — useful for building custom dashboards.', + }) + async getDimensions( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2DimensionsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/dimensions') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + // SEO is served by the Search Console API rather than ClickHouse, so it + // keeps its own registry outside the sqlExpr-based one. + if (query.type === 'seo') { + return this.seoV2Service.getDimensions() + } + + return this.analyticsV2Service.getDimensions(query.type || 'traffic') + } + + @Get('dimensions/:dimension/values') + @Auth(true, true) + @ApiOperation({ + summary: 'Dimension values', + description: + 'Distinct recorded values for a dimension — useful for filter autocompletion. browser_version and os_version return { name, version } pairs.', + }) + async getDimensionValues( + @Param() { pid, dimension }: V2DimensionValuesParamsDto, + @Query() query: V2DimensionValuesQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${dimension}`, + 'GET /v2/projects/:pid/dimensions/:dimension/values', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getDimensionValues( + pid, + dimension, + query.type || 'traffic', + ) + } +} diff --git a/backend/apps/community/src/analytics/v2/controllers/seo-v2.controller.ts b/backend/apps/community/src/analytics/v2/controllers/seo-v2.controller.ts new file mode 100644 index 000000000..85ee82f0c --- /dev/null +++ b/backend/apps/community/src/analytics/v2/controllers/seo-v2.controller.ts @@ -0,0 +1,202 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2SeoBreakdownDto, + V2SeoRangeDto, + V2SeoSummaryDto, + V2SeoTimeseriesDto, +} from '../dto/seo.dto' +import { V2ProjectParamsDto } from '../dto/v2-base.dto' +import { SeoV2Service } from '../seo-v2.service' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/seo') +export class SeoV2Controller { + constructor( + private readonly seoV2Service: SeoV2Service, + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get('status') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Search Console connection status', + description: + 'Whether Google Search Console is connected for this project and which property is linked. The other seo endpoints return 409 until both are true.', + }) + async getStatus( + @Param() { pid }: V2ProjectParamsDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/seo/status') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getStatus(pid) + } + + @Get('summary') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'SEO summary', + description: + 'Clicks, impressions, CTR and average position for the selected period, the previous period of the same length, and the change between them.', + }) + async getSummary( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SeoSummaryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/seo/summary') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getSummary(pid, query) + } + + @Get('timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'SEO timeseries', + description: + 'Selected SEO metrics grouped by time bucket, as rows of objects with ISO-8601 timestamps.', + }) + async getTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SeoTimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/seo/timeseries') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getTimeseries(pid, query) + } + + @Get('breakdown') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'SEO breakdown', + description: + 'SEO metrics grouped by a single dimension (query, page, country, device), paginated. Rows are always ordered by clicks descending.', + }) + async getBreakdown( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SeoBreakdownDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${query.dimension}`, + 'GET /v2/projects/:pid/seo/breakdown', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getBreakdown(pid, query) + } + + @Get('branded-traffic') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Branded vs non-branded traffic', + description: + "Clicks split by whether the search query contained one of the project's brand keywords. Skipped for ranges wider than 31 days.", + }) + async getBrandedTraffic( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SeoRangeDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/seo/branded-traffic') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getBrandedTraffic(pid, query) + } + + @Get('positions') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Search position analytics', + description: + 'Impressions bucketed by search position, and the daily count of ranking queries per position bucket. Skipped for ranges wider than 31 days.', + }) + async getPositions( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SeoRangeDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/seo/positions') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.seoV2Service.getPositions(pid, query) + } +} diff --git a/backend/apps/community/src/analytics/v2/controllers/sessions-v2.controller.ts b/backend/apps/community/src/analytics/v2/controllers/sessions-v2.controller.ts new file mode 100644 index 000000000..9870e3744 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/controllers/sessions-v2.controller.ts @@ -0,0 +1,99 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2SessionParamsDto, + V2SessionsQueryDto, + V2TimezoneQueryDto, +} from '../dto/entities.dto' +import { V2ProjectParamsDto } from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/sessions') +export class SessionsV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get() + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Sessions', + description: + 'Paginated list of individual sessions for the selected period.', + }) + async getSessions( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SessionsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/sessions') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getSessionsList(pid, query) + } + + @Get(':psid') + @Auth(true, true) + @ApiOperation({ + summary: 'Session details', + description: + 'Details for a single session: the page/event flow, session attributes and an activity chart.', + }) + async getSession( + @Param() { pid, psid }: V2SessionParamsDto, + @Query() query: V2TimezoneQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, psid: ${psid}`, + 'GET /v2/projects/:pid/sessions/:psid', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getSessionDetails(pid, psid, query.timezone) + } +} diff --git a/backend/apps/community/src/analytics/v2/controllers/traffic-v2.controller.ts b/backend/apps/community/src/analytics/v2/controllers/traffic-v2.controller.ts new file mode 100644 index 000000000..60c5b40db --- /dev/null +++ b/backend/apps/community/src/analytics/v2/controllers/traffic-v2.controller.ts @@ -0,0 +1,299 @@ +import { + Controller, + Get, + Headers, + Param, + Query, + UseGuards, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common' +import { + ApiBearerAuth, + ApiOperation, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger' + +import { Auth } from '../../../auth/decorators' +import { CurrentUserId } from '../../../auth/decorators/current-user-id.decorator' +import { OptionalJwtAccessTokenGuard } from '../../../auth/guards' +import { AuthenticationGuard } from '../../../auth/guards/authentication.guard' +import { AppLoggerService } from '../../../logger/logger.service' +import { AnalyticsReadGuard } from '../../protection/analytics-read.guard' +import { CacheableAnalytics } from '../../protection/cacheable-analytics.decorator' +import { PublicProjectCacheInterceptor } from '../../protection/public-project-cache.interceptor' +import { AnalyticsV2Service } from '../analytics-v2.service' +import { + V2CustomEventsTimeseriesDto, + V2CustomMetricsQueryDto, + V2EventMetadataQueryDto, + V2PropertyMetadataQueryDto, +} from '../dto/project.dto' +import { + V2BreakdownDto, + V2ListQueryDto, + V2ProjectParamsDto, + V2SummaryDto, + V2TimeseriesDto, +} from '../dto/v2-base.dto' + +@ApiTags('Analytics v2') +@ApiBearerAuth() +@ApiSecurity('apiKey') +@UseGuards(OptionalJwtAccessTokenGuard, AuthenticationGuard, AnalyticsReadGuard) +@UseInterceptors(PublicProjectCacheInterceptor) +@UsePipes(new ValidationPipe({ transform: true })) +@Controller('v2/projects/:pid/traffic') +export class TrafficV2Controller { + constructor( + private readonly analyticsV2Service: AnalyticsV2Service, + private readonly logger: AppLoggerService, + ) {} + + @Get('summary') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Traffic summary', + description: + 'Key traffic metrics for the selected period, the previous period of the same length, and the change between them.', + }) + async getSummary( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2SummaryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/traffic/summary') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficSummary(pid, query) + } + + @Get('timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Traffic timeseries', + description: + 'Selected traffic metrics grouped by time bucket, as rows of objects with ISO-8601 timestamps.', + }) + async getTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2TimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log(`pid: ${pid}`, 'GET /v2/projects/:pid/traffic/timeseries') + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficTimeseries(pid, query) + } + + @Get('breakdown') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Traffic breakdown', + description: + 'Selected traffic metrics grouped by a single dimension (country, page, browser, ...), paginated and sortable.', + }) + async getBreakdown( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2BreakdownDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, dimension: ${query.dimension}`, + 'GET /v2/projects/:pid/traffic/breakdown', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getBreakdown(pid, query, 'traffic') + } + + @Get('custom-events') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Custom events', + description: + 'Counts of custom events recorded in the selected period, sorted by count.', + }) + async getCustomEvents( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2ListQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}`, + 'GET /v2/projects/:pid/traffic/custom-events', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficCustomEvents(pid, query) + } + + @Get('custom-metrics') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Custom metrics', + description: + 'Sum / average of numeric custom event metadata values for the selected period and the previous one (project view custom metrics).', + }) + async getCustomMetrics( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2CustomMetricsQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}`, + 'GET /v2/projects/:pid/traffic/custom-metrics', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficCustomMetrics(pid, query) + } + + @Get('custom-events/timeseries') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Custom events timeseries', + description: + 'Occurrence counts for the selected custom events grouped by time bucket.', + }) + async getCustomEventsTimeseries( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2CustomEventsTimeseriesDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, events: ${query.events}`, + 'GET /v2/projects/:pid/traffic/custom-events/timeseries', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficCustomEventsTimeseries(pid, query) + } + + @Get('custom-events/metadata') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Custom event metadata', + description: + 'Aggregated metadata key/value counts for a specific custom event.', + }) + async getCustomEventMetadata( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2EventMetadataQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, event: ${query.event}`, + 'GET /v2/projects/:pid/traffic/custom-events/metadata', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficCustomEventMetadata(pid, query) + } + + @Get('page-properties/metadata') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Page property values', + description: 'Aggregated value counts for a specific page property (tag).', + }) + async getPagePropertyMetadata( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2PropertyMetadataQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}, property: ${query.property}`, + 'GET /v2/projects/:pid/traffic/page-properties/metadata', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficPagePropertyMetadata(pid, query) + } + + @Get('page-properties') + @Auth(true, true) + @CacheableAnalytics() + @ApiOperation({ + summary: 'Page properties', + description: + 'Counts of pageview metadata (page property) keys recorded in the selected period.', + }) + async getPageProperties( + @Param() { pid }: V2ProjectParamsDto, + @Query() query: V2ListQueryDto, + @CurrentUserId() uid: string, + @Headers() headers: { 'x-password'?: string }, + ) { + this.logger.log( + `pid: ${pid}`, + 'GET /v2/projects/:pid/traffic/page-properties', + ) + + await this.analyticsV2Service.assertReadAccess( + pid, + uid, + headers['x-password'], + ) + + return this.analyticsV2Service.getTrafficPageProperties(pid, query) + } +} diff --git a/backend/apps/community/src/analytics/v2/dto/entities.dto.ts b/backend/apps/community/src/analytics/v2/dto/entities.dto.ts new file mode 100644 index 000000000..b8579e767 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/dto/entities.dto.ts @@ -0,0 +1,149 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Transform, Type } from 'class-transformer' +import { + IsBoolean, + IsEnum, + IsIn, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Max, + Min, +} from 'class-validator' + +import { TimeBucketType } from '../../dto/getData.dto' +import { V2BaseQueryDto, V2ProjectParamsDto } from './v2-base.dto' + +export const V2_MAX_ENTITY_LIMIT = 150 + +class V2EntityListDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + default: 30, + maximum: V2_MAX_ENTITY_LIMIT, + description: 'Number of rows to return', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(V2_MAX_ENTITY_LIMIT) + limit?: number + + @ApiProperty({ + required: false, + default: 0, + description: 'Number of rows to skip (pagination)', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number +} + +export class V2SessionsQueryDto extends V2EntityListDto { + @ApiProperty({ + required: false, + enum: ['traffic', 'performance', 'error'], + default: 'traffic', + description: + 'Event scope for the returned sessions: sessions with traffic events (default), performance events, or errors in the selected range.', + }) + @IsOptional() + @IsIn(['traffic', 'performance', 'error'], { + message: 'The provided event_type is incorrect', + }) + event_type?: 'traffic' | 'performance' | 'error' +} + +export class V2ProfilesQueryDto extends V2EntityListDto { + @ApiProperty({ + required: false, + enum: ['all', 'anonymous', 'identified'], + default: 'all', + description: 'Filter profiles by type', + }) + @IsOptional() + @IsIn(['all', 'anonymous', 'identified'], { + message: 'The provided profile_type is incorrect', + }) + profile_type?: 'all' | 'anonymous' | 'identified' +} + +export class V2ErrorsQueryDto extends V2EntityListDto { + @ApiProperty({ + required: false, + default: false, + description: 'Include resolved error groups in the list', + }) + @IsOptional() + @Transform(({ value }) => { + if (value === true || value === 'true') return true + if (value === false || value === 'false') return false + return value + }) + @IsBoolean() + show_resolved?: boolean +} + +export class V2ErrorDetailQueryDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + enum: TimeBucketType, + description: + 'Time bucket for the error occurrence chart. Defaults to the lowest bucket allowed for the requested range.', + }) + @IsOptional() + @IsEnum(TimeBucketType, { message: 'The provided timeBucket is incorrect' }) + timeBucket?: TimeBucketType +} + +export class V2ErrorSessionsQueryDto extends V2BaseQueryDto { + @ApiProperty({ required: false, default: 10, maximum: 50 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(50) + limit?: number + + @ApiProperty({ required: false, default: 0 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number +} + +export class V2TimezoneQueryDto { + @ApiProperty({ + required: false, + description: 'IANA timezone used for displayed dates', + }) + @IsOptional() + @IsString() + timezone?: string +} + +export class V2SessionParamsDto extends V2ProjectParamsDto { + @ApiProperty({ description: 'The session identifier' }) + @IsNotEmpty() + @IsString() + psid: string +} + +export class V2ProfileParamsDto extends V2ProjectParamsDto { + @ApiProperty({ description: 'The profile identifier' }) + @IsNotEmpty() + @IsString() + profileId: string +} + +export class V2ErrorParamsDto extends V2ProjectParamsDto { + @ApiProperty({ description: 'The error group identifier (eid)' }) + @IsNotEmpty() + @IsString() + eid: string +} diff --git a/backend/apps/community/src/analytics/v2/dto/performance.dto.ts b/backend/apps/community/src/analytics/v2/dto/performance.dto.ts new file mode 100644 index 000000000..acb7c5dd7 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/dto/performance.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger' +import { IsIn, IsOptional } from 'class-validator' + +import { V2BreakdownDto, V2SummaryDto, V2TimeseriesDto } from './v2-base.dto' + +const MEASURE_VALUES = ['median', 'average', 'p75', 'p95', 'quantiles'] + +const MeasureApiProperty = () => + ApiProperty({ + required: false, + enum: MEASURE_VALUES, + default: 'median', + description: + "Aggregate function applied to performance timings. 'quantiles' is only meaningful on the timeseries endpoint (returns p50/p75/p95 of the total load time); other endpoints treat it as 'median'.", + }) + +export class V2PerfTimeseriesDto extends V2TimeseriesDto { + @MeasureApiProperty() + @IsOptional() + @IsIn(MEASURE_VALUES, { message: 'The provided measure is incorrect' }) + measure?: string +} + +export class V2PerfSummaryDto extends V2SummaryDto { + @MeasureApiProperty() + @IsOptional() + @IsIn(MEASURE_VALUES, { message: 'The provided measure is incorrect' }) + measure?: string +} + +export class V2PerfBreakdownDto extends V2BreakdownDto { + @MeasureApiProperty() + @IsOptional() + @IsIn(MEASURE_VALUES, { message: 'The provided measure is incorrect' }) + measure?: string +} diff --git a/backend/apps/community/src/analytics/v2/dto/project.dto.ts b/backend/apps/community/src/analytics/v2/dto/project.dto.ts new file mode 100644 index 000000000..e3cbc8dc5 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/dto/project.dto.ts @@ -0,0 +1,158 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Transform, Type } from 'class-transformer' +import { + IsBoolean, + IsEnum, + IsIn, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Max, + Min, +} from 'class-validator' + +import { TimeBucketType } from '../../dto/getData.dto' +import { V2_MAX_ENTITY_LIMIT } from './entities.dto' +import { V2BaseQueryDto, V2ProjectParamsDto } from './v2-base.dto' + +export class V2FunnelQueryDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + description: + 'JSON array of page paths / custom event names defining the funnel steps. Mutually exclusive with funnelId.', + example: '["/","/pricing","/signup"]', + }) + @IsOptional() + @IsString() + steps?: string + + @ApiProperty({ + required: false, + description: 'ID of a saved funnel. Mutually exclusive with steps.', + }) + @IsOptional() + @IsString() + funnelId?: string +} + +export class V2FunnelSessionsQueryDto extends V2FunnelQueryDto { + @ApiProperty({ + description: + 'Funnel step number the sessions must have reached (1-indexed)', + example: 2, + }) + @Type(() => Number) + @IsInt() + @Min(1) + step: number + + @ApiProperty({ + required: false, + default: false, + description: + 'Return sessions that reached the requested step but did not continue to the next one', + }) + @IsOptional() + @Transform(({ value }) => value === true || value === 'true') + @IsBoolean() + dropoff?: boolean + + @ApiProperty({ required: false, default: 30, maximum: V2_MAX_ENTITY_LIMIT }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(V2_MAX_ENTITY_LIMIT) + limit?: number + + @ApiProperty({ required: false, default: 0 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number +} + +export class V2DimensionsQueryDto { + @ApiProperty({ + required: false, + enum: ['traffic', 'performance', 'captcha', 'errors', 'seo'], + default: 'traffic', + description: 'Data type to list dimensions and metrics for', + }) + @IsOptional() + @IsIn(['traffic', 'performance', 'captcha', 'errors', 'seo'], { + message: 'The provided type is incorrect', + }) + type?: 'traffic' | 'performance' | 'captcha' | 'errors' | 'seo' +} + +export class V2DimensionValuesParamsDto extends V2ProjectParamsDto { + @ApiProperty({ description: "Dimension name, e.g. 'browser'" }) + @IsNotEmpty() + @IsString() + dimension: string +} + +export class V2DimensionValuesQueryDto { + @ApiProperty({ + required: false, + enum: ['traffic', 'errors'], + default: 'traffic', + description: 'Data type to list dimension values for', + }) + @IsOptional() + @IsIn(['traffic', 'errors'], { message: 'The provided type is incorrect' }) + type?: 'traffic' | 'errors' +} + +export class V2CustomMetricsQueryDto extends V2BaseQueryDto { + @ApiProperty({ + description: + 'JSON array of custom metric definitions (project view custom events)', + }) + @IsNotEmpty() + @IsString() + metrics: string +} + +export class V2EventMetadataQueryDto extends V2BaseQueryDto { + @ApiProperty({ description: 'Custom event name to return metadata for' }) + @IsNotEmpty() + @IsString() + event: string + + @ApiProperty({ required: false, enum: TimeBucketType }) + @IsOptional() + @IsEnum(TimeBucketType, { message: 'The provided timeBucket is incorrect' }) + timeBucket?: TimeBucketType +} + +export class V2PropertyMetadataQueryDto extends V2BaseQueryDto { + @ApiProperty({ description: 'Page property (tag) name to return values for' }) + @IsNotEmpty() + @IsString() + property: string + + @ApiProperty({ required: false, enum: TimeBucketType }) + @IsOptional() + @IsEnum(TimeBucketType, { message: 'The provided timeBucket is incorrect' }) + timeBucket?: TimeBucketType +} + +export class V2CustomEventsTimeseriesDto extends V2BaseQueryDto { + @ApiProperty({ + description: + 'Custom event names to chart: a comma-separated list or a JSON array', + example: 'signup,purchase', + }) + @IsNotEmpty() + @IsString() + events: string + + @ApiProperty({ required: false, enum: TimeBucketType }) + @IsOptional() + @IsEnum(TimeBucketType, { message: 'The provided timeBucket is incorrect' }) + timeBucket?: TimeBucketType +} diff --git a/backend/apps/community/src/analytics/v2/dto/seo.dto.ts b/backend/apps/community/src/analytics/v2/dto/seo.dto.ts new file mode 100644 index 000000000..f8153b33a --- /dev/null +++ b/backend/apps/community/src/analytics/v2/dto/seo.dto.ts @@ -0,0 +1,115 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { + IsIn, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Max, + Min, +} from 'class-validator' + +import { TimeBucketType } from '../../dto/getData.dto' +import { SEO_SORT } from '../registry/seo' +import { V2BaseQueryDto } from './v2-base.dto' + +const V2_SEO_MAX_BREAKDOWN_LIMIT = 100 + +/** + * Search Console reports nothing below hourly granularity, so `minute` is + * excluded from the buckets a caller may ask for. + */ +export const V2_SEO_TIME_BUCKETS = [ + TimeBucketType.HOUR, + TimeBucketType.DAY, + TimeBucketType.MONTH, + TimeBucketType.YEAR, +] as const + +export type V2SeoTimeBucket = (typeof V2_SEO_TIME_BUCKETS)[number] + +export class V2SeoSummaryDto extends V2BaseQueryDto {} + +export class V2SeoRangeDto extends V2BaseQueryDto {} + +export class V2SeoTimeseriesDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + enum: V2_SEO_TIME_BUCKETS, + description: + "Time bucket for grouping. One of: hour, day, month, year. Defaults to 'day', or 'hour' for intraday periods.", + }) + @IsOptional() + @IsIn(V2_SEO_TIME_BUCKETS as readonly string[], { + message: `The provided timeBucket is incorrect. Search Console supports: ${V2_SEO_TIME_BUCKETS.join(', ')}`, + }) + timeBucket?: V2SeoTimeBucket + + @ApiProperty({ + required: false, + description: + 'Comma-separated metric names to return. Defaults to clicks, impressions, ctr and position.', + example: 'clicks,impressions', + }) + @IsOptional() + @IsString() + metrics?: string +} + +export class V2SeoBreakdownDto extends V2BaseQueryDto { + @ApiProperty({ + description: + 'Dimension to break down by. One of: query, page, country, device', + example: 'query', + }) + @IsNotEmpty() + @IsString() + dimension: string + + @ApiProperty({ + required: false, + description: + 'Comma-separated metric names to compute per dimension value. Defaults to clicks, impressions, ctr and position.', + example: 'clicks,impressions', + }) + @IsOptional() + @IsString() + metrics?: string + + @ApiProperty({ + required: false, + default: 30, + maximum: V2_SEO_MAX_BREAKDOWN_LIMIT, + description: 'Number of rows to return', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(V2_SEO_MAX_BREAKDOWN_LIMIT) + limit?: number + + @ApiProperty({ + required: false, + default: 0, + description: 'Number of rows to skip (pagination)', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number + + @ApiProperty({ + required: false, + enum: [SEO_SORT], + default: SEO_SORT, + description: `Search Console orders rows by clicks descending and offers no sort control, so '${SEO_SORT}' is the only accepted value.`, + }) + @IsOptional() + @IsIn([SEO_SORT], { + message: `Search Console always orders rows by clicks descending; omit 'sort' or pass '${SEO_SORT}'`, + }) + sort?: string +} diff --git a/backend/apps/community/src/analytics/v2/dto/v2-base.dto.ts b/backend/apps/community/src/analytics/v2/dto/v2-base.dto.ts new file mode 100644 index 000000000..15e675fc1 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/dto/v2-base.dto.ts @@ -0,0 +1,197 @@ +import { ApiProperty } from '@nestjs/swagger' +import { Type } from 'class-transformer' +import { + IsEnum, + IsInt, + IsNotEmpty, + IsOptional, + IsString, + Matches, + Max, + Min, +} from 'class-validator' + +import { PID_REGEX } from '../../../common/constants' +import { DEFAULT_TIMEZONE } from '../../../user/entities/user.entity' +import { ValidatePeriod } from '../../decorators/validate-period.decorator' +import { ChartRenderMode, TimeBucketType } from '../../dto/getData.dto' + +export const V2_DEFAULT_PERIOD = '7d' + +export class V2ProjectParamsDto { + @ApiProperty({ + example: 'aUn1quEid-3', + description: 'The project ID', + }) + @IsNotEmpty() + @Matches(PID_REGEX, { message: 'The provided Project ID (pid) is incorrect' }) + pid: string +} + +export class V2BaseQueryDto { + @ApiProperty({ + required: false, + description: + "Predefined reporting period. One of: 1h, today, yesterday, 1d, 7d, 4w, 3M, 12M, 24M, all. Defaults to '7d'. Mutually exclusive with from/to.", + example: '7d', + }) + @IsOptional() + @ValidatePeriod() + period?: string + + @ApiProperty({ + required: false, + description: + 'Custom range start (ISO 8601 date or datetime). Must be used together with `to`.', + example: '2026-06-01', + }) + @IsOptional() + @IsString() + from?: string + + @ApiProperty({ + required: false, + description: + 'Custom range end (ISO 8601 date or datetime). Must be used together with `from`.', + example: '2026-06-30', + }) + @IsOptional() + @IsString() + to?: string + + @ApiProperty({ + required: false, + description: 'IANA timezone used for bucketing and date boundaries', + default: DEFAULT_TIMEZONE, + example: 'Europe/Kyiv', + }) + @IsOptional() + @IsString() + timezone?: string + + @ApiProperty({ + required: false, + description: + 'JSON array of filter objects: [{ "dimension": "country", "operator": "is" | "is_not" | "contains" | "contains_not", "value": "US" | ["US", "DE"] | null, "key": "" }]', + example: '[{"dimension":"country","operator":"is","value":"US"}]', + }) + @IsOptional() + @IsString() + filters?: string +} + +export class V2TimeseriesDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + enum: TimeBucketType, + description: + 'Time bucket for grouping. Defaults to the lowest bucket allowed for the requested range.', + }) + @IsOptional() + @IsEnum(TimeBucketType, { message: 'The provided timeBucket is incorrect' }) + timeBucket?: TimeBucketType + + @ApiProperty({ + required: false, + description: + 'Comma-separated metric names to return. Defaults depend on the data type.', + example: 'visitors,pageviews', + }) + @IsOptional() + @IsString() + metrics?: string + + @ApiProperty({ + required: false, + enum: ChartRenderMode, + default: ChartRenderMode.PERIODICAL, + description: 'Return periodical (per-bucket) or cumulative values', + }) + @IsOptional() + @IsEnum(ChartRenderMode, { message: 'The provided mode is incorrect' }) + mode?: ChartRenderMode +} + +const V2_MAX_BREAKDOWN_LIMIT = 100 + +export class V2BreakdownDto extends V2BaseQueryDto { + @ApiProperty({ + description: "Dimension to break down by, e.g. 'country', 'page'", + example: 'country', + }) + @IsNotEmpty() + @IsString() + dimension: string + + @ApiProperty({ + required: false, + description: + 'Comma-separated metric names to compute per dimension value. Defaults depend on the data type.', + example: 'visitors,pageviews', + }) + @IsOptional() + @IsString() + metrics?: string + + @ApiProperty({ + required: false, + default: 30, + maximum: V2_MAX_BREAKDOWN_LIMIT, + description: 'Number of rows to return', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(V2_MAX_BREAKDOWN_LIMIT) + limit?: number + + @ApiProperty({ + required: false, + default: 0, + description: 'Number of rows to skip (pagination)', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number + + @ApiProperty({ + required: false, + description: + "Sorting as 'field:direction', where field is 'value' or a selected metric and direction is 'asc' or 'desc'. Defaults to the first selected metric, descending.", + example: 'visitors:desc', + }) + @IsOptional() + @IsString() + sort?: string +} + +export class V2SummaryDto extends V2BaseQueryDto {} + +export class V2ListQueryDto extends V2BaseQueryDto { + @ApiProperty({ + required: false, + default: 30, + maximum: V2_MAX_BREAKDOWN_LIMIT, + description: 'Number of rows to return', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(V2_MAX_BREAKDOWN_LIMIT) + limit?: number + + @ApiProperty({ + required: false, + default: 0, + description: 'Number of rows to skip (pagination)', + }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(0) + offset?: number +} diff --git a/backend/apps/community/src/analytics/v2/mappers/entity.mapper.ts b/backend/apps/community/src/analytics/v2/mappers/entity.mapper.ts new file mode 100644 index 000000000..c63c303b9 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/mappers/entity.mapper.ts @@ -0,0 +1,47 @@ +const KEY_RENAMES: Record = { + cc: 'country', + rg: 'region', + rgc: 'region_code', + ct: 'city', + lc: 'locale', + dv: 'device', + br: 'browser', + brv: 'browser_version', + os: 'os', + osv: 'os_version', + ref: 'referrer', + so: 'utm_source', + me: 'utm_medium', + ca: 'utm_campaign', + te: 'utm_term', + co: 'utm_content', + isp: 'isp', + og: 'organization', + ut: 'user_type', + ctp: 'connection_type', + sdur: 'duration', +} + +export const renameEntityKeys = >( + entity: T, +): Record => { + const result: Record = {} + + for (const [key, value] of Object.entries(entity)) { + result[KEY_RENAMES[key] ?? key] = value + } + + return result +} + +export const renameEntityList = ( + entities: unknown, +): Record[] => { + if (!Array.isArray(entities)) { + return [] + } + + return entities.map((entity) => + renameEntityKeys(entity as Record), + ) +} diff --git a/backend/apps/community/src/analytics/v2/mappers/envelope.ts b/backend/apps/community/src/analytics/v2/mappers/envelope.ts new file mode 100644 index 000000000..ca058667a --- /dev/null +++ b/backend/apps/community/src/analytics/v2/mappers/envelope.ts @@ -0,0 +1,9 @@ +export interface V2Envelope { + data: T + meta: Record +} + +export const envelope = ( + data: T, + meta: Record = {}, +): V2Envelope => ({ data, meta }) diff --git a/backend/apps/community/src/analytics/v2/mappers/seo.mapper.ts b/backend/apps/community/src/analytics/v2/mappers/seo.mapper.ts new file mode 100644 index 000000000..efd1ab46c --- /dev/null +++ b/backend/apps/community/src/analytics/v2/mappers/seo.mapper.ts @@ -0,0 +1,221 @@ +import countries from 'i18n-iso-countries' +import dayjs from 'dayjs' + +import { TimeBucketType } from '../../dto/getData.dto' +import { V2SeoGscDimension } from '../registry/seo' +import { toIsoTimestamp, TimeseriesRow } from './timeseries.mapper' + +export interface SeoMetricValues { + clicks: number + impressions: number + ctr: number + position: number +} + +export type SeoDateSeriesEntry = { date: string } & SeoMetricValues + +export interface SeoSummaryData { + current: SeoMetricValues + previous: SeoMetricValues + change: SeoMetricValues +} + +const round = (value: unknown, precision: number): number => { + const number = Number(value) + + if (!Number.isFinite(number)) { + return 0 + } + + const factor = 10 ** precision + + return Math.round(number * factor) / factor +} + +const toInteger = (value: unknown): number => Math.round(Number(value) || 0) + +/** + * Search Console reports countries as lowercase ISO 3166-1 alpha-3, while the + * rest of the v2 surface (and the `cc` column behind the traffic `country` + * dimension) uses uppercase alpha-2. Normalise so a `country` value means the + * same thing whichever data type produced it. + */ +export const gscCountryToAlpha2 = (country: string): string => { + const alpha2 = countries.alpha3ToAlpha2(String(country || '').toUpperCase()) + + return alpha2 || String(country || '').toUpperCase() +} + +const EMPTY_METRICS: SeoMetricValues = { + clicks: 0, + impressions: 0, + ctr: 0, + position: 0, +} + +const mapSeoMetricValues = ( + values: Partial | null | undefined, +): SeoMetricValues => ({ + clicks: toInteger(values?.clicks), + impressions: toInteger(values?.impressions), + ctr: round(values?.ctr, 2), + position: round(values?.position, 2), +}) + +export const mapSeoSummary = ( + current: Partial | null | undefined, + previous: Partial | null | undefined, +): SeoSummaryData => { + const mappedCurrent = mapSeoMetricValues(current) + const mappedPrevious = previous ? mapSeoMetricValues(previous) : EMPTY_METRICS + + return { + current: mappedCurrent, + previous: mappedPrevious, + change: { + clicks: mappedCurrent.clicks - mappedPrevious.clicks, + impressions: mappedCurrent.impressions - mappedPrevious.impressions, + ctr: round(mappedCurrent.ctr - mappedPrevious.ctr, 2), + position: round(mappedCurrent.position - mappedPrevious.position, 2), + }, + } +} + +/** + * A raw row as returned by one of the GSCService per-dimension getters. They + * disagree on the label field, and getKeywords calls its click count `count`. + */ +export interface GscBreakdownRow extends Partial { + name?: string + page?: string + country?: string + device?: string + count?: number +} + +const readRowValue = ( + dimension: V2SeoGscDimension, + row: GscBreakdownRow, +): string => { + if (dimension === 'query') return row.name ?? '' + if (dimension === 'page') return row.page ?? '' + if (dimension === 'country') return gscCountryToAlpha2(row.country ?? '') + + return row.device ?? '' +} + +export const mapSeoBreakdownRows = ( + dimension: V2SeoGscDimension, + rows: GscBreakdownRow[], + metrics: string[], +): Record[] => + rows.map((row) => { + const values = mapSeoMetricValues({ + clicks: row.clicks ?? row.count, + impressions: row.impressions, + ctr: row.ctr, + position: row.position, + }) + + const mapped: Record = { + value: readRowValue(dimension, row), + } + + for (const metric of metrics) { + mapped[metric] = values[metric as keyof SeoMetricValues] + } + + return mapped + }) + +const startOfBucket = (date: string, bucket: TimeBucketType): string => { + const parsed = dayjs(date) + + if (bucket === TimeBucketType.MONTH) { + return parsed.startOf('month').format('YYYY-MM-DD') + } + + if (bucket === TimeBucketType.YEAR) { + return parsed.startOf('year').format('YYYY-MM-DD') + } + + return date +} + +/** + * Search Console only serves hourly and daily rows, so wider buckets are rolled + * up here. Clicks and impressions sum; ctr is recomputed from the totals and + * position is averaged weighted by impressions, since neither is additive. + */ +export const bucketSeoDateSeries = ( + series: SeoDateSeriesEntry[], + bucket: TimeBucketType, +): SeoDateSeriesEntry[] => { + if (bucket !== TimeBucketType.MONTH && bucket !== TimeBucketType.YEAR) { + return series + } + + const buckets = new Map< + string, + { clicks: number; impressions: number; weightedPosition: number } + >() + const orderedKeys: string[] = [] + + for (const entry of series) { + if (!entry.date) continue + + const key = startOfBucket(entry.date, bucket) + const existing = buckets.get(key) + + if (existing) { + existing.clicks += entry.clicks + existing.impressions += entry.impressions + existing.weightedPosition += entry.position * entry.impressions + } else { + orderedKeys.push(key) + buckets.set(key, { + clicks: entry.clicks, + impressions: entry.impressions, + weightedPosition: entry.position * entry.impressions, + }) + } + } + + return orderedKeys.map((key) => { + const entry = buckets.get(key) + + return { + date: key, + clicks: Math.round(entry.clicks), + impressions: Math.round(entry.impressions), + ctr: + entry.impressions > 0 + ? round((entry.clicks / entry.impressions) * 100, 2) + : 0, + position: + entry.impressions > 0 + ? round(entry.weightedPosition / entry.impressions, 2) + : 0, + } + }) +} + +export const mapSeoTimeseries = ( + series: SeoDateSeriesEntry[], + metrics: string[], + timezone: string, +): TimeseriesRow[] => + series + .filter((entry) => Boolean(entry.date)) + .map((entry) => { + const values = mapSeoMetricValues(entry) + const row: TimeseriesRow = { + timestamp: toIsoTimestamp(entry.date, timezone), + } + + for (const metric of metrics) { + row[metric] = values[metric as keyof SeoMetricValues] + } + + return row + }) diff --git a/backend/apps/community/src/analytics/v2/mappers/summary.mapper.ts b/backend/apps/community/src/analytics/v2/mappers/summary.mapper.ts new file mode 100644 index 000000000..b2b2219f6 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/mappers/summary.mapper.ts @@ -0,0 +1,63 @@ +interface V1SummaryPeriod { + all: number + unique: number + users: number + bounceRate: number + sdur: number | null +} + +export interface V1TrafficSummary { + current: V1SummaryPeriod + previous: V1SummaryPeriod + change: number + uniqueChange: number + usersChange: number + bounceRateChange: number + sdurChange: number | null +} + +export interface TrafficSummaryPeriod { + visitors: number + pageviews: number + users: number + bounce_rate: number + session_duration: number | null +} + +export interface TrafficSummaryData { + current: TrafficSummaryPeriod + previous: TrafficSummaryPeriod + change: TrafficSummaryPeriod +} + +const toNumber = (value: unknown): number => Number(value) || 0 + +const toNullableNumber = (value: unknown): number | null => { + if (value === null || typeof value === 'undefined') { + return null + } + + return Number(value) || 0 +} + +const mapPeriod = (period: V1SummaryPeriod): TrafficSummaryPeriod => ({ + visitors: toNumber(period.unique), + pageviews: toNumber(period.all), + users: toNumber(period.users), + bounce_rate: toNumber(period.bounceRate), + session_duration: toNullableNumber(period.sdur), +}) + +export const mapTrafficSummary = ( + v1Summary: V1TrafficSummary, +): TrafficSummaryData => ({ + current: mapPeriod(v1Summary.current), + previous: mapPeriod(v1Summary.previous), + change: { + visitors: toNumber(v1Summary.uniqueChange), + pageviews: toNumber(v1Summary.change), + users: toNumber(v1Summary.usersChange), + bounce_rate: toNumber(v1Summary.bounceRateChange), + session_duration: toNullableNumber(v1Summary.sdurChange), + }, +}) diff --git a/backend/apps/community/src/analytics/v2/mappers/timeseries.mapper.ts b/backend/apps/community/src/analytics/v2/mappers/timeseries.mapper.ts new file mode 100644 index 000000000..b0f80e876 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/mappers/timeseries.mapper.ts @@ -0,0 +1,59 @@ +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +import dayjsTimezone from 'dayjs/plugin/timezone' + +import { DEFAULT_TIMEZONE } from '../../../user/entities/user.entity' + +dayjs.extend(utc) +dayjs.extend(dayjsTimezone) + +export type TimeseriesRow = { timestamp: string } & Record< + string, + string | number | null +> + +export const toIsoTimestamp = (value: string, timezone: string): string => { + const zone = timezone || DEFAULT_TIMEZONE + + return dayjs.tz(value, zone).format() +} + +export const zipTimeseries = ( + x: string[], + series: Record, + timezone: string, +): TimeseriesRow[] => { + const names = Object.keys(series).filter((name) => + Array.isArray(series[name]), + ) + + return x.map((label, index) => { + const row: TimeseriesRow = { timestamp: toIsoTimestamp(label, timezone) } + + for (const name of names) { + const value = series[name][index] + row[name] = typeof value === 'undefined' ? null : value + } + + return row + }) +} + +export const computeBounceRateSeries = ( + bounces: number[] | undefined, + uniques: number[] | undefined, +): number[] | undefined => { + if (!Array.isArray(bounces) || !Array.isArray(uniques)) { + return undefined + } + + return bounces.map((bounceCount, index) => { + const uniqueCount = Number(uniques[index]) || 0 + + if (uniqueCount <= 0) { + return 0 + } + + return Math.round((Number(bounceCount) / uniqueCount) * 100 * 100) / 100 + }) +} diff --git a/backend/apps/community/src/analytics/v2/query/breakdown-query.builder.ts b/backend/apps/community/src/analytics/v2/query/breakdown-query.builder.ts new file mode 100644 index 000000000..645f15f9a --- /dev/null +++ b/backend/apps/community/src/analytics/v2/query/breakdown-query.builder.ts @@ -0,0 +1,106 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { getCaptchaColumnExpression } from '../../analytics.service' +import { + captchaExtraWhere, + V2DataType, + V2DimensionDef, + V2MetricCtx, + V2MetricDef, +} from '../registry' + +export interface BreakdownSort { + field: string + direction: 'asc' | 'desc' +} + +export interface BreakdownQueryOptions { + dataType: V2DataType + dimension: V2DimensionDef + metrics: V2MetricDef[] + subQuery: string + ctx: V2MetricCtx + sort: BreakdownSort +} + +export const parseSortParam = ( + sort: string | undefined, + dimension: V2DimensionDef, + metrics: V2MetricDef[], +): BreakdownSort => { + if (!sort) { + return { field: metrics[0].api, direction: 'desc' } + } + + const [field, direction = 'desc'] = sort.split(':') + + if (direction !== 'asc' && direction !== 'desc') { + throw new UnprocessableEntityException( + `Invalid sort direction '${direction}'; use 'asc' or 'desc'`, + ) + } + + const sortable = ['value', ...metrics.map((metric) => metric.api)] + + if (!sortable.includes(field)) { + throw new UnprocessableEntityException( + `Cannot sort '${dimension.api}' breakdown by '${field}'. Sortable fields: ${sortable.join(', ')}`, + ) + } + + return { field, direction } +} + +export const buildBreakdownQuery = (opts: BreakdownQueryOptions): string => { + const { dataType, dimension, metrics, subQuery, ctx, sort } = opts + + const dimensionSql = + dataType === 'captcha' + ? getCaptchaColumnExpression(dimension.column) + : dimension.column + + const dimensionCols = [ + `${dimensionSql} AS value`, + ...(dimension.extraFields ?? []).map( + (field) => `${field.column} AS ${field.api}`, + ), + ] + + const metricCols = metrics.map((metric) => { + const expr = metric.sqlExpr(ctx) + + if (!expr) { + throw new UnprocessableEntityException( + `The '${metric.api}' metric is not supported on breakdowns. Request it on the timeseries or summary endpoint instead`, + ) + } + + return `${expr} AS ${metric.api}` + }) + + const groupBy = ['value', ...(dimension.extraFields ?? []).map((f) => f.api)] + + const whereParts: string[] = [] + + if (dimension.excludeNull) { + whereParts.push(`AND ${dimension.column} IS NOT NULL`) + } + + if (dataType === 'captcha') { + const guard = captchaExtraWhere(dimension) + if (guard) { + whereParts.push(guard) + } + } + + return ` + SELECT + ${[...dimensionCols, ...metricCols].join(',\n ')}, + count() OVER () AS __total + ${subQuery} + ${whereParts.join(' ')} + GROUP BY ${groupBy.join(', ')} + ORDER BY ${sort.field} ${sort.direction} + LIMIT {v2_limit:UInt32} OFFSET {v2_offset:UInt32} + ` +} diff --git a/backend/apps/community/src/analytics/v2/query/filters.translator.ts b/backend/apps/community/src/analytics/v2/query/filters.translator.ts new file mode 100644 index 000000000..833cb0900 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/query/filters.translator.ts @@ -0,0 +1,223 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { + findDimension, + V2_KEYED_FILTER_DIMENSIONS, + V2DataType, +} from '../registry' + +const V2_FILTER_OPERATORS = [ + 'is', + 'is_not', + 'contains', + 'contains_not', +] as const + +type V2FilterOperator = (typeof V2_FILTER_OPERATORS)[number] + +export interface V2Filter { + dimension: string + operator: V2FilterOperator + value: string | null | (string | null)[] + key?: string +} + +interface V1Filter { + column: string + filter: string | null | (string | null)[] + isExclusive: boolean + isContains: boolean +} + +const parseFiltersJson = (filters: string): unknown => { + try { + return JSON.parse(filters) + } catch { + throw new UnprocessableEntityException( + 'The provided filters are not a valid JSON array', + ) + } +} + +const resolveColumn = ( + filter: V2Filter, + type: V2DataType, + lenient: boolean, +): string | null => { + const { dimension, key } = filter + + const keyedFamily = Object.hasOwn(V2_KEYED_FILTER_DIMENSIONS, dimension) + ? V2_KEYED_FILTER_DIMENSIONS[dimension] + : undefined + + if (keyedFamily) { + if (!keyedFamily.types.includes(type)) { + if (lenient) { + return null + } + + throw new UnprocessableEntityException( + `The '${dimension}' filter is not supported for ${type} data`, + ) + } + + return key ? `${keyedFamily.v1Prefix}${key}` : keyedFamily.v1KeyColumn + } + + const def = findDimension(dimension, type) + + if (!def) { + if (lenient) { + return null + } + + throw new UnprocessableEntityException( + `Unknown ${type} filter dimension '${dimension}'`, + ) + } + + return def.column +} + +export const parseV2Filters = (filters: string | undefined): V2Filter[] => { + if (!filters || filters === '""') { + return [] + } + + const parsed = parseFiltersJson(filters) + + if (!Array.isArray(parsed)) { + throw new UnprocessableEntityException( + 'The filters parameter must be a JSON array of filter objects', + ) + } + + return parsed.map((raw, index) => { + if (!raw || typeof raw !== 'object') { + throw new UnprocessableEntityException( + `Filter at index ${index} must be an object`, + ) + } + + const { dimension, operator, value, key } = raw as Record + + if (typeof dimension !== 'string' || !dimension) { + throw new UnprocessableEntityException( + `Filter at index ${index} is missing a 'dimension'`, + ) + } + + if (!V2_FILTER_OPERATORS.includes(operator as V2FilterOperator)) { + throw new UnprocessableEntityException( + `Filter at index ${index} has an invalid operator. Supported operators: ${V2_FILTER_OPERATORS.join(', ')}`, + ) + } + + const isValidValue = (v: unknown) => v === null || typeof v === 'string' + + if (Array.isArray(value)) { + if (!value.every(isValidValue)) { + throw new UnprocessableEntityException( + `Filter at index ${index} has an invalid 'value'; array values must be strings or null`, + ) + } + } else if (!isValidValue(value)) { + throw new UnprocessableEntityException( + `Filter at index ${index} has an invalid 'value'; it must be a string, null or an array`, + ) + } + + if (key !== undefined && (typeof key !== 'string' || !key)) { + throw new UnprocessableEntityException( + `Filter at index ${index} has an invalid 'key'; it must be a non-empty string`, + ) + } + + if ( + key !== undefined && + !Object.hasOwn(V2_KEYED_FILTER_DIMENSIONS, dimension) + ) { + throw new UnprocessableEntityException( + `Filter at index ${index} has a 'key', but the '${dimension}' dimension does not support keys. Keyed dimensions: ${Object.keys(V2_KEYED_FILTER_DIMENSIONS).join(', ')}`, + ) + } + + const filter: V2Filter = { + dimension, + operator: operator as V2FilterOperator, + value: value as V2Filter['value'], + } + + if (typeof key === 'string' && key) { + filter.key = key + } + + return filter + }) +} + +export const toV1FiltersJson = ( + filters: V2Filter[], + type: V2DataType, + options: { lenient?: boolean } = {}, +): string => { + if (filters.length === 0) { + return '' + } + + const v1Filters: V1Filter[] = [] + + for (const filter of filters) { + const column = resolveColumn(filter, type, Boolean(options.lenient)) + + if (column === null) { + continue + } + + v1Filters.push({ + column, + filter: filter.value, + isExclusive: + filter.operator === 'is_not' || filter.operator === 'contains_not', + isContains: + filter.operator === 'contains' || filter.operator === 'contains_not', + }) + } + + return JSON.stringify(v1Filters) +} + +export const normalizeFiltersToV1Json = ( + filters: string | undefined, + type: V2DataType, + options: { lenient?: boolean } = {}, +): string => { + if (!filters || filters === '""') { + return '' + } + + let parsed: unknown + try { + parsed = JSON.parse(filters) + } catch { + return filters + } + + if (!Array.isArray(parsed) || parsed.length === 0) { + return '' + } + + const isLegacyV1 = parsed.every( + (filter) => + filter && + typeof filter === 'object' && + 'column' in filter && + !('dimension' in filter), + ) + + if (isLegacyV1) { + return filters + } + + return toV1FiltersJson(parseV2Filters(filters), type, options) +} diff --git a/backend/apps/community/src/analytics/v2/registry/dimensions.ts b/backend/apps/community/src/analytics/v2/registry/dimensions.ts new file mode 100644 index 000000000..22858d453 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/registry/dimensions.ts @@ -0,0 +1,264 @@ +import { + CAPTCHA_EVENT_SQL, + CAPTCHA_DIFFICULTY_SQL, + CAPTCHA_REASON_SQL, + CAPTCHA_SOLVE_MS_SQL, +} from '../../analytics.service' +import { V2DimensionDef } from './types' + +const GEO_TYPES: V2DimensionDef['types'] = ['traffic', 'performance', 'errors'] + +export const V2_DIMENSIONS: V2DimensionDef[] = [ + { + api: 'country', + column: 'cc', + types: ['traffic', 'performance', 'captcha', 'errors'], + description: 'Country (ISO 3166-1 alpha-2 code)', + }, + { + api: 'region', + column: 'rg', + types: GEO_TYPES, + excludeNull: true, + extraFields: [ + { api: 'country', column: 'cc' }, + { api: 'region_code', column: 'rgc' }, + ], + description: 'Region / subdivision', + }, + { + api: 'city', + column: 'ct', + types: GEO_TYPES, + excludeNull: true, + extraFields: [{ api: 'country', column: 'cc' }], + description: 'City', + }, + { + api: 'page', + column: 'pg', + types: GEO_TYPES, + description: 'Page path', + }, + { + api: 'host', + column: 'host', + types: GEO_TYPES, + description: 'Hostname the event was recorded on', + }, + { + api: 'locale', + column: 'lc', + types: ['traffic', 'errors'], + description: 'Browser locale (e.g. en-GB)', + }, + { + api: 'browser', + column: 'br', + types: ['traffic', 'performance', 'captcha', 'errors'], + description: 'Browser name', + }, + { + api: 'browser_version', + column: 'brv', + types: GEO_TYPES, + extraFields: [{ api: 'browser', column: 'br' }], + description: 'Browser version', + }, + { + api: 'os', + column: 'os', + types: ['traffic', 'captcha', 'errors'], + description: 'Operating system name', + }, + { + api: 'os_version', + column: 'osv', + types: ['traffic', 'errors'], + extraFields: [{ api: 'os', column: 'os' }], + description: 'Operating system version', + }, + { + api: 'device', + column: 'dv', + types: ['traffic', 'performance', 'captcha', 'errors'], + description: 'Device type (desktop, mobile, tablet, ...)', + }, + { + api: 'referrer', + column: 'ref', + types: ['traffic'], + description: 'Full referrer URL', + }, + { + api: 'referrer_name', + column: 'refn', + types: ['traffic'], + filterOnly: true, + description: + 'Canonical referrer (root domain), filter-only; use the referrer dimension for breakdowns', + }, + { + api: 'utm_source', + column: 'so', + types: ['traffic'], + excludeNull: true, + description: 'UTM source', + }, + { + api: 'utm_medium', + column: 'me', + types: ['traffic'], + excludeNull: true, + description: 'UTM medium', + }, + { + api: 'utm_campaign', + column: 'ca', + types: ['traffic'], + excludeNull: true, + description: 'UTM campaign', + }, + { + api: 'utm_term', + column: 'te', + types: ['traffic'], + excludeNull: true, + description: 'UTM term', + }, + { + api: 'utm_content', + column: 'co', + types: ['traffic'], + excludeNull: true, + description: 'UTM content', + }, + { + api: 'isp', + column: 'isp', + types: GEO_TYPES, + excludeNull: true, + description: 'Internet service provider', + }, + { + api: 'organization', + column: 'og', + types: GEO_TYPES, + excludeNull: true, + description: 'Network organization', + }, + { + api: 'user_type', + column: 'ut', + types: GEO_TYPES, + excludeNull: true, + description: 'Connection user type (residential, hosting, ...)', + }, + { + api: 'connection_type', + column: 'ctp', + types: GEO_TYPES, + excludeNull: true, + description: 'Connection type (Cable/DSL, Cellular, ...)', + }, + { + api: 'entry_page', + column: 'entryPage', + types: ['traffic', 'errors'], + virtual: 'entry_page', + description: 'First page of the session', + }, + { + api: 'exit_page', + column: 'exitPage', + types: ['traffic', 'errors'], + virtual: 'exit_page', + description: 'Last page of the session', + }, + { + api: 'event', + column: 'ev', + types: ['traffic'], + filterOnly: true, + description: + 'Custom event name (filter-only; use /traffic/custom-events for a breakdown)', + }, + { + api: 'error_name', + column: 'name', + types: ['errors'], + filterOnly: true, + description: 'Error name', + }, + { + api: 'error_message', + column: 'message', + types: ['errors'], + filterOnly: true, + description: 'Error message', + }, + { + api: 'error_filename', + column: 'filename', + types: ['errors'], + filterOnly: true, + description: 'Source file the error originated from', + }, + { + api: 'captcha_event', + column: 'captcha_event', + types: ['captcha'], + description: 'Captcha lifecycle event (generate, pass, verify_fail, ...)', + }, + { + api: 'captcha_difficulty', + column: 'captcha_difficulty', + types: ['captcha'], + extraWhere: `AND ${CAPTCHA_EVENT_SQL} = 'generate' AND toUInt8OrZero(${CAPTCHA_DIFFICULTY_SQL}) > 0`, + description: 'Captcha difficulty level', + }, + { + api: 'captcha_reason', + column: 'captcha_reason', + types: ['captcha'], + extraWhere: `AND ${CAPTCHA_EVENT_SQL} = 'generate' AND ${CAPTCHA_REASON_SQL} NOT IN ('', 'baseline', 'manual')`, + description: 'Reason for elevated captcha difficulty', + }, + { + api: 'solve_time', + column: 'solve_time', + types: ['captcha'], + extraWhere: `AND ${CAPTCHA_EVENT_SQL} = 'pass' AND ${CAPTCHA_SOLVE_MS_SQL} > 0`, + description: 'Captcha solve-time bucket', + }, +] + +const CAPTCHA_PASS_GUARD = `AND ${CAPTCHA_EVENT_SQL} = 'pass'` + +export const captchaExtraWhere = (dim: V2DimensionDef): string | undefined => { + if (dim.extraWhere) { + return dim.extraWhere + } + + if (['cc', 'br', 'os', 'dv'].includes(dim.column)) { + return CAPTCHA_PASS_GUARD + } + + return undefined +} + +export const V2_KEYED_FILTER_DIMENSIONS: Record< + string, + { v1Prefix: string; v1KeyColumn: string; types: V2DimensionDef['types'] } +> = { + event_metadata: { + v1Prefix: 'ev:key:', + v1KeyColumn: 'ev:key', + types: ['traffic'], + }, + page_property: { + v1Prefix: 'tag:key:', + v1KeyColumn: 'tag:key', + types: ['traffic', 'errors'], + }, +} diff --git a/backend/apps/community/src/analytics/v2/registry/index.ts b/backend/apps/community/src/analytics/v2/registry/index.ts new file mode 100644 index 000000000..7fd13aa32 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/registry/index.ts @@ -0,0 +1,101 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { DataType } from '../../analytics.service' +import { V2_DIMENSIONS } from './dimensions' +import { V2_METRICS } from './metrics' +import { V2DataType, V2DimensionDef, V2MetricDef } from './types' + +export * from './types' +export { + V2_DIMENSIONS, + V2_KEYED_FILTER_DIMENSIONS, + captchaExtraWhere, +} from './dimensions' + +export const V2_TO_V1_DATATYPE: Record = { + traffic: DataType.ANALYTICS, + performance: DataType.PERFORMANCE, + captcha: DataType.CAPTCHA, + errors: DataType.ERRORS, +} + +export const listDimensions = (type: V2DataType): V2DimensionDef[] => + V2_DIMENSIONS.filter((dimension) => dimension.types.includes(type)) + +export const listMetrics = (type: V2DataType): V2MetricDef[] => + V2_METRICS.filter((metric) => metric.types.includes(type)) + +export const findDimension = ( + api: string, + type: V2DataType, +): V2DimensionDef | undefined => + V2_DIMENSIONS.find( + (dimension) => dimension.api === api && dimension.types.includes(type), + ) + +export const getBreakdownDimension = ( + api: string, + type: V2DataType, +): V2DimensionDef => { + const dimension = findDimension(api, type) + + if (!dimension) { + throw new UnprocessableEntityException( + `Unknown ${type} dimension '${api}'. Supported dimensions: ${listDimensions( + type, + ) + .filter((d) => !d.filterOnly) + .map((d) => d.api) + .join(', ')}`, + ) + } + + if (dimension.filterOnly) { + throw new UnprocessableEntityException( + `The '${api}' dimension can only be used in filters, not as a breakdown dimension`, + ) + } + + return dimension +} + +export const getMetric = (api: string, type: V2DataType): V2MetricDef => { + const metric = V2_METRICS.find((m) => m.api === api && m.types.includes(type)) + + if (!metric) { + throw new UnprocessableEntityException( + `Unknown ${type} metric '${api}'. Supported metrics: ${listMetrics(type) + .map((m) => m.api) + .join(', ')}`, + ) + } + + return metric +} + +export const getDefaultMetrics = (type: V2DataType): V2MetricDef[] => + listMetrics(type).filter((metric) => metric.isDefault) + +export const parseMetricsParam = ( + metrics: string | undefined, + type: V2DataType, +): V2MetricDef[] => { + if (!metrics) { + return getDefaultMetrics(type) + } + + const names = [ + ...new Set( + metrics + .split(',') + .map((name) => name.trim()) + .filter(Boolean), + ), + ] + + if (names.length === 0) { + return getDefaultMetrics(type) + } + + return names.map((name) => getMetric(name, type)) +} diff --git a/backend/apps/community/src/analytics/v2/registry/metrics.ts b/backend/apps/community/src/analytics/v2/registry/metrics.ts new file mode 100644 index 000000000..84b715f91 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/registry/metrics.ts @@ -0,0 +1,187 @@ +import { V2MetricDef } from './types' + +const MEASURES_MAP_V2: Record = { + average: 'avg', + median: 'median', + p95: 'quantileExact(0.95)', + p75: 'quantileExact(0.75)', +} + +const perfExpr = + (column: string) => + ({ measure }: { measure?: string }): string => { + const processedMeasure = + !measure || measure === 'quantiles' ? 'median' : measure + const fn = MEASURES_MAP_V2[processedMeasure] || 'median' + return `round(divide(${fn}(${column}), 1000), 2)` + } + +export const V2_METRICS: V2MetricDef[] = [ + { + api: 'visitors', + types: ['traffic'], + sqlExpr: ({ customEVFilterApplied }) => + customEVFilterApplied ? 'count(*)' : 'count(DISTINCT psid)', + isDefault: true, + format: 'integer', + description: 'Unique sessions', + }, + { + api: 'pageviews', + types: ['traffic'], + sqlExpr: () => 'count(*)', + isDefault: true, + format: 'integer', + description: 'Pageview (or matched custom event) count', + }, + { + api: 'users', + types: ['traffic'], + sqlExpr: () => 'count(DISTINCT profileId)', + format: 'integer', + description: 'Unique users (identified + anonymous profiles)', + }, + { + api: 'session_duration', + types: ['traffic'], + sqlExpr: () => null, + requiresSessionsJoin: true, + format: 'seconds', + description: + 'Average session duration in seconds (timeseries and summary only)', + }, + { + api: 'bounce_rate', + types: ['traffic'], + sqlExpr: () => null, + requiresSessionsJoin: true, + format: 'percent', + description: + 'Percentage of single-pageview sessions (timeseries and summary only)', + }, + { + api: 'concurrency', + types: ['traffic'], + sqlExpr: () => null, + requiresSessionsJoin: true, + format: 'integer', + description: + 'Concurrent live visitors over time (timeseries only; ignores filters)', + }, + { + api: 'load_time', + types: ['performance'], + sqlExpr: perfExpr('pageLoad'), + isDefault: true, + format: 'seconds', + description: 'Full page load time', + }, + { + api: 'ttfb', + types: ['performance'], + sqlExpr: perfExpr('ttfb'), + format: 'seconds', + description: 'Time to first byte', + }, + { + api: 'dns', + types: ['performance'], + sqlExpr: perfExpr('dns'), + format: 'seconds', + description: 'DNS resolution time', + }, + { + api: 'tls', + types: ['performance'], + sqlExpr: perfExpr('tls'), + format: 'seconds', + description: 'TLS setup time', + }, + { + api: 'connection', + types: ['performance'], + sqlExpr: perfExpr('conn'), + format: 'seconds', + description: 'Connection establishment time', + }, + { + api: 'response', + types: ['performance'], + sqlExpr: perfExpr('response'), + format: 'seconds', + description: 'Response time', + }, + { + api: 'render', + types: ['performance'], + sqlExpr: perfExpr('render'), + format: 'seconds', + description: 'Browser render time', + }, + { + api: 'dom_load', + types: ['performance'], + sqlExpr: perfExpr('domLoad'), + format: 'seconds', + description: 'DOM content load time', + }, + { + api: 'occurrences', + types: ['errors'], + sqlExpr: () => 'count(*)', + isDefault: true, + format: 'integer', + description: 'Error occurrences', + }, + { + api: 'affected_users', + types: ['errors'], + sqlExpr: () => 'uniqExact(profileId)', + format: 'integer', + description: 'Distinct users affected', + }, + { + api: 'events', + types: ['captcha'], + sqlExpr: () => 'count(*)', + isDefault: true, + format: 'integer', + description: 'Captcha events matching the dimension scope', + }, + { + api: 'generated', + types: ['captcha'], + sqlExpr: () => null, + format: 'integer', + description: 'Captchas generated (timeseries and summary only)', + }, + { + api: 'passed', + types: ['captcha'], + sqlExpr: () => null, + format: 'integer', + description: 'Captchas passed (timeseries and summary only)', + }, + { + api: 'failed', + types: ['captcha'], + sqlExpr: () => null, + format: 'integer', + description: 'Captchas failed verification (timeseries and summary only)', + }, + { + api: 'validation_failed', + types: ['captcha'], + sqlExpr: () => null, + format: 'integer', + description: + 'Captchas that failed validation (timeseries and summary only)', + }, + { + api: 'replayed', + types: ['captcha'], + sqlExpr: () => null, + format: 'integer', + description: 'Replayed captcha attempts (timeseries and summary only)', + }, +] diff --git a/backend/apps/community/src/analytics/v2/registry/seo.ts b/backend/apps/community/src/analytics/v2/registry/seo.ts new file mode 100644 index 000000000..7128783ee --- /dev/null +++ b/backend/apps/community/src/analytics/v2/registry/seo.ts @@ -0,0 +1,193 @@ +import { UnprocessableEntityException } from '@nestjs/common' + +import { V2Filter } from '../query/filters.translator' + +/** + * SEO data is served by the Google Search Console API rather than ClickHouse, + * so it cannot join the column/sqlExpr registry in ./dimensions and ./metrics. + * These definitions mirror that registry's shape and lookup semantics to keep + * /v2/projects/:pid/seo/* consistent with the rest of the v2 surface. + */ + +export type V2SeoGscDimension = 'query' | 'page' | 'country' | 'device' + +export interface V2SeoDimensionDef { + api: string + gsc: V2SeoGscDimension + description: string +} + +export interface V2SeoMetricDef { + api: string + isDefault?: boolean + format: 'integer' | 'percent' | 'position' + description: string +} + +export const V2_SEO_DIMENSIONS: V2SeoDimensionDef[] = [ + { + api: 'query', + gsc: 'query', + description: 'Search query the site was surfaced for', + }, + { + api: 'page', + gsc: 'page', + description: 'Landing page URL that appeared in search results', + }, + { + api: 'country', + gsc: 'country', + description: 'Country the search was made from (ISO 3166-1 alpha-2 code)', + }, + { + api: 'device', + gsc: 'device', + description: 'Device category the search was made on', + }, +] + +const V2_SEO_METRICS: V2SeoMetricDef[] = [ + { + api: 'clicks', + isDefault: true, + format: 'integer', + description: 'Clicks through to the site from search results', + }, + { + api: 'impressions', + isDefault: true, + format: 'integer', + description: 'Times a link to the site appeared in search results', + }, + { + api: 'ctr', + isDefault: true, + format: 'percent', + description: 'Click-through rate, as a percentage of impressions', + }, + { + api: 'position', + isDefault: true, + format: 'position', + description: 'Average position in search results', + }, +] + +export const listSeoDimensions = (): V2SeoDimensionDef[] => V2_SEO_DIMENSIONS + +export const listSeoMetrics = (): V2SeoMetricDef[] => V2_SEO_METRICS + +const findSeoDimension = (api: string): V2SeoDimensionDef | undefined => + V2_SEO_DIMENSIONS.find((dimension) => dimension.api === api) + +export const getSeoBreakdownDimension = (api: string): V2SeoDimensionDef => { + const dimension = findSeoDimension(api) + + if (!dimension) { + throw new UnprocessableEntityException( + `Unknown seo dimension '${api}'. Supported dimensions: ${V2_SEO_DIMENSIONS.map( + (d) => d.api, + ).join(', ')}`, + ) + } + + return dimension +} + +export const getSeoMetric = (api: string): V2SeoMetricDef => { + const metric = V2_SEO_METRICS.find((m) => m.api === api) + + if (!metric) { + throw new UnprocessableEntityException( + `Unknown seo metric '${api}'. Supported metrics: ${V2_SEO_METRICS.map( + (m) => m.api, + ).join(', ')}`, + ) + } + + return metric +} + +export const getDefaultSeoMetrics = (): V2SeoMetricDef[] => + V2_SEO_METRICS.filter((metric) => metric.isDefault) + +export const parseSeoMetricsParam = ( + metrics: string | undefined, +): V2SeoMetricDef[] => { + if (!metrics) { + return getDefaultSeoMetrics() + } + + const names = [ + ...new Set( + metrics + .split(',') + .map((name) => name.trim()) + .filter(Boolean), + ), + ] + + if (names.length === 0) { + return getDefaultSeoMetrics() + } + + return names.map((name) => getSeoMetric(name)) +} + +/** + * Search Console orders rows by clicks descending and exposes no sort control, + * so this is the only ordering the seo breakdown can honour. It is accepted + * (rather than rejected outright) so callers can be explicit about it. + */ +export const SEO_SORT = 'clicks:desc' + +/** + * GSCService.parseFilters keys filters by their v1 URL names, where the keyword + * dimension is 'keywords'. v2 calls it 'query', after Search Console's own + * naming, so translate on the way down — the same v2 -> v1 hop the ClickHouse + * data types make through toV1FiltersJson. + */ +export const toGscFiltersJson = (filters: V2Filter[]): string | undefined => { + if (filters.length === 0) { + return undefined + } + + return JSON.stringify( + filters.map((filter) => ({ + ...filter, + dimension: filter.dimension === 'query' ? 'keywords' : filter.dimension, + })), + ) +} + +/** + * Search Console expresses filters as dimensionFilterGroups of scalar + * comparisons, so the array / null values the rest of v2 accepts have no + * equivalent here. Reject them rather than silently dropping the filter. + */ +export const validateSeoFilters = (filters: V2Filter[]): V2Filter[] => { + for (const filter of filters) { + if (!findSeoDimension(filter.dimension)) { + throw new UnprocessableEntityException( + `Unknown seo filter dimension '${filter.dimension}'. Supported dimensions: ${V2_SEO_DIMENSIONS.map( + (d) => d.api, + ).join(', ')}`, + ) + } + + if (Array.isArray(filter.value)) { + throw new UnprocessableEntityException( + `The seo filter on '${filter.dimension}' cannot take an array of values; Search Console only supports a single value per filter`, + ) + } + + if (filter.value === null) { + throw new UnprocessableEntityException( + `The seo filter on '${filter.dimension}' cannot take a null value; Search Console does not report unset dimension values`, + ) + } + } + + return filters +} diff --git a/backend/apps/community/src/analytics/v2/registry/types.ts b/backend/apps/community/src/analytics/v2/registry/types.ts new file mode 100644 index 000000000..49ee973b5 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/registry/types.ts @@ -0,0 +1,35 @@ +import { PerfMeasure } from '../../interfaces' + +export type V2DataType = 'traffic' | 'performance' | 'captcha' | 'errors' + +interface V2DimensionExtraField { + api: string + column: string +} + +export interface V2DimensionDef { + api: string + column: string + types: V2DataType[] + extraFields?: V2DimensionExtraField[] + excludeNull?: boolean + filterOnly?: boolean + virtual?: 'entry_page' | 'exit_page' + extraWhere?: string + description: string +} + +export interface V2MetricCtx { + customEVFilterApplied: boolean + measure?: PerfMeasure +} + +export interface V2MetricDef { + api: string + types: V2DataType[] + sqlExpr: (ctx: V2MetricCtx) => string | null + requiresSessionsJoin?: boolean + isDefault?: boolean + format?: 'integer' | 'seconds' | 'percent' + description: string +} diff --git a/backend/apps/community/src/analytics/v2/seo-v2.service.ts b/backend/apps/community/src/analytics/v2/seo-v2.service.ts new file mode 100644 index 000000000..a8261e0a3 --- /dev/null +++ b/backend/apps/community/src/analytics/v2/seo-v2.service.ts @@ -0,0 +1,512 @@ +import { + BadRequestException, + ConflictException, + Injectable, + UnprocessableEntityException, +} from '@nestjs/common' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' + +import { + BrandedTrafficAnalytics, + GSC_ALL_TIME_DAYS, + GSCContext, + GSCService, + MAX_BRANDED_TRAFFIC_RANGE_DAYS, + MAX_FILTER_EXPRESSION_LENGTH, + MAX_FILTERS, + MAX_POSITION_ANALYTICS_RANGE_DAYS, + OPTIONAL_ANALYTICS_TIMEOUT_MS, + PositionAnalytics, +} from '../../project/gsc.service' +import { AnalyticsService } from '../analytics.service' +import { TimeBucketType } from '../dto/getData.dto' +import { + V2SeoBreakdownDto, + V2SeoRangeDto, + V2SeoSummaryDto, + V2SeoTimeseriesDto, + V2_SEO_TIME_BUCKETS, +} from './dto/seo.dto' +import { V2_DEFAULT_PERIOD, V2BaseQueryDto } from './dto/v2-base.dto' +import { envelope, V2Envelope } from './mappers/envelope' +import { + bucketSeoDateSeries, + mapSeoBreakdownRows, + mapSeoSummary, + mapSeoTimeseries, + SeoDateSeriesEntry, + SeoSummaryData, +} from './mappers/seo.mapper' +import { toIsoTimestamp, TimeseriesRow } from './mappers/timeseries.mapper' +import { parseV2Filters, V2Filter } from './query/filters.translator' +import { + getSeoBreakdownDimension, + listSeoDimensions, + listSeoMetrics, + parseSeoMetricsParam, + SEO_SORT, + toGscFiltersJson, + validateSeoFilters, +} from './registry/seo' + +dayjs.extend(utc) + +const DEFAULT_SEO_BREAKDOWN_LIMIT = 30 + +const INTRADAY_PERIODS = ['1h', 'today', 'yesterday', '1d'] + +interface SeoTimeframe { + period: string | null + timeBucket: TimeBucketType + safeTimezone: string + groupFrom: string + groupTo: string + rangeDays: number +} + +const SKIPPED = Symbol('skipped') + +/** + * Runs an expensive Search Console rollup under a deadline. A timeout aborts + * the in-flight paging and reports the panel as skipped; a genuine upstream + * failure still propagates, so a broken panel reads as broken rather than as + * "too much data". + */ +const resolveOptional = async ( + load: (signal: AbortSignal) => Promise, +): Promise => { + const controller = new AbortController() + let timeout: ReturnType | undefined + + const loading = load(controller.signal) + + // Aborting rejects `loading` after the race has already settled on SKIPPED; + // this keeps that late rejection from surfacing as an unhandled rejection + // without stopping Promise.race from seeing a real, pre-deadline failure. + loading.catch(() => {}) + + try { + return await Promise.race([ + loading, + new Promise((resolve) => { + timeout = setTimeout(() => { + controller.abort() + resolve(SKIPPED) + }, OPTIONAL_ANALYTICS_TIMEOUT_MS) + }), + ]) + } finally { + if (timeout) { + clearTimeout(timeout) + } + } +} + +@Injectable() +export class SeoV2Service { + constructor( + private readonly gscService: GSCService, + private readonly analyticsService: AnalyticsService, + ) {} + + /** + * Search Console has to be connected and a property linked before any of the + * data endpoints can answer. Both are project configuration rather than a bad + * request, hence 409 rather than 400. + * + * Call this only after the request itself has been validated: a malformed + * filter is a 422 whether or not the project happens to be connected, and + * answering 409 first would send callers debugging their query down the + * wrong path. + */ + private async requireContext(pid: string): Promise { + const { connected, property } = + await this.gscService.getConnectionState(pid) + + if (!connected) { + throw new ConflictException( + 'Google Search Console is not connected for this project', + ) + } + + if (!property) { + throw new ConflictException( + 'No Google Search Console property is linked to this project', + ) + } + + return this.gscService.getGSCContext(pid) + } + + private prepareFilters(filters: string | undefined): { + appliedFilters: V2Filter[] + gscFilters: string | undefined + } { + const appliedFilters = validateSeoFilters(parseV2Filters(filters)) + + if (appliedFilters.length > MAX_FILTERS) { + throw new UnprocessableEntityException( + `Too many filters; at most ${MAX_FILTERS} may be applied at once`, + ) + } + + for (const filter of appliedFilters) { + if (String(filter.value).length > MAX_FILTER_EXPRESSION_LENGTH) { + throw new UnprocessableEntityException( + `The seo filter on '${filter.dimension}' has a value longer than ${MAX_FILTER_EXPRESSION_LENGTH} characters`, + ) + } + } + + // GSCService.parseFilters already understands the v2 filter shape, so the + // validated filters are handed straight back to it, mirroring how the + // ClickHouse data types translate v2 -> v1 JSON -> getFiltersQuery. + return { appliedFilters, gscFilters: toGscFiltersJson(appliedFilters) } + } + + private resolveTimeframe( + dto: V2BaseQueryDto, + requestedBucket?: TimeBucketType, + ): SeoTimeframe { + const { from, to, timezone } = dto + const safeTimezone = this.analyticsService.getSafeTimezone(timezone) + + if (Boolean(from) !== Boolean(to)) { + throw new BadRequestException( + "The 'from' and 'to' parameters must be provided together", + ) + } + + const hasCustomRange = Boolean(from && to) + const period = hasCustomRange + ? null + : dto.period && dto.period !== 'custom' + ? dto.period + : V2_DEFAULT_PERIOD + + const timeBucket = + requestedBucket ?? + (period && INTRADAY_PERIODS.includes(period) + ? TimeBucketType.HOUR + : TimeBucketType.DAY) + + // getGroupFromTo validates the bucket against the range, which neither an + // open-ended 'all' nor a caller-chosen custom range can satisfy. + const groupTimeBucket = + hasCustomRange || period === 'all' ? null : timeBucket + const diff = period === 'all' ? GSC_ALL_TIME_DAYS : undefined + + const { groupFrom, groupTo } = this.analyticsService.getGroupFromTo( + hasCustomRange ? from : undefined, + hasCustomRange ? to : undefined, + groupTimeBucket, + period ?? undefined, + safeTimezone, + diff, + ) + + const rangeDays = + Math.abs( + dayjs(groupTo) + .startOf('day') + .diff(dayjs(groupFrom).startOf('day'), 'day'), + ) + 1 + + return { period, timeBucket, safeTimezone, groupFrom, groupTo, rangeDays } + } + + private baseMeta( + pid: string, + timeframe: SeoTimeframe, + appliedFilters: V2Filter[], + ): Record { + return { + pid, + period: timeframe.period, + from: toIsoTimestamp(timeframe.groupFrom, timeframe.safeTimezone), + to: toIsoTimestamp(timeframe.groupTo, timeframe.safeTimezone), + timezone: timeframe.safeTimezone, + appliedFilters, + } + } + + async getStatus( + pid: string, + ): Promise> { + const state = await this.gscService.getConnectionState(pid) + + return envelope(state, { pid }) + } + + /** + * Mirrors AnalyticsV2Service.getDimensions so `?type=seo` answers in the same + * shape as every other data type on the discovery endpoint. + */ + getDimensions(): V2Envelope<{ + dimensions: Record[] + metrics: Record[] + }> { + const dimensions = listSeoDimensions().map((dimension) => ({ + name: dimension.api, + description: dimension.description, + filterOnly: false, + extraFields: [] as string[], + })) + + const metrics = listSeoMetrics().map((metric) => ({ + name: metric.api, + description: metric.description, + format: metric.format, + default: Boolean(metric.isDefault), + })) + + return envelope({ dimensions, metrics }, { type: 'seo' }) + } + + async getSummary( + pid: string, + dto: V2SeoSummaryDto, + ): Promise> { + const { appliedFilters, gscFilters } = this.prepareFilters(dto.filters) + const timeframe = this.resolveTimeframe(dto) + const ctx = await this.requireContext(pid) + const { groupFrom, groupTo } = timeframe + + const fromDate = dayjs(groupFrom) + const durationMs = dayjs(groupTo).diff(fromDate) + const previousTo = fromDate + .subtract(1, 'millisecond') + .format('YYYY-MM-DD HH:mm:ss') + const previousFrom = fromDate + .subtract(durationMs, 'millisecond') + .format('YYYY-MM-DD HH:mm:ss') + + const [current, previous] = await Promise.all([ + this.gscService.getSummary(pid, groupFrom, groupTo, gscFilters, ctx), + this.gscService + .getSummary(pid, previousFrom, previousTo, gscFilters, ctx) + .catch(() => null), + ]) + + return envelope(mapSeoSummary(current, previous), { + ...this.baseMeta(pid, timeframe, appliedFilters), + metrics: listSeoMetrics().map((metric) => metric.api), + }) + } + + async getTimeseries( + pid: string, + dto: V2SeoTimeseriesDto, + ): Promise> { + const { appliedFilters, gscFilters } = this.prepareFilters(dto.filters) + const metrics = parseSeoMetricsParam(dto.metrics).map( + (metric) => metric.api, + ) + const timeframe = this.resolveTimeframe( + dto, + dto.timeBucket as TimeBucketType | undefined, + ) + const ctx = await this.requireContext(pid) + + // Search Console only serves hourly and daily rows; wider buckets are + // rolled up from the daily series rather than requested upstream. + const series = (await this.gscService.getDateSeries( + pid, + timeframe.groupFrom, + timeframe.groupTo, + timeframe.timeBucket === TimeBucketType.HOUR ? 'hour' : 'day', + gscFilters, + ctx, + )) as SeoDateSeriesEntry[] + + const bucketed = bucketSeoDateSeries(series, timeframe.timeBucket) + + return envelope( + mapSeoTimeseries(bucketed, metrics, timeframe.safeTimezone), + { + ...this.baseMeta(pid, timeframe, appliedFilters), + metrics, + timeBucket: timeframe.timeBucket, + allowedTimeBuckets: [...V2_SEO_TIME_BUCKETS], + }, + ) + } + + async getBreakdown( + pid: string, + dto: V2SeoBreakdownDto, + ): Promise[]>> { + const dimension = getSeoBreakdownDimension(dto.dimension) + const { appliedFilters, gscFilters } = this.prepareFilters(dto.filters) + const metrics = parseSeoMetricsParam(dto.metrics).map( + (metric) => metric.api, + ) + const timeframe = this.resolveTimeframe(dto) + const limit = dto.limit ?? DEFAULT_SEO_BREAKDOWN_LIMIT + const offset = dto.offset ?? 0 + const ctx = await this.requireContext(pid) + const { groupFrom, groupTo } = timeframe + + const rows = await this.fetchBreakdownRows( + dimension.gsc, + pid, + groupFrom, + groupTo, + limit, + offset, + gscFilters, + ctx, + ) + + return envelope(mapSeoBreakdownRows(dimension.gsc, rows, metrics), { + ...this.baseMeta(pid, timeframe, appliedFilters), + dimension: dimension.api, + metrics, + limit, + offset, + sort: SEO_SORT, + }) + } + + private fetchBreakdownRows( + dimension: 'query' | 'page' | 'country' | 'device', + pid: string, + from: string, + to: string, + limit: number, + offset: number, + filters: string | undefined, + ctx: GSCContext, + ) { + if (dimension === 'query') { + return this.gscService.getKeywords( + pid, + from, + to, + limit, + offset, + filters, + undefined, + ctx, + ) + } + + if (dimension === 'page') { + return this.gscService.getTopPages( + pid, + from, + to, + limit, + offset, + filters, + undefined, + ctx, + ) + } + + if (dimension === 'country') { + return this.gscService.getTopCountries( + pid, + from, + to, + limit, + offset, + filters, + ctx, + ) + } + + return this.gscService.getTopDevices( + pid, + from, + to, + limit, + offset, + filters, + ctx, + ) + } + + async getBrandedTraffic( + pid: string, + dto: V2SeoRangeDto, + ): Promise> { + const { appliedFilters, gscFilters } = this.prepareFilters(dto.filters) + const timeframe = this.resolveTimeframe(dto) + const ctx = await this.requireContext(pid) + const meta = this.baseMeta(pid, timeframe, appliedFilters) + + if (timeframe.rangeDays > MAX_BRANDED_TRAFFIC_RANGE_DAYS) { + return envelope(null, { + ...meta, + skipped: true, + skippedReason: 'range_too_large', + maxRangeDays: MAX_BRANDED_TRAFFIC_RANGE_DAYS, + }) + } + + const result = await resolveOptional((signal) => + this.gscService.getBrandedTraffic( + pid, + timeframe.groupFrom, + timeframe.groupTo, + gscFilters, + ctx, + signal, + ), + ) + + if (result === SKIPPED) { + return envelope(null, { + ...meta, + skipped: true, + skippedReason: 'timeout', + timeoutMs: OPTIONAL_ANALYTICS_TIMEOUT_MS, + }) + } + + return envelope(result, { ...meta, skipped: false }) + } + + async getPositions( + pid: string, + dto: V2SeoRangeDto, + ): Promise> { + const { appliedFilters, gscFilters } = this.prepareFilters(dto.filters) + const timeframe = this.resolveTimeframe(dto) + const ctx = await this.requireContext(pid) + const meta = this.baseMeta(pid, timeframe, appliedFilters) + + if (timeframe.rangeDays > MAX_POSITION_ANALYTICS_RANGE_DAYS) { + return envelope(null, { + ...meta, + skipped: true, + skippedReason: 'range_too_large', + maxRangeDays: MAX_POSITION_ANALYTICS_RANGE_DAYS, + }) + } + + const result = await resolveOptional((signal) => + this.gscService.getPositionAnalytics( + pid, + timeframe.groupFrom, + timeframe.groupTo, + gscFilters, + ctx, + signal, + ), + ) + + if (result === SKIPPED) { + return envelope(null, { + ...meta, + skipped: true, + skippedReason: 'timeout', + timeoutMs: OPTIONAL_ANALYTICS_TIMEOUT_MS, + }) + } + + return envelope(result, { ...meta, skipped: false }) + } +} diff --git a/backend/apps/community/src/common/constants.ts b/backend/apps/community/src/common/constants.ts index 4c779973e..b2d7632bf 100644 --- a/backend/apps/community/src/common/constants.ts +++ b/backend/apps/community/src/common/constants.ts @@ -133,6 +133,39 @@ const ERROR_COLUMNS = [ const ALL_COLUMNS = [...TRAFFIC_COLUMNS, 'ev', 'entryPage', 'exitPage'] +const V2_VIEW_FILTER_DIMENSIONS = [ + 'country', + 'region', + 'city', + 'page', + 'host', + 'locale', + 'browser', + 'browser_version', + 'os', + 'os_version', + 'device', + 'referrer', + 'referrer_name', + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_term', + 'utm_content', + 'isp', + 'organization', + 'user_type', + 'connection_type', + 'entry_page', + 'exit_page', + 'event', + 'error_name', + 'error_message', + 'error_filename', + 'event_metadata', + 'page_property', +] + const PERFORMANCE_COLUMNS = [ 'cc', 'rg', @@ -170,6 +203,7 @@ export { ERROR_COLUMNS, PID_REGEX, ALL_COLUMNS, + V2_VIEW_FILTER_DIMENSIONS, SELFHOSTED_GEOIP_DB_PATH, TWO_FACTOR_AUTHENTICATION_APP_NAME, OIDC_ENABLED, diff --git a/backend/apps/community/src/experiment/experiment.controller.ts b/backend/apps/community/src/experiment/experiment.controller.ts index 91725a85a..c190b835e 100644 --- a/backend/apps/community/src/experiment/experiment.controller.ts +++ b/backend/apps/community/src/experiment/experiment.controller.ts @@ -36,6 +36,7 @@ import { DataType, getLowestPossibleTimeBucket, } from '../analytics/analytics.service' +import { normalizeFiltersToV1Json } from '../analytics/v2/query/filters.translator' import { TimeBucketType } from '../analytics/dto/getData.dto' import { Auth } from '../auth/decorators' import { CurrentUserId } from '../auth/decorators/current-user-id.decorator' @@ -674,7 +675,10 @@ export class ExperimentController { } const [filtersQuery, filtersParams, appliedFilters] = - this.analyticsService.getFiltersQuery(filters || '[]', DataType.ANALYTICS) + this.analyticsService.getFiltersQuery( + normalizeFiltersToV1Json(filters, 'traffic'), + DataType.ANALYTICS, + ) const isSegmented = Array.isArray(appliedFilters) && appliedFilters.length > 0 diff --git a/backend/apps/community/src/goal/goal.controller.ts b/backend/apps/community/src/goal/goal.controller.ts index 0be7b6976..69d9fced6 100644 --- a/backend/apps/community/src/goal/goal.controller.ts +++ b/backend/apps/community/src/goal/goal.controller.ts @@ -27,6 +27,7 @@ import { DataType, getLowestPossibleTimeBucket, } from '../analytics/analytics.service' +import { normalizeFiltersToV1Json } from '../analytics/v2/query/filters.translator' import { Auth } from '../auth/decorators' import { CurrentUserId } from '../auth/decorators/current-user-id.decorator' import { @@ -71,10 +72,20 @@ const clampPagination = (take?: number, skip?: number) => { const GOAL_CONDITION_COLUMNS: Record = { page: 'pg', - pg: 'pg', event: 'event_name', - event_name: 'event_name', host: 'host', + country: 'cc', + device: 'dv', + browser: 'br', + os: 'os', + referrer: 'ref', + utm_source: 'so', + utm_medium: 'me', + utm_campaign: 'ca', + utm_term: 'te', + utm_content: 'co', + pg: 'pg', + event_name: 'event_name', ref: 'ref', so: 'so', me: 'me', @@ -84,7 +95,6 @@ const GOAL_CONDITION_COLUMNS: Record = { cc: 'cc', dv: 'dv', br: 'br', - os: 'os', } type GoalBreakdownKey = @@ -793,7 +803,7 @@ export class GoalController { ) const [filtersQuery, filtersParams] = this.analyticsService.getFiltersQuery( - filters || '[]', + normalizeFiltersToV1Json(filters, 'traffic'), DataType.ANALYTICS, ) const { query: conversionsSubquery, params: goalParams } = @@ -1106,7 +1116,7 @@ export class GoalController { const [selector, groupBy] = this.getGroupSubquery(resolvedTimeBucket) const [filtersQuery, filtersParams] = this.analyticsService.getFiltersQuery( - filters || '[]', + normalizeFiltersToV1Json(filters, 'traffic'), DataType.ANALYTICS, ) const { query: conversionsSubquery, params: goalParams } = diff --git a/backend/apps/community/src/project/dto/create-project-view.dto.ts b/backend/apps/community/src/project/dto/create-project-view.dto.ts index 8d89ebbbd..0c773d5b6 100644 --- a/backend/apps/community/src/project/dto/create-project-view.dto.ts +++ b/backend/apps/community/src/project/dto/create-project-view.dto.ts @@ -1,5 +1,6 @@ import { ArrayMaxSize, + IsArray, IsEnum, IsNotEmpty, IsOptional, @@ -12,7 +13,6 @@ import { import { ApiProperty } from '@nestjs/swagger' import { ProjectViewType } from '../entity/project-view.entity' import { ProjectViewCustomEventMetaValueType } from '../entity/project-view-custom-event.entity' -import { TRAFFIC_COLUMNS } from '../../common/constants' export class ProjectViewCustomEventDto { @ApiProperty() @@ -48,14 +48,16 @@ export class ProjectViewCustomEventDto { } export interface Filter { - column: keyof typeof TRAFFIC_COLUMNS - filter: string - isExclusive: boolean - isContains?: boolean + dimension: string + operator: 'is' | 'is_not' | 'contains' | 'contains_not' + value: string | null | (string | null)[] + key?: string } export const MAX_METRICS_IN_VIEW = 3 +const MAX_FILTERS_IN_VIEW = 100 + export class CreateProjectViewDto { @ApiProperty() @MaxLength(20) @@ -71,11 +73,13 @@ export class CreateProjectViewDto { @ApiProperty({ description: - 'An array of properties to filter [{ column, filter, isExclusive, isContains }]', + 'An array of properties to filter [{ dimension, operator, value, key? }]', required: false, isArray: true, }) @IsOptional() + @IsArray() + @ArrayMaxSize(MAX_FILTERS_IN_VIEW) filters?: Filter[] @ApiProperty({ diff --git a/backend/apps/community/src/project/gsc.service.ts b/backend/apps/community/src/project/gsc.service.ts index ebd548d21..dc4350e23 100644 --- a/backend/apps/community/src/project/gsc.service.ts +++ b/backend/apps/community/src/project/gsc.service.ts @@ -39,11 +39,17 @@ const ENCRYPTION_KEY = deriveKey('gsc-token') const MAX_ROW_LIMIT = 25000 const MAX_BRANDED_PAGES = 10 -const MAX_FILTER_EXPRESSION_LENGTH = 500 -const MAX_FILTERS = 20 -const MAX_BRANDED_TRAFFIC_RANGE_DAYS = 31 -const MAX_POSITION_ANALYTICS_RANGE_DAYS = 31 -const OPTIONAL_ANALYTICS_TIMEOUT_MS = 12000 +export const MAX_FILTER_EXPRESSION_LENGTH = 500 +export const MAX_FILTERS = 20 +export const MAX_BRANDED_TRAFFIC_RANGE_DAYS = 31 +export const MAX_POSITION_ANALYTICS_RANGE_DAYS = 31 +export const OPTIONAL_ANALYTICS_TIMEOUT_MS = 12000 + +/** + * Search Console keeps roughly 16 months of history, so an 'all' period is + * resolved against a fixed window rather than the project's first event. + */ +export const GSC_ALL_TIME_DAYS = 480 const IMPRESSION_POSITION_BUCKETS = [ { key: 'pos1To3', label: '1-3' }, @@ -67,13 +73,8 @@ type OrganicPositionSeriesEntry = { date: string } & Record< OrganicPositionBucketKey, number > -type BrandedTrafficAnalytics = { branded: number; nonBranded: number } -type SkippedBrandedAnalytics = { - skipped: true - branded: null - nonBranded: null -} -type PositionAnalytics = { +export type BrandedTrafficAnalytics = { branded: number; nonBranded: number } +export type PositionAnalytics = { impressionsByPosition: { key: ImpressionPositionBucketKey label: string @@ -82,23 +83,6 @@ type PositionAnalytics = { }[] organicPositions: OrganicPositionSeriesEntry[] } -type SkippedPositionAnalytics = { - skipped: true - impressionsByPosition: null - organicPositions: null -} - -const SKIPPED_BRANDED_ANALYTICS: SkippedBrandedAnalytics = { - skipped: true, - branded: null, - nonBranded: null, -} - -const SKIPPED_POSITION_ANALYTICS: SkippedPositionAnalytics = { - skipped: true, - impressionsByPosition: null, - organicPositions: null, -} const initialImpressionPositionBuckets = (): Record< ImpressionPositionBucketKey, @@ -140,32 +124,6 @@ const getOrganicPositionBucketKey = ( return 'pos51Plus' } -const resolveOptionalAnalytics = async ( - load: (signal: AbortSignal) => Promise, - fallback: T, -): Promise => { - const controller = new AbortController() - let timeout: ReturnType | undefined - - try { - return await Promise.race([ - load(controller.signal), - new Promise((resolve) => { - timeout = setTimeout(() => { - controller.abort() - resolve(fallback) - }, OPTIONAL_ANALYTICS_TIMEOUT_MS) - }), - ]) - } catch { - return fallback - } finally { - if (timeout) { - clearTimeout(timeout) - } - } -} - type GSCRow = { keys: string[] clicks?: number @@ -174,7 +132,7 @@ type GSCRow = { position?: number } -type GSCContext = { +export type GSCContext = { sc: ReturnType siteUrl: string } @@ -428,6 +386,28 @@ export class GSCService { return !_isEmpty(tokens?.refresh_token || tokens?.access_token) } + /** + * Whether the project can currently be queried: Search Console has to be + * configured on this instance, an account connected and a property linked. + * Unlike getStatus this needs no account email, so it is safe to expose to + * anyone with project read access. + */ + async getConnectionState( + pid: string, + ): Promise<{ connected: boolean; property: string | null }> { + if (!this.isAvailable()) { + return { connected: false, property: null } + } + + const tokens = await this.getStoredTokens(pid) + const connected = !_isEmpty(tokens?.refresh_token || tokens?.access_token) + + return { + connected, + property: connected ? tokens.property_uri || null : null, + } + } + async getStatus(pid: string): Promise<{ connected: boolean email: string | null @@ -482,7 +462,7 @@ export class GSCService { return oauth2Client } - private async getGSCContext(pid: string): Promise { + async getGSCContext(pid: string): Promise { const tokens = await this.getStoredTokens(pid) if (_isEmpty(tokens) || _isEmpty(tokens.property_uri)) { throw new BadRequestException( @@ -587,31 +567,33 @@ export class GSCService { throw new BadRequestException('Invalid filters parameter') } - const { - column, - filter: value, - isExclusive, - isContains, - } = filter as { + const raw = filter as { + dimension?: unknown + operator?: unknown + value?: unknown column?: unknown filter?: unknown isExclusive?: unknown isContains?: unknown } - if (typeof column !== 'string' || typeof value !== 'string') { - throw new BadRequestException('Invalid filters parameter') + const isV2 = typeof raw.dimension === 'string' + const filterDimension = isV2 ? raw.dimension : raw.column + const value = isV2 ? raw.value : raw.filter + + if (typeof filterDimension !== 'string' || typeof value !== 'string') { + continue } if (value.length > MAX_FILTER_EXPRESSION_LENGTH) continue let dimension: string | undefined let expression = value - if (column === 'pg') { + if (filterDimension === 'page' || filterDimension === 'pg') { dimension = 'page' - } else if (column === 'keywords') { + } else if (filterDimension === 'keywords') { dimension = 'query' - } else if (column === 'country' || column === 'cc') { + } else if (filterDimension === 'country' || filterDimension === 'cc') { dimension = 'country' if (expression.length === 2) { const alpha3 = countries.alpha2ToAlpha3(expression.toUpperCase()) @@ -620,7 +602,7 @@ export class GSCService { } else { expression = expression.toLowerCase() } - } else if (column === 'device' || column === 'dv') { + } else if (filterDimension === 'device' || filterDimension === 'dv') { dimension = 'device' expression = expression.toUpperCase() } else { @@ -628,9 +610,17 @@ export class GSCService { } let operator = 'equals' - if (isExclusive) { - operator = isContains ? 'notContains' : 'notEquals' - } else if (isContains) { + if (isV2) { + if (raw.operator === 'is_not') { + operator = 'notEquals' + } else if (raw.operator === 'contains') { + operator = 'contains' + } else if (raw.operator === 'contains_not') { + operator = 'notContains' + } + } else if (raw.isExclusive) { + operator = raw.isContains ? 'notContains' : 'notEquals' + } else if (raw.isContains) { operator = 'contains' } @@ -1235,101 +1225,4 @@ export class GSCService { ) } } - - async getDashboard( - pid: string, - from: string, - to: string, - timeBucket?: string, - filtersStr?: string, - ) { - if (!this.isAvailable()) { - return { notConnected: true } - } - - const connected = await this.isConnected(pid) - if (!connected) { - return { notConnected: true } - } - - const tokens = await this.getStoredTokens(pid) - if (_isEmpty(tokens.property_uri)) { - return { notConnected: true, noProperty: true } - } - - let ctx: GSCContext - try { - ctx = await this.getGSCContext(pid) - } catch { - return { notConnected: true } - } - - const fromDate = dayjs(from) - const toDate = dayjs(to) - const durationMs = toDate.diff(fromDate) - const rangeDays = - Math.abs(toDate.startOf('day').diff(fromDate.startOf('day'), 'day')) + 1 - const shouldLoadBrandedTraffic = rangeDays <= MAX_BRANDED_TRAFFIC_RANGE_DAYS - const shouldLoadPositionAnalytics = - rangeDays <= MAX_POSITION_ANALYTICS_RANGE_DAYS - const prevTo = fromDate - .subtract(1, 'millisecond') - .format('YYYY-MM-DD HH:mm:ss') - const prevFrom = fromDate - .subtract(durationMs, 'millisecond') - .format('YYYY-MM-DD HH:mm:ss') - - const [ - summary, - previousSummary, - dateSeries, - topPages, - topQueries, - topCountries, - topDevices, - brandedTraffic, - positionAnalytics, - ] = await Promise.all([ - this.getSummary(pid, from, to, filtersStr, ctx), - this.getSummary(pid, prevFrom, prevTo, filtersStr, ctx).catch(() => null), - this.getDateSeries(pid, from, to, timeBucket, filtersStr, ctx), - this.getTopPages(pid, from, to, 50, 0, filtersStr, undefined, ctx), - this.getKeywords(pid, from, to, 50, 0, filtersStr, undefined, ctx), - this.getTopCountries(pid, from, to, 50, 0, filtersStr, ctx), - this.getTopDevices(pid, from, to, 50, 0, filtersStr, ctx), - shouldLoadBrandedTraffic - ? resolveOptionalAnalytics< - BrandedTrafficAnalytics | SkippedBrandedAnalytics - >( - (signal) => - this.getBrandedTraffic(pid, from, to, filtersStr, ctx, signal), - SKIPPED_BRANDED_ANALYTICS, - ) - : Promise.resolve(SKIPPED_BRANDED_ANALYTICS), - shouldLoadPositionAnalytics - ? resolveOptionalAnalytics< - PositionAnalytics | SkippedPositionAnalytics - >( - (signal) => - this.getPositionAnalytics(pid, from, to, filtersStr, ctx, signal), - SKIPPED_POSITION_ANALYTICS, - ) - : Promise.resolve(SKIPPED_POSITION_ANALYTICS), - ]) - - return { - notConnected: false, - summary, - previousSummary, - dateSeries, - topPages, - topQueries, - topCountries, - topDevices, - brandedTraffic, - positionAnalyticsSkipped: 'skipped' in positionAnalytics, - impressionsByPosition: positionAnalytics.impressionsByPosition, - organicPositions: positionAnalytics.organicPositions, - } - } } diff --git a/backend/apps/community/src/project/project.service.ts b/backend/apps/community/src/project/project.service.ts index 35994c47d..28ba99865 100644 --- a/backend/apps/community/src/project/project.service.ts +++ b/backend/apps/community/src/project/project.service.ts @@ -32,8 +32,7 @@ import { ORIGINS_REGEX, getRedisProjectKey, redisProjectCacheTimeout, - ALL_COLUMNS, - TRAFFIC_METAKEY_COLUMNS, + V2_VIEW_FILTER_DIMENSIONS, } from '../common/constants' import { clickhouse } from '../common/integrations/clickhouse' import { @@ -454,11 +453,8 @@ export class ProjectService { return [] } - return _filter( - filters, - ({ column }) => - _includes(ALL_COLUMNS, column as string) || - _includes(TRAFFIC_METAKEY_COLUMNS, column as string), + return _filter(filters, ({ dimension }) => + _includes(V2_VIEW_FILTER_DIMENSIONS, dimension), ) } diff --git a/backend/apps/community/tsconfig.app.json b/backend/apps/community/tsconfig.app.json index 3cef421cd..951e710e2 100644 --- a/backend/apps/community/tsconfig.app.json +++ b/backend/apps/community/tsconfig.app.json @@ -7,7 +7,7 @@ "esModuleInterop": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "target": "es2017", + "target": "es2022", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", diff --git a/backend/jest.v2.config.js b/backend/jest.v2.config.js new file mode 100644 index 000000000..463fb95bb --- /dev/null +++ b/backend/jest.v2.config.js @@ -0,0 +1,21 @@ +/** + * Scoped jest setup for the v2 analytics API. Only covers the pure modules + * under apps/{cloud,community}/src/analytics/v2 (registry, filter translator, + * query builder, mappers) — it is not a general backend test runner. The v2 + * source is duplicated per app (repo convention); running the specs against + * both copies keeps them from drifting. + */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: [ + '/apps/cloud/src/analytics/v2', + '/apps/community/src/analytics/v2', + ], + testMatch: ['**/*.spec.ts'], + moduleNameMapper: { + // common/constants instantiates a Redis client at import time; the v2 + // pure modules transitively import it via analytics.service.ts + '^ioredis$': '/apps/cloud/src/analytics/v2/__tests__/ioredis.stub.ts', + }, +} diff --git a/backend/package-lock.json b/backend/package-lock.json index 5c23271c7..5948e3bef 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -84,6 +84,7 @@ "@types/cookie-parser": "^1.4.10", "@types/express": "^5.0.6", "@types/ioredis": "^4.28.10", + "@types/jest": "^30.0.0", "@types/lodash": "^4.17.24", "@types/multer": "^2.1.0", "@types/node": "^26.0.0", @@ -91,10 +92,12 @@ "@types/passport-jwt": "^4.0.1", "@types/web-push": "^3.6.4", "cross-env": "^10.1.0", + "jest": "^30.4.2", "knip": "^6.17.1", "oxfmt": "^0.55.0", "oxlint": "^1.70.0", "rimraf": "^6.1.3", + "ts-jest": "^29.4.11", "ts-prune": "^0.10.3", "typescript": "^5.9.3" }, @@ -403,13 +406,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "devOptional": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -417,604 +420,1190 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "devOptional": true, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "@babel/types": "^7.28.5" - }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" + "semver": "bin/semver.js" } }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@borewit/text-codec": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", - "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@clickhouse/client": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.21.0.tgz", - "integrity": "sha512-HJLHX60clPFV4ZYjhd9Radgfqkl0MX6T4GDaS6CK4K2cpMWW4uIDPzXjueIVJh+LRVt1pi7kJKn8BWjmbGAlEA==", - "license": "Apache-2.0", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", "dependencies": { - "@clickhouse/client-common": "1.21.0" - }, - "engines": { - "node": ">=16" + "yallist": "^3.0.2" } }, - "node_modules/@clickhouse/client-common": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@clickhouse/client-common/-/client-common-1.21.0.tgz", - "integrity": "sha512-UjvA32aaLKbQ5U735qAGdzohrOrfU8zFoOWIOWorWQk+7UzHZxq6pf4Uh/wQ08zAMb9cmkd9h9N1FV8zP8UCOg==", - "license": "Apache-2.0" + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } }, - "node_modules/@colordx/core": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@colordx/core/-/core-5.4.3.tgz", - "integrity": "sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ==", + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, "license": "MIT", - "optional": true + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": ">=0.1.90" + "node": ">=6.9.0" } }, - "node_modules/@css-inline/css-inline": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline/-/css-inline-0.20.0.tgz", - "integrity": "sha512-WABsvMSBs/DDadwhAUDw6uXwUE6w4DC/bnC4E2Y2rqbCTw5E1PPfj6VksnSCALkv9ynZfYGYYO4+Rvhmn5kgpw==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, "engines": { - "node": ">= 10" + "node": ">=6.9.0" }, - "optionalDependencies": { - "@css-inline/css-inline-android-arm-eabi": "0.20.0", - "@css-inline/css-inline-android-arm64": "0.20.0", - "@css-inline/css-inline-darwin-arm64": "0.20.0", - "@css-inline/css-inline-darwin-x64": "0.20.0", - "@css-inline/css-inline-linux-arm-gnueabihf": "0.20.0", - "@css-inline/css-inline-linux-arm64-gnu": "0.20.0", - "@css-inline/css-inline-linux-arm64-musl": "0.20.0", - "@css-inline/css-inline-linux-x64-gnu": "0.20.0", - "@css-inline/css-inline-linux-x64-musl": "0.20.0", - "@css-inline/css-inline-win32-arm64-msvc": "0.20.0", - "@css-inline/css-inline-win32-x64-msvc": "0.20.0" + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@css-inline/css-inline-android-arm-eabi": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-android-arm-eabi/-/css-inline-android-arm-eabi-0.20.0.tgz", - "integrity": "sha512-w1+cicyd2xGzuOcFYxL9Yp3E/KT6opGpdhaKo1vFAepyn8zHrpAR3c1DHH1RWSWK3ZVcD3ne1naqJXQa4k/9uw==", - "cpu": [ - "arm" - ], + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">= 10" + "node": ">=6.9.0" } }, - "node_modules/@css-inline/css-inline-android-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-android-arm64/-/css-inline-android-arm64-0.20.0.tgz", - "integrity": "sha512-V6ELV+DEjhYPqRcyCezUcMDSrXINMSCL4RBseEqWG18WhOZ9GoGd5XFpZzciQhHq/2MQS+06Jcc6RX59k7QS5g==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">= 10" + "node": ">=6.9.0" } }, - "node_modules/@css-inline/css-inline-darwin-arm64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-darwin-arm64/-/css-inline-darwin-arm64-0.20.0.tgz", - "integrity": "sha512-hT8Mtwp4PJ9mMYFDG/xg7KCa7nPqseJdXaajxXVjXB8+6oemxgx+7TUO7HSfmeY4Ox7X2vaNfaIfti4ySgNvyg==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">= 10" + "node": ">=6.9.0" } }, - "node_modules/@css-inline/css-inline-darwin-x64": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-darwin-x64/-/css-inline-darwin-x64-0.20.0.tgz", - "integrity": "sha512-caFvVySabHZG4N1QUysbhWp2+VstMlYLysZYfAX0I6c9QfWcr+OZThoGOO2gC9lYy6QMCTBSD+9t+iceH9oBQw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">= 10" + "node": ">=6.9.0" } }, - "node_modules/@css-inline/css-inline-linux-arm-gnueabihf": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm-gnueabihf/-/css-inline-linux-arm-gnueabihf-0.20.0.tgz", - "integrity": "sha512-9/TdB6cHgJ285uL7Y+1XjdJGjIQOeF9/YNq47N+azf2SiYdA3ahGYBmZhCGtYG7aBTJly9iJCxjP9ZZN8+Lajw==", - "cpu": [ - "arm" - ], + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, "engines": { - "node": ">= 10" + "node": ">=6.9.0" } }, - "node_modules/@css-inline/css-inline-linux-arm64-gnu": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm64-gnu/-/css-inline-linux-arm64-gnu-0.20.0.tgz", - "integrity": "sha512-umcG26teSSUxWGQm7sECI8KweUgBgq9Cqd6yNMTNzqC1K56ZhW3Iwi7QewVNGOpVPXoGsywIIwIuY9lyed1Jdw==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, "engines": { - "node": ">= 10" + "node": ">=6.0.0" } }, - "node_modules/@css-inline/css-inline-linux-arm64-musl": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm64-musl/-/css-inline-linux-arm64-musl-0.20.0.tgz", - "integrity": "sha512-wHQykZw3pX2R5Yp5VChOWzVXXO3XPdgWkQH1B9QBmft926EOkZpwxvubeAYy9I89qJzzdGNqiRl26+AUP88J3A==", - "cpu": [ - "arm64" - ], + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@css-inline/css-inline-linux-x64-gnu": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-x64-gnu/-/css-inline-linux-x64-gnu-0.20.0.tgz", - "integrity": "sha512-lHQZ+31CN3XJTn5MMFv1NYjyvuhAWEvul0fAYyppw4haA1TM+9UuA9jXdjqWXp26ItmainUnDMBlNLYQwMYt7g==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@css-inline/css-inline-linux-x64-musl": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-x64-musl/-/css-inline-linux-x64-musl-0.20.0.tgz", - "integrity": "sha512-DO/cba/zndTY3s86nNNfeHx7DvrvLb+IxhkQWTr29IQeiUgbawCH9X3B/4Li2eo/sEx/imOOnYDdXYu8PeX2Fg==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@css-inline/css-inline-win32-arm64-msvc": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-win32-arm64-msvc/-/css-inline-win32-arm64-msvc-0.20.0.tgz", - "integrity": "sha512-1o8xla5ljVHS7SguH6nTV6uoAIMRZv+2MrcxfLRObpkUtCO7oKXDeWnia7XlmnLhDX8Ncx5bDOenZb1N35kCiA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@css-inline/css-inline-win32-x64-msvc": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@css-inline/css-inline-win32-x64-msvc/-/css-inline-win32-x64-msvc-0.20.0.tgz", - "integrity": "sha512-pdO/QXLRwuq/RxO5PSFMhmWGJSQOvkiDyV4RrewEkE7SOCY8HAYNhx1T0y0pj2WzxuVdbNQbpwiCHAdQNplUJw==", - "cpu": [ - "x64" - ], + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, "engines": { - "node": ">= 10" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@epic-web/invariant": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", - "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", "dev": true, - "license": "MIT" - }, - "node_modules/@google-analytics/data": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@google-analytics/data/-/data-6.1.0.tgz", - "integrity": "sha512-72sICGmThvOuy1yVlaVRvWgUZ+4VvAlmkIlDgzo1SSSRl2MvbNJavnMTG7uGhNnlgwuHNwReKrm6R/7gAKo4hw==", - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "google-gax": "^5.0.0" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { - "node": ">=18" - } - }, - "node_modules/@googleapis/searchconsole": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@googleapis/searchconsole/-/searchconsole-6.0.1.tgz", - "integrity": "sha512-aSBv0yp9HV9BgqX3bUj2DRY2kaIwcoN6ift+fKwPXj8eMoccJZzceQNshxt44V2ECkqwn7gvVEXVYTkw6zaTeQ==", - "license": "Apache-2.0", - "dependencies": { - "googleapis-common": "^8.0.0" + "node": ">=6.9.0" }, - "engines": { - "node": ">=12.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", - "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", - "license": "Apache-2.0", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" + "@babel/helper-plugin-utils": "^7.10.4" }, - "engines": { - "node": ">=12.10.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@grpc/proto-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", - "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", - "license": "Apache-2.0", + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.3", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=6" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@inquirer/ansi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", - "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@inquirer/checkbox": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", - "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "node_modules/@inquirer/confirm": { - "version": "5.1.21", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", - "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "node_modules/@inquirer/core": { - "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", - "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "node_modules/@inquirer/editor": { - "version": "4.2.23", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", - "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/external-editor": "^1.0.3", - "@inquirer/type": "^3.0.10" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=18" + "node": ">=6.9.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "node_modules/@inquirer/expand": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", - "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=18" + "node": ">=6.9.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", - "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", "dev": true, "license": "MIT", "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { - "node": ">=18" + "node": ">=6.9.0" }, "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "node_modules/@inquirer/figures": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", - "dev": true, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", + "optional": true, "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/@inquirer/input": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", - "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": ">=6.9.0" } }, - "node_modules/@inquirer/number": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", - "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" }, "engines": { - "node": ">=18" - }, + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@clickhouse/client": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.21.0.tgz", + "integrity": "sha512-HJLHX60clPFV4ZYjhd9Radgfqkl0MX6T4GDaS6CK4K2cpMWW4uIDPzXjueIVJh+LRVt1pi7kJKn8BWjmbGAlEA==", + "license": "Apache-2.0", + "dependencies": { + "@clickhouse/client-common": "1.21.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@clickhouse/client-common": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/@clickhouse/client-common/-/client-common-1.21.0.tgz", + "integrity": "sha512-UjvA32aaLKbQ5U735qAGdzohrOrfU8zFoOWIOWorWQk+7UzHZxq6pf4Uh/wQ08zAMb9cmkd9h9N1FV8zP8UCOg==", + "license": "Apache-2.0" + }, + "node_modules/@colordx/core": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@colordx/core/-/core-5.4.3.tgz", + "integrity": "sha512-kIxYSfA5T8HXjav55UaaH/o/cKivF6jCCGIb8eqtcsfI46wsvlSiT8jMDyrl779qLec3c2c2oHBZo4oAhvbjrQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@css-inline/css-inline": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline/-/css-inline-0.20.0.tgz", + "integrity": "sha512-WABsvMSBs/DDadwhAUDw6uXwUE6w4DC/bnC4E2Y2rqbCTw5E1PPfj6VksnSCALkv9ynZfYGYYO4+Rvhmn5kgpw==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@css-inline/css-inline-android-arm-eabi": "0.20.0", + "@css-inline/css-inline-android-arm64": "0.20.0", + "@css-inline/css-inline-darwin-arm64": "0.20.0", + "@css-inline/css-inline-darwin-x64": "0.20.0", + "@css-inline/css-inline-linux-arm-gnueabihf": "0.20.0", + "@css-inline/css-inline-linux-arm64-gnu": "0.20.0", + "@css-inline/css-inline-linux-arm64-musl": "0.20.0", + "@css-inline/css-inline-linux-x64-gnu": "0.20.0", + "@css-inline/css-inline-linux-x64-musl": "0.20.0", + "@css-inline/css-inline-win32-arm64-msvc": "0.20.0", + "@css-inline/css-inline-win32-x64-msvc": "0.20.0" + } + }, + "node_modules/@css-inline/css-inline-android-arm-eabi": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-android-arm-eabi/-/css-inline-android-arm-eabi-0.20.0.tgz", + "integrity": "sha512-w1+cicyd2xGzuOcFYxL9Yp3E/KT6opGpdhaKo1vFAepyn8zHrpAR3c1DHH1RWSWK3ZVcD3ne1naqJXQa4k/9uw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-android-arm64": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-android-arm64/-/css-inline-android-arm64-0.20.0.tgz", + "integrity": "sha512-V6ELV+DEjhYPqRcyCezUcMDSrXINMSCL4RBseEqWG18WhOZ9GoGd5XFpZzciQhHq/2MQS+06Jcc6RX59k7QS5g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-darwin-arm64": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-darwin-arm64/-/css-inline-darwin-arm64-0.20.0.tgz", + "integrity": "sha512-hT8Mtwp4PJ9mMYFDG/xg7KCa7nPqseJdXaajxXVjXB8+6oemxgx+7TUO7HSfmeY4Ox7X2vaNfaIfti4ySgNvyg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-darwin-x64": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-darwin-x64/-/css-inline-darwin-x64-0.20.0.tgz", + "integrity": "sha512-caFvVySabHZG4N1QUysbhWp2+VstMlYLysZYfAX0I6c9QfWcr+OZThoGOO2gC9lYy6QMCTBSD+9t+iceH9oBQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-linux-arm-gnueabihf": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm-gnueabihf/-/css-inline-linux-arm-gnueabihf-0.20.0.tgz", + "integrity": "sha512-9/TdB6cHgJ285uL7Y+1XjdJGjIQOeF9/YNq47N+azf2SiYdA3ahGYBmZhCGtYG7aBTJly9iJCxjP9ZZN8+Lajw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-linux-arm64-gnu": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm64-gnu/-/css-inline-linux-arm64-gnu-0.20.0.tgz", + "integrity": "sha512-umcG26teSSUxWGQm7sECI8KweUgBgq9Cqd6yNMTNzqC1K56ZhW3Iwi7QewVNGOpVPXoGsywIIwIuY9lyed1Jdw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-linux-arm64-musl": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-arm64-musl/-/css-inline-linux-arm64-musl-0.20.0.tgz", + "integrity": "sha512-wHQykZw3pX2R5Yp5VChOWzVXXO3XPdgWkQH1B9QBmft926EOkZpwxvubeAYy9I89qJzzdGNqiRl26+AUP88J3A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-linux-x64-gnu": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-x64-gnu/-/css-inline-linux-x64-gnu-0.20.0.tgz", + "integrity": "sha512-lHQZ+31CN3XJTn5MMFv1NYjyvuhAWEvul0fAYyppw4haA1TM+9UuA9jXdjqWXp26ItmainUnDMBlNLYQwMYt7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-linux-x64-musl": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-linux-x64-musl/-/css-inline-linux-x64-musl-0.20.0.tgz", + "integrity": "sha512-DO/cba/zndTY3s86nNNfeHx7DvrvLb+IxhkQWTr29IQeiUgbawCH9X3B/4Li2eo/sEx/imOOnYDdXYu8PeX2Fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-win32-arm64-msvc": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-win32-arm64-msvc/-/css-inline-win32-arm64-msvc-0.20.0.tgz", + "integrity": "sha512-1o8xla5ljVHS7SguH6nTV6uoAIMRZv+2MrcxfLRObpkUtCO7oKXDeWnia7XlmnLhDX8Ncx5bDOenZb1N35kCiA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@css-inline/css-inline-win32-x64-msvc": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@css-inline/css-inline-win32-x64-msvc/-/css-inline-win32-x64-msvc-0.20.0.tgz", + "integrity": "sha512-pdO/QXLRwuq/RxO5PSFMhmWGJSQOvkiDyV4RrewEkE7SOCY8HAYNhx1T0y0pj2WzxuVdbNQbpwiCHAdQNplUJw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@google-analytics/data": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@google-analytics/data/-/data-6.1.0.tgz", + "integrity": "sha512-72sICGmThvOuy1yVlaVRvWgUZ+4VvAlmkIlDgzo1SSSRl2MvbNJavnMTG7uGhNnlgwuHNwReKrm6R/7gAKo4hw==", + "license": "Apache-2.0", + "dependencies": { + "google-gax": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@googleapis/searchconsole": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@googleapis/searchconsole/-/searchconsole-6.0.1.tgz", + "integrity": "sha512-aSBv0yp9HV9BgqX3bUj2DRY2kaIwcoN6ift+fKwPXj8eMoccJZzceQNshxt44V2ECkqwn7gvVEXVYTkw6zaTeQ==", + "license": "Apache-2.0", + "dependencies": { + "googleapis-common": "^8.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, "peerDependencies": { "@types/node": ">=18" }, @@ -1024,240 +1613,587 @@ } } }, - "node_modules/@inquirer/password": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", - "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@ioredis/commands": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", + "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz", + "integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", + "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", + "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/pattern": "30.4.0", + "@jest/reporters": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.4.1", + "jest-config": "30.4.2", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-resolve-dependencies": "30.4.2", + "jest-runner": "30.4.2", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "jest-watcher": "30.4.1", + "pretty-format": "30.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", + "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", + "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.4.1", + "jest-snapshot": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", + "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", + "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@sinonjs/fake-timers": "^15.4.0", + "@types/node": "*", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", + "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/types": "30.4.1", + "jest-mock": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", + "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", + "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": ">=18" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@types/node": ">=18" + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { - "@types/node": { + "node-notifier": { "optional": true } } }, - "node_modules/@inquirer/prompts": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", - "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.3.2", - "@inquirer/confirm": "^5.1.21", - "@inquirer/editor": "^4.2.23", - "@inquirer/expand": "^4.0.23", - "@inquirer/input": "^4.3.1", - "@inquirer/number": "^3.0.23", - "@inquirer/password": "^4.0.23", - "@inquirer/rawlist": "^4.1.11", - "@inquirer/search": "^3.2.2", - "@inquirer/select": "^4.4.2" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.4.1", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@inquirer/rawlist": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", - "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "node": ">=10" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/@inquirer/search": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", - "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "node_modules/@jest/schemas": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@inquirer/select": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", - "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "node_modules/@jest/snapshot-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", + "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@inquirer/type": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", - "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@ioredis/commands": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.10.0.tgz", - "integrity": "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q==", - "license": "MIT" - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", + "node_modules/@jest/test-result": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", + "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", + "dev": true, + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "@jest/console": "30.4.1", + "@jest/types": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" }, "engines": { - "node": ">=12" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/@jest/test-sequencer": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", + "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "@jest/test-result": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "slash": "^3.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@jest/transform": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", + "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", + "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "@babel/core": "^7.27.4", + "@jest/types": "30.4.1", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@jest/types": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", + "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "@jest/pattern": "30.4.0", + "@jest/schemas": "30.4.1", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1267,7 +2203,7 @@ "version": "0.3.11", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1284,7 +2220,7 @@ "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3810,6 +4746,19 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -4283,6 +5232,33 @@ "node": ">=18" } }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", + "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -4406,6 +5382,51 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", @@ -4536,7 +5557,45 @@ "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" } }, "node_modules/@types/json-schema": { @@ -4611,159 +5670,503 @@ "undici-types": "~8.3.0" } }, - "node_modules/@types/nodemailer": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz", - "integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==", + "node_modules/@types/nodemailer": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.1.tgz", + "integrity": "sha512-PxpaInm8V1JQDd4j0ds5HfvWQk8JupS1C0Picb96QJsrrRDjBH+DlK7L4ZdNSqNULhiZRQHc40nLVShaGxXAMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/pug": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", + "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/relateurl": { + "version": "0.2.33", + "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz", + "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/@types/web-push": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz", + "integrity": "sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ua-parser-js/pro-business": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@ua-parser-js/pro-business/-/pro-business-2.0.10.tgz", + "integrity": "sha512-GFSZXHrEM2hkFtsVepwEzEuLieOas4C6t0oU1/i9GgoxPXraCdApm8wdhFXtF61F1+MX+t2Ju+qOJcvI1hesLA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "detect-europe-js": "^0.1.2", + "is-standalone-pwa": "^0.1.1", + "ua-is-frozen": "^0.1.2" + }, + "bin": { + "pro-business": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/parse-json": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", - "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/passport": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", - "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/express": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/passport-jwt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", - "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/jsonwebtoken": "*", - "@types/passport-strategy": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/passport-strategy": { - "version": "0.2.38", - "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", - "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/express": "*", - "@types/passport": "*" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/pug": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", - "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], + "dev": true, "license": "MIT", - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/relateurl": { - "version": "0.2.33", - "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz", - "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==", + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "optional": true + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/@types/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@types/http-errors": "*", - "@types/node": "*" + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@types/validator": { - "version": "13.15.10", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", - "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", - "license": "MIT" - }, - "node_modules/@types/web-push": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/@types/web-push/-/web-push-3.6.4.tgz", - "integrity": "sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==", + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@ua-parser-js/pro-business": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@ua-parser-js/pro-business/-/pro-business-2.0.10.tgz", - "integrity": "sha512-GFSZXHrEM2hkFtsVepwEzEuLieOas4C6t0oU1/i9GgoxPXraCdApm8wdhFXtF61F1+MX+t2Ju+qOJcvI1hesLA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" ], - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "detect-europe-js": "^0.1.2", - "is-standalone-pwa": "^0.1.1", - "ua-is-frozen": "^0.1.2" - }, - "bin": { - "pro-business": "script/cli.js" - }, - "engines": { - "node": "*" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@vercel/oidc": { "version": "3.2.0", @@ -5160,7 +6563,36 @@ "devOptional": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ansi-regex": { @@ -5259,90 +6691,454 @@ "license": "MIT", "optional": true }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/assert-never": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", + "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "license": "MIT", + "optional": true + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/babel-jest": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", + "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.4.1", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.4.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", + "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", + "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.4.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC", + "optional": true + }, + "node_modules/boxen": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", "license": "MIT", "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/assert-never": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.4.0.tgz", - "integrity": "sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==", + "node_modules/boxen/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "optional": true + "engines": { + "node": ">=8" + } }, - "node_modules/astring": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "node_modules/boxen/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", - "bin": { - "astring": "bin/astring" + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { - "possible-typed-array-names": "^1.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", - "engines": { - "node": ">= 6.0.0" + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/babel-walk": { - "version": "3.0.0-canary-5", - "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", - "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "license": "MIT", - "optional": true, "dependencies": { - "@babel/types": "^7.9.6" + "fill-range": "^7.1.1" }, "engines": { - "node": ">= 10.0.0" + "node": ">=8" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, "engines": { - "node": ">= 0.6.0" + "node": ">= 6" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "funding": [ { "type": "github", @@ -5357,1431 +7153,1667 @@ "url": "https://feross.org/support" } ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "license": "MIT" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "license": "MIT" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bullmq": { + "version": "5.79.0", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.79.0.tgz", + "integrity": "sha512-sg+kYGn7PIDI/AAkSWINPNz0vMp745YaBYMyo3AtcfXk5iCvjEPU+XMbU3yf7rSf1KsU+OfU+GH8FhxUa+WDNA==", + "license": "MIT", + "dependencies": { + "cron-parser": "4.9.0", + "ioredis": "5.10.1", + "msgpackr": "2.0.2", + "node-abort-controller": "3.1.1", + "semver": "7.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "redis": ">=5.0.0" + }, + "peerDependenciesMeta": { + "redis": { + "optional": true + } + } + }, + "node_modules/bullmq/node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", "license": "MIT" }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "node_modules/bullmq/node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { - "node": "^4.5.0 || >= 5.9" + "node": ">= 0.8" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.31", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", - "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" }, "engines": { - "node": ">=6.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/bcrypt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", - "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", - "hasInstallScript": true, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">= 18" + "node": ">= 0.4" } }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "devOptional": true, "license": "MIT", "engines": { - "node": "*" + "node": ">=6" } }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", "license": "MIT", + "optional": true, "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" } }, - "node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT" + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "devOptional": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" }, - "node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "license": "MIT", "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", - "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "license": "ISC", - "optional": true - }, - "node_modules/boxen": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", - "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.0", - "camelcase": "^6.2.0", - "chalk": "^4.1.0", - "cli-boxes": "^2.2.1", - "string-width": "^4.2.2", - "type-fest": "^0.20.2", - "widest-line": "^3.1.0", - "wrap-ansi": "^7.0.0" - }, "engines": { "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/boxen/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", "license": "MIT", - "engines": { - "node": ">=8" + "optional": true, + "dependencies": { + "is-regex": "^1.0.3" } }, - "node_modules/boxen/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/check-disk-space": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz", + "integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==", "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">=16" } }, - "node_modules/boxen/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", "license": "MIT", + "optional": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" }, "engines": { - "node": ">=10" + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "optional": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "node_modules/cheerio/node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], "license": "MIT", + "optional": true, "dependencies": { - "balanced-match": "^1.0.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" + "readdirp": "^4.0.1" }, "engines": { - "node": ">=8" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "devOptional": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": ">=6.0" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "funding": [ { "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "url": "https://github.com/sponsors/sibiraj-s" } ], "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "license": "MIT", - "dependencies": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" + "optional": true, + "engines": { + "node": ">=8" } }, - "node_modules/buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "license": "MIT" - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", "license": "MIT" }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", "license": "MIT" }, - "node_modules/bullmq": { - "version": "5.79.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.79.0.tgz", - "integrity": "sha512-sg+kYGn7PIDI/AAkSWINPNz0vMp745YaBYMyo3AtcfXk5iCvjEPU+XMbU3yf7rSf1KsU+OfU+GH8FhxUa+WDNA==", + "node_modules/class-validator": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", + "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", "license": "MIT", "dependencies": { - "cron-parser": "4.9.0", - "ioredis": "5.10.1", - "msgpackr": "2.0.2", - "node-abort-controller": "3.1.1", - "semver": "7.8.1", - "tslib": "2.8.1" - }, + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "license": "MIT", "engines": { - "node": ">=12.22.0" - }, - "peerDependencies": { - "redis": ">=5.0.0" + "node": ">=6" }, - "peerDependenciesMeta": { - "redis": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bullmq/node_modules/@ioredis/commands": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", - "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", - "license": "MIT" - }, - "node_modules/bullmq/node_modules/ioredis": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", - "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, "license": "MIT", "dependencies": { - "@ioredis/commands": "1.5.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" + "restore-cursor": "^3.1.0" }, "engines": { - "node": ">=12.22.0" + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", "dependencies": { - "streamsearch": "^1.1.0" + "string-width": "^4.2.0" }, "engines": { - "node": ">=10.16.0" + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, "engines": { - "node": ">= 0.8" + "node": ">=12" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "devOptional": true, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.8" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "license": "MIT", + "node_modules/cluster-key-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", + "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", + "license": "Apache-2.0", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", - "devOptional": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" + "node_modules/code-block-writer": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", + "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", + "dev": true, + "license": "MIT" }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=7.0.0" } }, - "node_modules/character-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", - "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", + "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "is-regex": "^1.0.3" + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, "license": "MIT" }, - "node_modules/check-disk-space": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/check-disk-space/-/check-disk-space-3.4.0.tgz", - "integrity": "sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==", + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], "license": "MIT", - "engines": { - "node": ">=16" + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" } }, - "node_modules/cheerio": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", "license": "MIT", "optional": true, "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "encoding-sniffer": "^0.2.0", - "htmlparser2": "^9.1.0", - "parse5": "^7.1.2", - "parse5-htmlparser2-tree-adapter": "^7.0.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^6.19.5", - "whatwg-mimetype": "^4.0.0" - }, + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", "engines": { - "node": ">=18.17" + "node": ">=18" }, "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/cheerio/node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", "license": "MIT", - "optional": true, "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "object-assign": "^4", + "vary": "^1" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 0.10" }, "funding": { - "url": "https://paulmillr.com/funding/" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, "engines": { - "node": ">=6.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/ci-info": { + "node_modules/countries-and-timezones": { "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], + "resolved": "https://registry.npmjs.org/countries-and-timezones/-/countries-and-timezones-3.9.0.tgz", + "integrity": "sha512-3upkVtUfZgdp5xOQI/Deg1ye73ae6bygJMO4UvtUi6wLjwcj7cBqx2PzSZ1VL8i5iveBbjhsQp2Judx7kPKivA==", "license": "MIT", - "optional": true, "engines": { - "node": ">=8" + "node": ">=8.x", + "npm": ">=5.x" } }, - "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", - "license": "MIT" - }, - "node_modules/class-transformer": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT" - }, - "node_modules/class-validator": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.15.1.tgz", - "integrity": "sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==", + "node_modules/cron": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/cron/-/cron-4.4.0.tgz", + "integrity": "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==", "license": "MIT", "dependencies": { - "@types/validator": "^13.15.3", - "libphonenumber-js": "^1.11.1", - "validator": "^13.15.22" - } - }, - "node_modules/cli-boxes": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", - "license": "MIT", + "@types/luxon": "~3.7.0", + "luxon": "~3.7.0" + }, "engines": { - "node": ">=6" + "node": ">=18.x" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "ko-fi", + "url": "https://ko-fi.com/intcreator" } }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", "license": "MIT", "dependencies": { - "restore-cursor": "^3.1.0" + "luxon": "^3.2.1" }, "engines": { - "node": ">=8" + "node": ">=12.0.0" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" } }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", - "dev": true, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { - "string-width": "^4.2.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" + "node": ">= 8" } }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, + "node_modules/css-declaration-sorter": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.4.0.tgz", + "integrity": "sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==", "license": "ISC", + "optional": true, "engines": { - "node": ">= 12" + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "optional": true, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" }, - "engines": { - "node": ">=12" + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "license": "MIT", + "optional": true, + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "optional": true, "engines": { - "node": ">=8" + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" + "optional": true, + "bin": { + "cssesc": "bin/cssesc" }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/cssnano": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.9.tgz", + "integrity": "sha512-uPR75+5Dk/WJ/YSPR1/YDHdwMM9c5FsaARljfKWgeCKLKOtJ0we21xy/RcCjn53fZnD/f6yYEIZ8pu18+GnbNQ==", "license": "MIT", + "optional": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "cssnano-preset-default": "^7.0.17", + "lilconfig": "^3.1.3" }, "engines": { - "node": ">=10" + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.5.13" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, + "node_modules/cssnano-preset-default": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.17.tgz", + "integrity": "sha512-11qO63A+czwguQFJCaTdICvbaxn0pJzz/XghLlv+OT7WyToDxAMR0Xb3/26/l0y0hQJywwNbj/SLSQlGBHE1OA==", "license": "MIT", + "optional": true, + "dependencies": { + "browserslist": "^4.28.2", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^5.0.3", + "postcss-calc": "^10.1.1", + "postcss-colormin": "^7.0.10", + "postcss-convert-values": "^7.0.12", + "postcss-discard-comments": "^7.0.8", + "postcss-discard-duplicates": "^7.0.4", + "postcss-discard-empty": "^7.0.3", + "postcss-discard-overridden": "^7.0.3", + "postcss-merge-longhand": "^7.0.7", + "postcss-merge-rules": "^7.0.11", + "postcss-minify-font-values": "^7.0.3", + "postcss-minify-gradients": "^7.0.5", + "postcss-minify-params": "^7.0.9", + "postcss-minify-selectors": "^7.1.2", + "postcss-normalize-charset": "^7.0.3", + "postcss-normalize-display-values": "^7.0.3", + "postcss-normalize-positions": "^7.0.4", + "postcss-normalize-repeat-style": "^7.0.4", + "postcss-normalize-string": "^7.0.3", + "postcss-normalize-timing-functions": "^7.0.3", + "postcss-normalize-unicode": "^7.0.9", + "postcss-normalize-url": "^7.0.3", + "postcss-normalize-whitespace": "^7.0.3", + "postcss-ordered-values": "^7.0.4", + "postcss-reduce-initial": "^7.0.9", + "postcss-reduce-transforms": "^7.0.3", + "postcss-svgo": "^7.1.3", + "postcss-unique-selectors": "^7.0.7" + }, "engines": { - "node": ">=0.8" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.1.tgz", - "integrity": "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" } }, - "node_modules/code-block-writer": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-11.0.3.tgz", - "integrity": "sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/cssnano-preset-lite": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/cssnano-preset-lite/-/cssnano-preset-lite-4.0.4.tgz", + "integrity": "sha512-iV7xT9JEk0OJwRWuSV2Y48IkntNjvoiscoioFDcgmuoIvpZV8gIg1++GBHTj72HgmDCALH8y9ELgt+aFM2Nh9g==", "license": "MIT", + "optional": true, "dependencies": { - "color-name": "~1.1.4" + "cssnano-utils": "^5.0.1", + "postcss-discard-comments": "^7.0.4", + "postcss-discard-empty": "^7.0.1", + "postcss-normalize-whitespace": "^7.0.1" }, "engines": { - "node": ">=7.0.0" + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, + "node_modules/cssnano-utils": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.3.tgz", + "integrity": "sha512-ynIREMICLxkxm7e9bCR9sh75s4Q5drICi0ua1yxo5jH2XPBqSKkl4dOh4EbFqtUmnTMhRffHgYL0EKKkMjtJTg==", "license": "MIT", + "optional": true, "engines": { - "node": ">= 6" + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.5.13" } }, - "node_modules/comment-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", - "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", - "dev": true, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", "license": "MIT", + "optional": true, "dependencies": { - "array-timsort": "^1.0.3", - "esprima": "^4.0.1" + "css-tree": "~2.2.0" }, "engines": { - "node": ">= 6" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", - "engines": [ - "node >= 6.0" - ], + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", "license": "MIT", + "optional": true, "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" } }, - "node_modules/constantinople": { + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0", + "optional": true + }, + "node_modules/csv-parse": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-6.2.1.tgz", + "integrity": "sha512-LRLMV+UCyfMokp8Wb411duBf1gaBKJfOfBWU9eHMJ+b+cJYZsNu3AFmjJf3+yPGd59Exz1TsMjaSFyxnYB9+IQ==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", - "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "license": "MIT", - "optional": true, - "dependencies": { - "@babel/parser": "^7.6.0", - "@babel/types": "^7.6.1" + "engines": { + "node": ">= 12" } }, - "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": ">=18" + "node": ">=6.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "node_modules/dedent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "license": "MIT", - "engines": { - "node": ">= 0.6" + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } } }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", + "optional": true, "engines": { - "node": ">= 0.6" + "node": ">=4.0.0" } }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" + "clone": "^1.0.2" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "dev": true, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "license": "MIT", "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/countries-and-timezones": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/countries-and-timezones/-/countries-and-timezones-3.9.0.tgz", - "integrity": "sha512-3upkVtUfZgdp5xOQI/Deg1ye73ae6bygJMO4UvtUi6wLjwcj7cBqx2PzSZ1VL8i5iveBbjhsQp2Judx7kPKivA==", + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { - "node": ">=8.x", - "npm": ">=5.x" + "node": ">= 0.8" } }, - "node_modules/cron": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/cron/-/cron-4.4.0.tgz", - "integrity": "sha512-fkdfq+b+AHI4cKdhZlppHveI/mgz2qpiYxcm+t5E5TsxX7QrLS1VE0+7GENEk9z0EeGPcpSciGv6ez24duWhwQ==", + "node_modules/detect-europe-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", + "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "license": "MIT", - "dependencies": { - "@types/luxon": "~3.7.0", - "luxon": "~3.7.0" - }, + "optional": true, "engines": { - "node": ">=18.x" - }, - "funding": { - "type": "ko-fi", - "url": "https://ko-fi.com/intcreator" + "node": ">=8" } }, - "node_modules/cron-parser": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", - "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "devOptional": true, "license": "MIT", - "dependencies": { - "luxon": "^3.2.1" - }, "engines": { - "node": ">=12.0.0" + "node": ">=8" } }, - "node_modules/cross-env": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", - "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", - "dev": true, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT", + "optional": true + }, + "node_modules/diacritics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", + "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==", + "license": "MIT" + }, + "node_modules/display-notification": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/display-notification/-/display-notification-3.0.0.tgz", + "integrity": "sha512-/qAvqRy4zWP847zJc1GvXOc+AV1l9/ECKPA7APrLnqjur0o5liMM4bDJ/b1hnJo6Tyb5BfOHyyd4vn9lCh/NSg==", "license": "MIT", + "optional": true, "dependencies": { - "@epic-web/invariant": "^1.0.0", - "cross-spawn": "^7.0.6" - }, - "bin": { - "cross-env": "dist/bin/cross-env.js", - "cross-env-shell": "dist/bin/cross-env-shell.js" + "escape-string-applescript": "^3.0.0", + "run-applescript": "^5.0.0" }, "engines": { - "node": ">=20" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "license": "MIT", + "optional": true + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "license": "MIT", + "optional": true, "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, - "engines": { - "node": ">= 8" + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/crypto-js": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", - "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", - "license": "MIT" - }, - "node_modules/css-declaration-sorter": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.4.0.tgz", - "integrity": "sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw==", - "license": "ISC", + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", "optional": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, "engines": { - "node": "^14 || ^16 || >=18" + "node": ">= 4" }, - "peerDependencies": { - "postcss": "^8.0.9" + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "license": "BSD-2-Clause", "optional": true, "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, "funding": { - "url": "https://github.com/sponsors/fb55" + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "license": "MIT", - "optional": true, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "license": "BSD-2-Clause", "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" + "dotenv": "^16.4.5" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "license": "BSD-2-Clause", - "optional": true, "engines": { - "node": ">= 6" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/fb55" + "url": "https://dotenvx.com" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", - "optional": true, - "bin": { - "cssesc": "bin/cssesc" + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { - "node": ">=4" + "node": ">= 0.4" } }, - "node_modules/cssnano": { - "version": "7.1.9", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.9.tgz", - "integrity": "sha512-uPR75+5Dk/WJ/YSPR1/YDHdwMM9c5FsaARljfKWgeCKLKOtJ0we21xy/RcCjn53fZnD/f6yYEIZ8pu18+GnbNQ==", + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", "license": "MIT", - "optional": true, "dependencies": { - "cssnano-preset-default": "^7.0.17", - "lilconfig": "^3.1.3" + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.1.tgz", + "integrity": "sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==", + "license": "Apache-2.0", + "optional": true, + "bin": { + "ejs": "bin/cli.js" }, "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" + "node": ">=0.12.18" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.360", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz", + "integrity": "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.5.13" + "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, - "node_modules/cssnano-preset-default": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.17.tgz", - "integrity": "sha512-11qO63A+czwguQFJCaTdICvbaxn0pJzz/XghLlv+OT7WyToDxAMR0Xb3/26/l0y0hQJywwNbj/SLSQlGBHE1OA==", + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", - "optional": true, - "dependencies": { - "browserslist": "^4.28.2", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^5.0.3", - "postcss-calc": "^10.1.1", - "postcss-colormin": "^7.0.10", - "postcss-convert-values": "^7.0.12", - "postcss-discard-comments": "^7.0.8", - "postcss-discard-duplicates": "^7.0.4", - "postcss-discard-empty": "^7.0.3", - "postcss-discard-overridden": "^7.0.3", - "postcss-merge-longhand": "^7.0.7", - "postcss-merge-rules": "^7.0.11", - "postcss-minify-font-values": "^7.0.3", - "postcss-minify-gradients": "^7.0.5", - "postcss-minify-params": "^7.0.9", - "postcss-minify-selectors": "^7.1.2", - "postcss-normalize-charset": "^7.0.3", - "postcss-normalize-display-values": "^7.0.3", - "postcss-normalize-positions": "^7.0.4", - "postcss-normalize-repeat-style": "^7.0.4", - "postcss-normalize-string": "^7.0.3", - "postcss-normalize-timing-functions": "^7.0.3", - "postcss-normalize-unicode": "^7.0.9", - "postcss-normalize-url": "^7.0.3", - "postcss-normalize-whitespace": "^7.0.3", - "postcss-ordered-values": "^7.0.4", - "postcss-reduce-initial": "^7.0.9", - "postcss-reduce-transforms": "^7.0.3", - "postcss-svgo": "^7.1.3", - "postcss-unique-selectors": "^7.0.7" - }, "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.5.13" + "node": ">= 0.8" } }, - "node_modules/cssnano-preset-lite": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/cssnano-preset-lite/-/cssnano-preset-lite-4.0.4.tgz", - "integrity": "sha512-iV7xT9JEk0OJwRWuSV2Y48IkntNjvoiscoioFDcgmuoIvpZV8gIg1++GBHTj72HgmDCALH8y9ELgt+aFM2Nh9g==", + "node_modules/encoding-japanese": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz", + "integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==", "license": "MIT", "optional": true, - "dependencies": { - "cssnano-utils": "^5.0.1", - "postcss-discard-comments": "^7.0.4", - "postcss-discard-empty": "^7.0.1", - "postcss-normalize-whitespace": "^7.0.1" - }, "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" - }, - "peerDependencies": { - "postcss": "^8.4.32" + "node": ">=8.10.0" } }, - "node_modules/cssnano-utils": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.3.tgz", - "integrity": "sha512-ynIREMICLxkxm7e9bCR9sh75s4Q5drICi0ua1yxo5jH2XPBqSKkl4dOh4EbFqtUmnTMhRffHgYL0EKKkMjtJTg==", + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "license": "MIT", "optional": true, - "engines": { - "node": "^18.12.0 || ^20.9.0 || >=22.0" + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" }, - "peerDependencies": { - "postcss": "^8.5.13" + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "license": "MIT", "optional": true, "dependencies": { - "css-tree": "~2.2.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=0.10.0" } }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", - "optional": true, "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", + "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=10.2.0" } }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "license": "CC0-1.0", - "optional": true - }, - "node_modules/csv-parse": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-6.2.1.tgz", - "integrity": "sha512-LRLMV+UCyfMokp8Wb411duBf1gaBKJfOfBWU9eHMJ+b+cJYZsNu3AFmjJf3+yPGd59Exz1TsMjaSFyxnYB9+IQ==", - "license": "MIT" - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=10.0.0" } }, - "node_modules/dayjs": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", - "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">= 0.6" } }, - "node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } + "engines": { + "node": ">= 0.6" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", - "optional": true, "engines": { - "node": ">=4.0.0" + "node": ">= 0.6" } }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, - "node_modules/deepmerge-ts": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", - "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", - "license": "BSD-3-Clause", + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "optional": true, "engines": { - "node": ">=16.0.0" + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optional": true, + "engines": { + "node": ">=6" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "devOptional": true, "license": "MIT", "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "is-arrayish": "^0.2.1" } }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { - "node": ">=0.10" + "node": ">= 0.4" } }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.4" } }, - "node_modules/detect-europe-js": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", - "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - } - ], + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "license": "MIT" }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", - "optional": true, + "dependencies": { + "es-errors": "^1.3.0" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "node_modules/escape-goat": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", + "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", "license": "MIT", "optional": true, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT", - "optional": true - }, - "node_modules/diacritics": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", - "integrity": "sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA==", + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/display-notification": { + "node_modules/escape-string-applescript": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/display-notification/-/display-notification-3.0.0.tgz", - "integrity": "sha512-/qAvqRy4zWP847zJc1GvXOc+AV1l9/ECKPA7APrLnqjur0o5liMM4bDJ/b1hnJo6Tyb5BfOHyyd4vn9lCh/NSg==", + "resolved": "https://registry.npmjs.org/escape-string-applescript/-/escape-string-applescript-3.0.0.tgz", + "integrity": "sha512-Wru0bY9XSICPNsy7KwbAZww9SLkoYjP9GtJkmnQOEqOy9U13KA0OJXoti7FaVMsiQ0mQfh916/xoByM6PqdW4g==", "license": "MIT", "optional": true, - "dependencies": { - "escape-string-applescript": "^3.0.0", - "run-applescript": "^5.0.0" - }, "engines": { "node": ">=12" }, @@ -6789,828 +8821,1035 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/doctypes": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", - "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", - "license": "MIT", - "optional": true - }, - "node_modules/dom-serializer": { + "node_modules/escape-string-regexp": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "engines": { + "node": ">=8" } }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "license": "BSD-2-Clause", - "optional": true + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", - "optional": true, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "license": "BSD-3-Clause", "dependencies": { - "domelementtype": "^2.3.0" + "estraverse": "^5.1.0" }, "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "node": ">=0.10" } }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "license": "BSD-2-Clause", - "optional": true, "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" + "estraverse": "^5.2.0" }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" + "engines": { + "node": ">=4.0" } }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "license": "BSD-2-Clause", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" + "node": ">=4.0" } }, - "node_modules/dotenv-expand": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", - "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", - "license": "BSD-2-Clause", + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "devOptional": true, + "license": "MIT", "dependencies": { - "dotenv": "^16.4.5" + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://dotenvx.com" + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/dotenv-expand/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", + "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-util": "30.4.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, "engines": { - "node": ">=12" + "node": ">= 18" }, "funding": { - "url": "https://dotenvx.com" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" + "node": ">=6.6.0" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } + "node_modules/extend-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/extend-object/-/extend-object-1.0.0.tgz", + "integrity": "sha512-0dHDIXC7y7LDmCh/lp1oYkmv73K25AMugQI07r8eFopkW6f7Ufn1q+ETMsJjnV9Am14SlElkqy3O92r6xEaxPw==", + "license": "MIT", + "optional": true }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, - "node_modules/ejs": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-5.0.1.tgz", - "integrity": "sha512-COqBPFMxuPTPspXl2DkVYaDS3HtrD1GpzOGkNTJ1IYkifq/r9h8SVEFrjA3D9/VJGOEoMQcrlhpntcSUrM8k6A==", - "license": "Apache-2.0", - "optional": true, - "bin": { - "ejs": "bin/cli.js" + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" }, "engines": { - "node": ">=0.12.18" + "node": ">=8.6.0" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.360", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.360.tgz", - "integrity": "sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==", - "devOptional": true, - "license": "ISC" + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" } }, - "node_modules/encoding-japanese": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/encoding-japanese/-/encoding-japanese-2.2.0.tgz", - "integrity": "sha512-EuJWwlHPZ1LbADuKTClvHtwbaFn4rOD+dRAbWysqEOXRc2Uui0hJInNJrsdH0c+OhJA4nrCBdSkW4DD5YxAo6A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.10.0" + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" } }, - "node_modules/encoding-sniffer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "node_modules/fd-package-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", + "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + "walk-up-path": "^4.0.0" } }, - "node_modules/encoding-sniffer/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], "license": "MIT", "dependencies": { - "once": "^1.4.0" + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" } }, - "node_modules/engine.io": { - "version": "6.6.9", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", - "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", "license": "MIT", "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "@types/ws": "^8.5.12", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.21.0" + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" }, "engines": { - "node": ">=10.2.0" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, "engines": { - "node": ">=10.0.0" + "node": ">=8" } }, - "node_modules/engine.io/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/engine.io/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/engine.io/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/fixpack": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fixpack/-/fixpack-4.0.0.tgz", + "integrity": "sha512-5SM1+H2CcuJ3gGEwTiVo/+nd/hYpNj9Ch3iMDOQ58ndY+VGQ2QdvaUTkd3otjZvYnd/8LF/HkJ5cx7PBq0orCQ==", "license": "MIT", + "optional": true, "dependencies": { - "mime-db": "1.52.0" + "alce": "1.2.0", + "chalk": "^3.0.0", + "detect-indent": "^6.0.0", + "detect-newline": "^3.1.0", + "extend-object": "^1.0.0", + "rc": "^1.2.8" }, - "engines": { - "node": ">= 0.6" + "bin": { + "fixpack": "bin/fixpack" } }, - "node_modules/engine.io/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "node_modules/fixpack/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", - "dev": true, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "is-callable": "^1.2.7" }, "engines": { - "node": ">=10.13.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "optional": true, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, "engines": { - "node": ">=0.12" + "node": ">=14" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", + "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^4.0.1", + "cosmiconfig": "^8.2.0", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, "engines": { - "node": ">=6" + "node": ">=14.21.3" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "devOptional": true, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, "license": "MIT", "dependencies": { - "is-arrayish": "^0.2.1" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", + "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, "engines": { - "node": ">= 0.4" + "node": "*" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/formatly": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", + "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", + "dev": true, "license": "MIT", + "dependencies": { + "fd-package-json": "^2.0.0" + }, + "bin": { + "formatly": "bin/index.mjs" + }, "engines": { - "node": ">= 0.4" + "node": ">=18.3.0" } }, - "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "license": "MIT", "dependencies": { - "es-errors": "^1.3.0" + "fetch-blob": "^3.1.2" }, "engines": { - "node": ">= 0.4" + "node": ">=12.20.0" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.6" } }, - "node_modules/escape-goat": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz", - "integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==", + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", - "optional": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.8" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-applescript": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/escape-string-applescript/-/escape-string-applescript-3.0.0.tgz", - "integrity": "sha512-Wru0bY9XSICPNsy7KwbAZww9SLkoYjP9GtJkmnQOEqOy9U13KA0OJXoti7FaVMsiQ0mQfh916/xoByM6PqdW4g==", + "node_modules/front-matter": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", + "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", "license": "MIT", - "optional": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "js-yaml": "^3.13.1" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" + "node_modules/front-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" } }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "license": "BSD-3-Clause", + "node_modules/front-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", "dependencies": { - "estraverse": "^5.1.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" }, - "engines": { - "node": ">=0.10" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">=4.0" + "node": ">=12" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "dev": true, + "license": "Unlicense" }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.6" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, "engines": { - "node": ">=0.8.x" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, - "node_modules/eventsource-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", - "license": "MIT", + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, "engines": { - "node": ">=18.0.0" + "node": ">=18" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", "license": "MIT", - "optional": true, "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, + "is-property": "^1.0.2" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "node": ">=6.9.0" } }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "license": "ISC", - "optional": true + "engines": { + "node": "6.* || 8.* || >= 10.*" + } }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": ">= 18" + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/express/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, "license": "MIT", "engines": { - "node": ">=6.6.0" + "node": ">=8.0.0" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/extend-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/extend-object/-/extend-object-1.0.0.tgz", - "integrity": "sha512-0dHDIXC7y7LDmCh/lp1oYkmv73K25AMugQI07r8eFopkW6f7Ufn1q+ETMsJjnV9Am14SlElkqy3O92r6xEaxPw==", - "license": "MIT", - "optional": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=8.6.0" + "node": ">= 0.4" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { - "reusify": "^1.0.4" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fd-package-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-2.0.0.tgz", - "integrity": "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ==", - "dev": true, - "license": "MIT", + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", "dependencies": { - "walk-up-path": "^4.0.0" + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "license": "BSD-2-Clause" + }, + "node_modules/google-auth-library": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", + "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "engines": { + "node": ">=18" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", + "node_modules/google-gax": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.6.tgz", + "integrity": "sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==", + "license": "Apache-2.0", "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.8.0", + "duplexify": "^4.1.3", + "google-auth-library": "^10.1.0", + "google-logging-utils": "^1.1.1", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^3.0.0", + "protobufjs": "^7.5.3", + "retry-request": "^8.0.0", + "rimraf": "^5.0.1" }, "engines": { - "node": "^12.20 || >= 14.13" + "node": ">=18" } }, - "node_modules/fflate": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", - "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", - "license": "MIT" - }, - "node_modules/file-type": { - "version": "21.3.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", - "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "node_modules/google-gax/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "license": "MIT", "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" }, "engines": { - "node": ">=20" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", + "node_modules/google-gax/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", "dependencies": { - "to-regex-range": "^5.0.1" + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": ">=14" } }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", + "node_modules/googleapis-common": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz", + "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==", + "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" + "extend": "^3.0.2", + "gaxios": "^7.0.0-rc.4", + "google-auth-library": "^10.1.0", + "qs": "^6.7.0", + "url-template": "^2.0.8" }, "engines": { - "node": ">= 18.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fixpack": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fixpack/-/fixpack-4.0.0.tgz", - "integrity": "sha512-5SM1+H2CcuJ3gGEwTiVo/+nd/hYpNj9Ch3iMDOQ58ndY+VGQ2QdvaUTkd3otjZvYnd/8LF/HkJ5cx7PBq0orCQ==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "license": "MIT", - "optional": true, "dependencies": { - "alce": "1.2.0", - "chalk": "^3.0.0", - "detect-indent": "^6.0.0", - "detect-newline": "^3.1.0", - "extend-object": "^1.0.0", - "rc": "^1.2.8" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" }, "bin": { - "fixpack": "bin/fixpack" + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" } }, - "node_modules/fixpack/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, "engines": { "node": ">=8" } }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "license": "MIT", "dependencies": { - "is-callable": "^1.2.7" + "es-define-property": "^1.0.0" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7618,561 +9857,648 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "has-symbols": "^1.0.3" }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fork-ts-checker-webpack-plugin": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz", - "integrity": "sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==", - "dev": true, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.16.7", - "chalk": "^4.1.2", - "chokidar": "^4.0.1", - "cosmiconfig": "^8.2.0", - "deepmerge": "^4.2.2", - "fs-extra": "^10.0.0", - "memfs": "^3.4.1", - "minimatch": "^3.0.4", - "node-abort-controller": "^3.0.1", - "schema-utils": "^3.1.1", - "semver": "^7.3.5", - "tapable": "^2.2.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=14.21.3" - }, - "peerDependencies": { - "typescript": ">3.6.0", - "webpack": "^5.11.0" + "node": ">= 0.4" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "optional": true, + "bin": { + "he": "bin/he" } }, - "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/html-to-text": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-10.0.0.tgz", + "integrity": "sha512-2OH59Gtprdczel+7Rxgpz9hGVJREaf8Lt1H4kZwWHpEn70VQKRuMNGsb2eDbwaTzrYzb0hheiOG1P7Dim0B4dQ==", + "license": "MIT", + "optional": true, "dependencies": { - "brace-expansion": "^1.1.7" + "@selderee/plugin-htmlparser2": "~0.12.0", + "deepmerge-ts": "^7.1.5", + "dom-serializer": "^2.0.0", + "htmlparser2": "^10.1.0", + "selderee": "~0.12.0" }, "engines": { - "node": "*" + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/KillyMXI" } }, - "node_modules/formatly": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/formatly/-/formatly-0.3.0.tgz", - "integrity": "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==", - "dev": true, + "node_modules/htmlnano": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-3.2.0.tgz", + "integrity": "sha512-ZUCyB3w9EdpkaePGuYTBF7hT1MWdcz7y0/h2Cnl1MKLwbyj7aIM+Eq7SRg+ILF1mvR7zJKu06lw9zpgCxOFymw==", "license": "MIT", + "optional": true, "dependencies": { - "fd-package-json": "^2.0.0" + "@types/relateurl": "^0.2.33", + "commander": "^14.0.0", + "cosmiconfig": "^9.0.0", + "posthtml": "^0.16.5" }, "bin": { - "formatly": "bin/index.mjs" + "htmlnano": "dist/bin.js" }, - "engines": { - "node": ">=18.3.0" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" + "peerDependencies": { + "cssnano": "^7.0.0", + "postcss": "^8.3.11", + "purgecss": "^8.0.0", + "relateurl": "^0.2.7", + "srcset": "^5.0.1", + "svgo": "^4.0.0", + "terser": "^5.21.0", + "uncss": "^0.17.3" }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/front-matter": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/front-matter/-/front-matter-4.0.2.tgz", - "integrity": "sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==", - "license": "MIT", - "dependencies": { - "js-yaml": "^3.13.1" + "peerDependenciesMeta": { + "cssnano": { + "optional": true + }, + "postcss": { + "optional": true + }, + "purgecss": { + "optional": true + }, + "relateurl": { + "optional": true + }, + "srcset": { + "optional": true + }, + "svgo": { + "optional": true + }, + "terser": { + "optional": true + }, + "uncss": { + "optional": true + } } }, - "node_modules/front-matter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/htmlnano/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" + "optional": true, + "engines": { + "node": ">=20" } }, - "node_modules/front-matter/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "node_modules/htmlnano/node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "license": "MIT", + "optional": true, "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/fs-extra": { + "node_modules/htmlparser2": { "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], "license": "MIT", + "optional": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, - "node_modules/fs-monkey": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", - "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", - "dev": true, - "license": "Unlicense" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", + "node": ">=0.12" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" - }, + "node_modules/http_ece": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", + "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==", + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=16" } }, - "node_modules/gaxios/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 0.8" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "url": "https://opencollective.com/express" } }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">=18" + "node": ">= 14" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", "dependencies": { - "is-property": "^1.0.2" + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "devOptional": true, + "license": "Apache-2.0", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=10.17.0" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "node_modules/i18n-iso-countries": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz", + "integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "diacritics": "1.3.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 12" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "devOptional": true, "license": "MIT", - "optional": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, "engines": { - "node": ">=8" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", + "node_modules/import-in-the-middle": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz", + "integrity": "sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==", + "license": "Apache-2.0", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "acorn": "^8.15.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" }, "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-tsconfig": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", - "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "once": "^1.3.0", + "wrappy": "1" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC", + "optional": true + }, + "node_modules/ioredis": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", + "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "@ioredis/commands": "1.10.0", + "cluster-key-slot": "1.1.1", + "debug": "4.4.3", + "denque": "2.1.0", + "redis-errors": "1.2.0", + "redis-parser": "3.0.0", + "standard-as-callback": "2.1.0" }, - "bin": { - "glob": "dist/esm/bin.mjs" + "engines": { + "node": ">=12.22.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/ioredis" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", "engines": { - "node": ">= 6" + "node": ">= 0.10" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "devOptional": true, + "license": "MIT" }, - "node_modules/google-auth-library": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", - "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==", - "license": "Apache-2.0", + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.1.4", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", - "jws": "^4.0.0" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/google-gax": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.6.tgz", - "integrity": "sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==", - "license": "Apache-2.0", + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "optional": true, "dependencies": { - "@grpc/grpc-js": "^1.12.6", - "@grpc/proto-loader": "^0.8.0", - "duplexify": "^4.1.3", - "google-auth-library": "^10.1.0", - "google-logging-utils": "^1.1.1", - "node-fetch": "^3.3.2", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^3.0.0", - "protobufjs": "^7.5.3", - "retry-request": "^8.0.0", - "rimraf": "^5.0.1" + "hasown": "^2.0.2" }, "engines": { - "node": ">=18" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/google-gax/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" + "optional": true, + "bin": { + "is-docker": "cli.js" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/google-gax/node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "license": "MIT", + "optional": true, "dependencies": { - "glob": "^10.3.7" - }, + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-expression/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "optional": true, "bin": { - "rimraf": "dist/esm/bin.mjs" + "acorn": "bin/acorn" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=0.4.0" } }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", "engines": { - "node": ">=14" + "node": ">=0.10.0" } }, - "node_modules/googleapis-common": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-8.0.1.tgz", - "integrity": "sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "gaxios": "^7.0.0-rc.4", - "google-auth-library": "^10.1.0", - "qs": "^6.7.0", - "url-template": "^2.0.8" - }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=8" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=6" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "license": "MIT", "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" + "node": ">=0.10.0" } }, - "node_modules/handlebars/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/is-json": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", + "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", + "license": "ISC", + "optional": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.12.0" } }, - "node_modules/has-property-descriptors": { + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "license": "MIT", + "optional": true + }, + "node_modules/is-property": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "license": "MIT", + "optional": true, "dependencies": { - "es-define-property": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/is-standalone-pwa": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", + "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "license": "MIT", "dependencies": { - "has-symbols": "^1.0.3" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -8181,651 +10507,809 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, "engines": { - "node": ">= 0.4" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "license": "MIT", - "optional": true, - "bin": { - "he": "bin/he" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/html-to-text": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-10.0.0.tgz", - "integrity": "sha512-2OH59Gtprdczel+7Rxgpz9hGVJREaf8Lt1H4kZwWHpEn70VQKRuMNGsb2eDbwaTzrYzb0hheiOG1P7Dim0B4dQ==", + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "license": "MIT", "optional": true, "dependencies": { - "@selderee/plugin-htmlparser2": "~0.12.0", - "deepmerge-ts": "^7.1.5", - "dom-serializer": "^2.0.0", - "htmlparser2": "^10.1.0", - "selderee": "~0.12.0" + "is-docker": "^2.0.0" }, "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/sponsors/KillyMXI" + "node": ">=8" } }, - "node_modules/htmlnano": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-3.2.0.tgz", - "integrity": "sha512-ZUCyB3w9EdpkaePGuYTBF7hT1MWdcz7y0/h2Cnl1MKLwbyj7aIM+Eq7SRg+ILF1mvR7zJKu06lw9zpgCxOFymw==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/relateurl": "^0.2.33", - "commander": "^14.0.0", - "cosmiconfig": "^9.0.0", - "posthtml": "^0.16.5" - }, - "bin": { - "htmlnano": "dist/bin.js" - }, - "peerDependencies": { - "cssnano": "^7.0.0", - "postcss": "^8.3.11", - "purgecss": "^8.0.0", - "relateurl": "^0.2.7", - "srcset": "^5.0.1", - "svgo": "^4.0.0", - "terser": "^5.21.0", - "uncss": "^0.17.3" - }, - "peerDependenciesMeta": { - "cssnano": { - "optional": true - }, - "postcss": { - "optional": true - }, - "purgecss": { - "optional": true - }, - "relateurl": { - "optional": true - }, - "srcset": { - "optional": true - }, - "svgo": { - "optional": true - }, - "terser": { - "optional": true - }, - "uncss": { - "optional": true - } + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isbot": { + "version": "5.1.43", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.43.tgz", + "integrity": "sha512-drJhFmibra4LO6Wd7D3Oi6UICRK9244vSZkmxzhlZP0TTdwCA2ueK4PEkUkzPYeuqug9+cqqdWPgihjk5+83Cg==", + "license": "Unlicense", + "engines": { + "node": ">=18" } }, - "node_modules/htmlnano/node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "license": "MIT", - "optional": true, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">=20" + "node": ">=8" } }, - "node_modules/htmlnano/node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", - "license": "MIT", - "optional": true, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=10" } }, - "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "optional": true, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" } }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "license": "BSD-2-Clause", - "optional": true, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, "engines": { - "node": ">=0.12" + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "engines": { + "node": ">=8" } }, - "node_modules/http_ece": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", - "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==", - "license": "MIT", + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "license": "ISC", "engines": { - "node": ">=16" + "node": ">=6" } }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" + "@isaacs/cliui": "^8.0.2" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "node_modules/jest": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", + "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", + "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "@jest/core": "30.4.2", + "@jest/types": "30.4.1", + "import-local": "^3.2.0", + "jest-cli": "30.4.2" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">= 14" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "node_modules/jest-changed-files": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", + "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", + "dev": true, "license": "MIT", "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" + "execa": "^5.1.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0" }, "engines": { - "node": ">= 14" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "license": "Apache-2.0", - "optional": true, + "node_modules/jest-circus": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", + "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.4.1", + "@jest/expect": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-runtime": "30.4.2", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "p-limit": "^3.1.0", + "pretty-format": "30.4.1", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, "engines": { - "node": ">=10.17.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/i18n-iso-countries": { - "version": "7.14.0", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz", - "integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==", + "node_modules/jest-cli": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", + "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", + "dev": true, "license": "MIT", "dependencies": { - "diacritics": "1.3.0" + "@jest/core": "30.4.2", + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">= 12" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "node_modules/jest-config": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", + "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", + "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.4.0", + "@jest/test-sequencer": "30.4.1", + "@jest/types": "30.4.1", + "babel-jest": "30.4.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-circus": "30.4.2", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-runner": "30.4.2", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "parse-json": "^5.2.0", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "node_modules/jest-config/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, "funding": [ { "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" + "url": "https://github.com/sponsors/sibiraj-s" } ], - "license": "BSD-3-Clause" + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "devOptional": true, + "node_modules/jest-diff": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", + "dev": true, "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.4.1" }, "engines": { - "node": ">=6" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", + "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/import-in-the-middle": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz", - "integrity": "sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==", - "license": "Apache-2.0", + "node_modules/jest-each": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", + "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", + "dev": true, + "license": "MIT", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^2.2.0", - "module-details-from-path": "^1.0.4" + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "jest-util": "30.4.1", + "pretty-format": "30.4.1" }, "engines": { - "node": ">=18" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC", - "optional": true - }, - "node_modules/ioredis": { - "version": "5.11.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.11.1.tgz", - "integrity": "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A==", + "node_modules/jest-environment-node": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", + "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", + "dev": true, "license": "MIT", "dependencies": { - "@ioredis/commands": "1.10.0", - "cluster-key-slot": "1.1.1", - "debug": "4.4.3", - "denque": "2.1.0", - "redis-errors": "1.2.0", - "redis-parser": "3.0.0", - "standard-as-callback": "2.1.0" + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-mock": "30.4.1", + "jest-util": "30.4.1", + "jest-validate": "30.4.1" }, "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/jest-haste-map": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", + "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.4.0", + "jest-util": "30.4.1", + "jest-worker": "30.4.1", + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, "engines": { - "node": ">= 0.10" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "dev": true, "license": "MIT", "dependencies": { - "binary-extensions": "^2.0.0" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.4.1", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "node_modules/jest-leak-detector": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", + "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "hasown": "^2.0.2" + "@jest/get-type": "30.1.0", + "pretty-format": "30.4.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "node_modules/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, "license": "MIT", - "optional": true, - "bin": { - "is-docker": "cli.js" + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-expression": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", - "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "acorn": "^7.1.1", - "object-assign": "^4.1.1" + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.4.1", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-expression/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", + "dev": true, "license": "MIT", - "optional": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" }, "engines": { - "node": ">=0.4.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jest-resolve": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", + "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", + "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.4.1", + "jest-validate": "30.4.1", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": ">=0.10.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "node_modules/jest-resolve-dependencies": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", + "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", "dev": true, "license": "MIT", + "dependencies": { + "jest-regex-util": "30.4.0", + "jest-snapshot": "30.4.1" + }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-json": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", - "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", - "license": "ISC", - "optional": true - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-runner": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", + "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", + "dev": true, "license": "MIT", + "dependencies": { + "@jest/console": "30.4.1", + "@jest/environment": "30.4.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.4.0", + "jest-environment-node": "30.4.1", + "jest-haste-map": "30.4.1", + "jest-leak-detector": "30.4.1", + "jest-message-util": "30.4.1", + "jest-resolve": "30.4.1", + "jest-runtime": "30.4.2", + "jest-util": "30.4.1", + "jest-watcher": "30.4.1", + "jest-worker": "30.4.1", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, "engines": { - "node": ">=0.12.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "license": "MIT", - "optional": true - }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", + "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.4.1", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-standalone-pwa": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", - "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - } - ], - "license": "MIT" + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "node_modules/jest-runtime": { + "version": "30.4.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", + "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", + "dev": true, "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.16" + "@jest/environment": "30.4.1", + "@jest/fake-timers": "30.4.1", + "@jest/globals": "30.4.1", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.5.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.4.1", + "jest-message-util": "30.4.1", + "jest-mock": "30.4.1", + "jest-regex-util": "30.4.0", + "jest-resolve": "30.4.1", + "jest-snapshot": "30.4.1", + "jest-util": "30.4.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "node_modules/jest-snapshot": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", + "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", + "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "is-docker": "^2.0.0" + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.4.1", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.4.1", + "@jest/transform": "30.4.1", + "@jest/types": "30.4.1", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.4.1", + "graceful-fs": "^4.2.11", + "jest-diff": "30.4.1", + "jest-matcher-utils": "30.4.1", + "jest-message-util": "30.4.1", + "jest-util": "30.4.1", + "pretty-format": "30.4.1", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/isbot": { - "version": "5.1.43", - "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.43.tgz", - "integrity": "sha512-drJhFmibra4LO6Wd7D3Oi6UICRK9244vSZkmxzhlZP0TTdwCA2ueK4PEkUkzPYeuqug9+cqqdWPgihjk5+83Cg==", - "license": "Unlicense", + "node_modules/jest-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", + "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.4.1", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, "engines": { - "node": ">=18" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/iterare": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", - "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", - "license": "ISC", + "node_modules/jest-util/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", + "node_modules/jest-validate": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", + "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", + "dev": true, + "license": "MIT", "dependencies": { - "@isaacs/cliui": "^8.0.2" + "@jest/get-type": "30.1.0", + "@jest/types": "30.4.1", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.4.1" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-watcher": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", + "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.4.1", + "@jest/types": "30.4.1", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.4.1", + "string-length": "^4.0.2" }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-worker": { @@ -8904,6 +11388,19 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-bigint": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", @@ -9158,6 +11655,16 @@ "url": "https://github.com/sponsors/KillyMXI" } }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/libbase64": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.3.0.tgz", @@ -9300,6 +11807,19 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -9370,8 +11890,8 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/lodash.once": { "version": "4.1.1", @@ -9480,12 +12000,45 @@ "tlds": "1.261.0" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/make-plural": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.5.0.tgz", "integrity": "sha512-0booA+aVYyVFoR67JBHdfVk0U08HmrBH2FrtmBqBa+NldlqXv/G2Z9VQuQq6Wgp2jDWdybEWGfBkk1cq5264WA==", "license": "Unicode-DFS-2016" }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10385,6 +12938,29 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -10625,6 +13201,13 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.44", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", @@ -10654,8 +13237,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "path-key": "^3.0.0" }, @@ -11017,6 +13600,51 @@ "node": ">=4" } }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-timeout": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", @@ -11030,6 +13658,16 @@ "node": ">=8" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/p-wait-for": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-3.2.0.tgz", @@ -11213,6 +13851,26 @@ "dev": true, "license": "MIT" }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -11314,6 +13972,29 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/playwright": { "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", @@ -12000,15 +14681,44 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "license": "MIT", - "optional": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, + "optional": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", + "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.4.1", + "ansi-styles": "^5.2.0", + "react-is-18": "npm:react-is@^18.3.1", + "react-is-19": "npm:react-is@^19.2.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=14" + "node": ">=10" }, "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/preview-email": { @@ -12275,6 +14985,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -12361,6 +15088,22 @@ "node": ">=0.10.0" } }, + "node_modules/react-is-18": { + "name": "react-is", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-is-19": { + "name": "react-is", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", + "integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -12490,6 +15233,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -13133,6 +15899,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/slick": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz", @@ -13281,7 +16057,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", @@ -13292,7 +16068,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -13335,6 +16111,19 @@ "node": ">=14" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -13388,6 +16177,43 @@ "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==", "license": "WTFPL OR MIT" }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -13510,12 +16336,25 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=6" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strtok3": { "version": "10.3.5", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", @@ -13635,6 +16474,22 @@ "node": ">=0.10" } }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -13717,7 +16572,7 @@ "version": "5.44.1", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -13808,9 +16663,70 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/tiny-lru": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-13.0.0.tgz", @@ -13875,6 +16791,13 @@ "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==", "license": "MIT" }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-buffer": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", @@ -13951,6 +16874,72 @@ "node": "10.* || >= 12.*" } }, + "node_modules/ts-jest": { + "version": "29.4.11", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.11.tgz", + "integrity": "sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.0", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-morph": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-13.0.3.tgz", @@ -14044,6 +17033,16 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -14245,6 +17244,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -14362,6 +17362,44 @@ "node": ">= 0.8" } }, + "node_modules/unrs-resolver": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", + "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.4" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.12.2", + "@unrs/resolver-binding-android-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-arm64": "1.12.2", + "@unrs/resolver-binding-darwin-x64": "1.12.2", + "@unrs/resolver-binding-freebsd-x64": "1.12.2", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", + "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", + "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", + "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", + "@unrs/resolver-binding-linux-x64-musl": "1.12.2", + "@unrs/resolver-binding-openharmony-arm64": "1.12.2", + "@unrs/resolver-binding-wasm32-wasi": "1.12.2", + "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", + "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", + "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -14438,6 +17476,21 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/valid-data-url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz", @@ -14486,6 +17539,16 @@ "node": "20 || >=22" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", @@ -14911,6 +17974,20 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", @@ -14941,6 +18018,13 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { "version": "1.10.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", @@ -14978,6 +18062,19 @@ "node": ">=12" } }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors-cjs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", diff --git a/backend/package.json b/backend/package.json index d887a3d56..88e1a246e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,7 @@ "lint": "oxlint apps/cloud/src apps/community/src", "lint:fix": "oxlint --fix apps/cloud/src apps/community/src", "lint:tsc": "tsc --noEmit", + "test:v2": "jest --config jest.v2.config.js", "find-deadcode": "ts-prune", "sentry:sourcemaps": "sentry-cli sourcemaps inject --org swetrix --project swetrix-api ./dist && sentry-cli sourcemaps upload --org swetrix --project swetrix-api ./dist", "knip": "knip" @@ -86,8 +87,8 @@ "php-serialize": "^5.1.3", "reflect-metadata": "^0.2.2", "request-filtering-agent": "^3.2.0", - "rrweb": "2.0.1", "rrvideo": "2.0.1", + "rrweb": "2.0.1", "rxjs": "^7.8.2", "sns-payload-validator": "^2.1.0", "socket.io": "^4.8.3", @@ -105,6 +106,7 @@ "@types/cookie-parser": "^1.4.10", "@types/express": "^5.0.6", "@types/ioredis": "^4.28.10", + "@types/jest": "^30.0.0", "@types/lodash": "^4.17.24", "@types/multer": "^2.1.0", "@types/node": "^26.0.0", @@ -112,10 +114,12 @@ "@types/passport-jwt": "^4.0.1", "@types/web-push": "^3.6.4", "cross-env": "^10.1.0", + "jest": "^30.4.2", "knip": "^6.17.1", "oxfmt": "^0.55.0", "oxlint": "^1.70.0", "rimraf": "^6.1.3", + "ts-jest": "^29.4.11", "ts-prune": "^0.10.3", "typescript": "^5.9.3" }, diff --git a/backend/tsconfig.build.json b/backend/tsconfig.build.json index 26c30d4ed..05e7663a1 100644 --- a/backend/tsconfig.build.json +++ b/backend/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist"] + "exclude": ["node_modules", "test", "dist", "**/*.spec.ts", "**/__tests__/**"] } diff --git a/docs/content/docs/api/meta.json b/docs/content/docs/api/meta.json index 958983eb7..d0853db6d 100644 --- a/docs/content/docs/api/meta.json +++ b/docs/content/docs/api/meta.json @@ -1,4 +1,4 @@ { "title": "API", - "pages": ["stats", "events", "admin"] + "pages": ["stats", "stats-v2", "events", "admin"] } diff --git a/docs/content/docs/api/stats-v2.mdx b/docs/content/docs/api/stats-v2.mdx new file mode 100644 index 000000000..46240fd58 --- /dev/null +++ b/docs/content/docs/api/stats-v2.mdx @@ -0,0 +1,694 @@ +--- +title: Statistics API v2 reference +slug: /statistics-api-v2 +--- + +The v2 statistics API is a redesigned, per-dimension read API for your analytics data. Instead of one monolithic response, each endpoint returns exactly one kind of data — a summary, a timeseries, or a breakdown by a single dimension — so you can fetch only what you need and compose your own dashboards. + +The v1 API ([Statistics API reference](/statistics-api)) keeps working; v2 is additive. + +## Authentication + +Each request must be authenticated with an API key using the `X-Api-Key` HTTP header. You can obtain an API key in your Swetrix [account settings](https://swetrix.com/user-settings). Public projects can be queried without a key. + +Rate limits are the same as for v1 and depend on your plan (Standard: 300 requests/hour, Plus: 6000 requests/hour, Enterprise: custom). + +## Concepts + +### Base URL and project scoping + +All v2 endpoints are scoped to a project by path: + +``` +https://api.swetrix.com/v2/projects/{pid}/... +``` + +### Response envelope + +Every endpoint returns the same envelope: + +```json +{ + "data": "", + "meta": { + "pid": "YOUR_PROJECT_ID", + "period": "7d", + "from": "2026-07-01T00:00:00+03:00", + "to": "2026-07-07T23:59:59+03:00", + "timezone": "Europe/Kyiv", + "appliedFilters": [] + } +} +``` + +`meta` echoes the resolved request parameters (the exact time bounds used, the applied filters, pagination, etc.), so you never have to guess how the API interpreted your query. + +### Periods and custom ranges + +- `period` — one of `1h`, `today`, `yesterday`, `1d`, `7d` (default), `4w`, `3M`, `12M`, `24M`, `all`. +- `from` + `to` — ISO 8601 dates (`2026-06-01`) or datetimes, always provided together. Mutually exclusive with `period`. +- `timezone` — any IANA timezone (default `Etc/GMT`). Applied to time bucketing and date boundaries. + +### Time buckets + +`timeBucket` is one of `minute`, `hour`, `day`, `month`, `year`. On v2 timeseries endpoints it is **optional** — when omitted, the API picks the finest bucket allowed for the requested range. For `period=all`, `meta.allowedTimeBuckets` lists the buckets you may request. + +### Metrics + +`metrics` is a comma-separated list of metric names, validated per data type. Each endpoint has sensible defaults. Discover all metrics for a data type via the [dimensions endpoint](#get-v2projectspiddimensions). + +| Data type | Metrics | +| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| traffic | `visitors` (unique sessions), `pageviews`, `users` (unique profiles; breakdown/summary only), `session_duration` (timeseries/summary only), `bounce_rate` (timeseries/summary only), `concurrency` (live visitors over time; timeseries only, zero-filled when filters are applied) | +| performance | `load_time`, `ttfb`, `dns`, `tls`, `connection`, `response`, `render`, `dom_load` — all in seconds, aggregated by `measure` | +| errors | `occurrences`, `affected_users` | +| captcha | `events` (breakdown), `generated`, `passed`, `failed`, `validation_failed`, `replayed` (timeseries/summary) | +| seo | `clicks`, `impressions`, `ctr` (percentage), `position` (average search position) — all four returned by default | + +### Dimensions + +Dimensions use human-readable names (mapped internally onto the v1 short codes): + +| Dimension | v1 code | Notes | +| ------------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------- | +| `country` | `cc` | ISO 3166-1 alpha-2 | +| `region` | `rg` | rows include `country`, `region_code` | +| `city` | `ct` | rows include `country` | +| `page` | `pg` | | +| `host` | `host` | | +| `locale` | `lc` | | +| `browser` | `br` | | +| `browser_version` | `brv` | rows include `browser` | +| `os` | `os` | | +| `os_version` | `osv` | rows include `os` | +| `device` | `dv` | | +| `referrer` | `ref` | | +| `referrer_name` | `refn` | filter-only (canonical root domain) | +| `utm_source` / `utm_medium` / `utm_campaign` / `utm_term` / `utm_content` | `so` / `me` / `ca` / `te` / `co` | | +| `isp` / `organization` / `user_type` / `connection_type` | `isp` / `og` / `ut` / `ctp` | | +| `entry_page` / `exit_page` | — | session-scoped virtual dimensions | +| `event` | `ev` | filter-only; use `/traffic/custom-events` for breakdowns | +| `event_metadata` | `ev:key:` | filter-only, takes a `key` | +| `page_property` | `tag:key:` | filter-only, takes a `key` | +| `error_name` / `error_message` / `error_filename` | — | errors, filter-only | +| `captcha_event` / `captcha_difficulty` / `captcha_reason` / `solve_time` | — | captcha only | +| `query` | `keywords` | seo only — the search query | + +Which dimensions apply to which data type is discoverable via [`GET /v2/projects/:pid/dimensions`](#get-v2projectspiddimensions). + +### Filters + +`filters` is a JSON array of filter objects with explicit operators: + +```json +[ + { "dimension": "country", "operator": "is", "value": ["US", "DE"] }, + { "dimension": "page", "operator": "contains", "value": "/blog" }, + { "dimension": "event_metadata", "key": "plan", "operator": "is_not", "value": "free" } +] +``` + +- **operator** — `is`, `is_not`, `contains`, `contains_not`. +- **value** — a string, `null` (matches unknown/unset values), or an array (values are OR-ed). +- **key** — required for `event_metadata` and `page_property` filters; selects the metadata key. + +Unknown dimensions or operators are rejected with a `422` and a message listing the supported values. + +### Pagination and sorting + +Breakdown and list endpoints accept `limit` (default 30) and `offset`. Breakdown responses include `meta.total` — the total number of dimension values — for pagination. Breakdowns also accept `sort=field:direction` where `field` is `value` or any selected metric and `direction` is `asc` or `desc` (default: first selected metric, descending). + +The `seo` data type is the exception on both counts: Google Search Console reports no row total and offers no sort control, so `/seo/breakdown` omits `meta.total` and always orders by clicks descending. Page through it until you get fewer rows than you asked for. + +### Measure (performance) + +Performance endpoints accept `measure`: `median` (default), `average`, `p75`, `p95`. The special `quantiles` measure is only meaningful on `/performance/timeseries`, where it returns `p50`/`p75`/`p95` of the total load time. + +## Traffic endpoints + +### GET /v2/projects/:pid/traffic/summary + +Key traffic metrics for the selected period, the previous period of the same length, and the change between them (the v2 equivalent of v1 `birdseye`, one project per request). + +```bash +curl 'https://api.swetrix.com/v2/projects/YOUR_PROJECT_ID/traffic/summary?period=7d' \ + -H "X-Api-Key: ${SWETRIX_API_KEY}" +``` + +```json title="Response" +{ + "data": { + "current": { + "visitors": 1043, + "pageviews": 2911, + "users": 402, + "bounce_rate": 43.1, + "session_duration": 74 + }, + "previous": { + "visitors": 980, + "pageviews": 2700, + "users": 371, + "bounce_rate": 45, + "session_duration": 69 + }, + "change": { + "visitors": 63, + "pageviews": 211, + "users": 31, + "bounce_rate": -1.9, + "session_duration": 5 + } + }, + "meta": { "pid": "YOUR_PROJECT_ID", "period": "7d", "timezone": "Etc/GMT", "appliedFilters": [] } +} +``` + +### GET /v2/projects/:pid/traffic/timeseries + +Traffic metrics grouped by time bucket, returned as self-describing rows with ISO 8601 timestamps (no index-aligned arrays). + +Parameters: `period`/`from`+`to`, `timezone`, `timeBucket` (optional), `metrics` (default `visitors,pageviews`; also `session_duration`, `bounce_rate`, `concurrency`), `mode` (`periodical` default, or `cumulative`), `filters`. + +```bash +curl 'https://api.swetrix.com/v2/projects/YOUR_PROJECT_ID/traffic/timeseries?period=7d&metrics=visitors,pageviews,bounce_rate&timezone=Europe/Kyiv' \ + -H "X-Api-Key: ${SWETRIX_API_KEY}" +``` + +```json title="Response" +{ + "data": [ + { + "timestamp": "2026-07-01T00:00:00+03:00", + "visitors": 120, + "pageviews": 340, + "bounce_rate": 41.2 + }, + { + "timestamp": "2026-07-02T00:00:00+03:00", + "visitors": 98, + "pageviews": 275, + "bounce_rate": 44 + } + ], + "meta": { + "pid": "YOUR_PROJECT_ID", + "period": "7d", + "timezone": "Europe/Kyiv", + "timeBucket": "day", + "allowedTimeBuckets": null, + "metrics": ["visitors", "pageviews", "bounce_rate"], + "mode": "periodical", + "appliedFilters": [] + } +} +``` + +The `concurrency` metric returns the number of concurrent live visitors over time (the "live visitors" series in the dashboard's traffic chart), reconstructed from session activity. It is only computed when explicitly requested. Because it is derived from session intervals — which carry no dimension data — it cannot be segmented: when `filters` are applied, or the requested range exceeds one year, the series is zero-filled. For the `minute` time bucket it is the exact number of visitors active during that minute; for larger buckets it is the peak concurrency within the bucket. + +### GET /v2/projects/:pid/traffic/breakdown + +Traffic metrics grouped by **one dimension** — the building block for custom dashboards. Paginated, sortable, multi-metric. + +Parameters: `dimension` (required), `metrics` (default `visitors,pageviews`), `limit` (max 100), `offset`, `sort`, plus the usual time range and `filters`. + +```bash +curl 'https://api.swetrix.com/v2/projects/YOUR_PROJECT_ID/traffic/breakdown?dimension=region&metrics=visitors,pageviews&period=4w&limit=2&sort=visitors:desc' \ + -H "X-Api-Key: ${SWETRIX_API_KEY}" +``` + +```json title="Response" +{ + "data": [ + { + "value": "Kyiv City", + "country": "UA", + "region_code": "30", + "visitors": 512, + "pageviews": 1204 + }, + { "value": "Bavaria", "country": "DE", "region_code": "BY", "visitors": 301, "pageviews": 700 } + ], + "meta": { + "pid": "YOUR_PROJECT_ID", + "dimension": "region", + "metrics": ["visitors", "pageviews"], + "total": 34, + "limit": 2, + "offset": 0, + "sort": "visitors:desc", + "period": "4w", + "timezone": "Etc/GMT", + "appliedFilters": [] + } +} +``` + +Notes: + +- `entry_page` and `exit_page` are session-scoped: they only support the `visitors` metric and cannot be combined with custom-event filters (the API returns a `422` explaining why, instead of silently returning empty data like v1). +- `session_duration` and `bounce_rate` are not computable per dimension value; request them on the timeseries or summary endpoints. +- When a custom-event filter (`event`, `event_metadata`) is applied, the base event set becomes custom events and `visitors` counts matched events (v1 semantics preserved). + +### GET /v2/projects/:pid/traffic/custom-events + +Counts of custom events in the selected period: `data: [{ "event": "signup", "count": 10 }, ...]` with `meta.total`, `limit`, `offset`. + +### GET /v2/projects/:pid/traffic/custom-events/timeseries + +Occurrence counts for specific custom events over time. `events` is a comma-separated list (or JSON array) of event names. + +```bash +curl 'https://api.swetrix.com/v2/projects/YOUR_PROJECT_ID/traffic/custom-events/timeseries?events=signup,purchase&period=7d' \ + -H "X-Api-Key: ${SWETRIX_API_KEY}" +``` + +```json title="Response (data)" +[ + { "timestamp": "2026-07-01T00:00:00Z", "signup": 4, "purchase": 1 }, + { "timestamp": "2026-07-02T00:00:00Z", "signup": 7, "purchase": 3 } +] +``` + +### GET /v2/projects/:pid/traffic/custom-events/metadata + +Aggregated metadata key/value counts for one custom event (v1 `/log/meta` equivalent). Requires `event`; returns `data: [{ "key": "Affiliate", "value": "Yes", "count": 1 }, ...]`. + +### GET /v2/projects/:pid/traffic/custom-metrics + +Sum / average of numeric custom event metadata values for the selected period and the previous one (powers dashboard "custom metric" cards). `metrics` is a JSON array of metric definitions (`{ "customEventName", "metaKey", "metaValueType": "integer" | "float" }`). Returns `data: [{ "key": "", "current": { "sum", "avg" }, "previous": { "sum", "avg" } }, ...]`. + +### GET /v2/projects/:pid/traffic/page-properties + +Counts of pageview metadata (page property) keys: `data: [{ "property": "author", "count": 30 }, ...]`. Cannot be combined with custom-event filters (`422`). + +### GET /v2/projects/:pid/traffic/page-properties/metadata + +Aggregated value counts for one page property (v1 `/log/property` equivalent). Requires `property`. + +## Performance endpoints + +### GET /v2/projects/:pid/performance/summary + +Aggregated `frontend` (render + DOM load), `network` (DNS + TLS + connection + response) and `backend` (TTFB) timings in seconds for the current and previous period, plus the change. Accepts `measure`. + +```json title="Response (data)" +{ + "current": { "frontend": 1.32, "network": 0.42, "backend": 0.14 }, + "previous": { "frontend": 1.11, "network": 0.16, "backend": 0.11 }, + "change": { "frontend": 0.21, "network": 0.26, "backend": 0.03 } +} +``` + +### GET /v2/projects/:pid/performance/timeseries + +Performance timings per time bucket (seconds). Default metrics: all of `dns`, `tls`, `connection`, `response`, `render`, `dom_load`, `ttfb`. With `measure=quantiles`, rows contain `p50`/`p75`/`p95` of the total load time instead. + +### GET /v2/projects/:pid/performance/breakdown + +Performance timings grouped by one dimension. Default metric: `load_time`. Accepts `measure`, `metrics`, `limit`/`offset`/`sort`. + +```bash +curl 'https://api.swetrix.com/v2/projects/YOUR_PROJECT_ID/performance/breakdown?dimension=country&metrics=load_time,ttfb&measure=p95&period=7d' \ + -H "X-Api-Key: ${SWETRIX_API_KEY}" +``` + +## Captcha endpoints + +### GET /v2/projects/:pid/captcha/summary + +`data: { "current": { generated, passed, passRate, solveP50, solveP75, solveP95, difficulty: [...], solveTime: [...] }, "previous": { ... } }`. `previous` is `null` for `period=all`. + +### GET /v2/projects/:pid/captcha/timeseries + +Captcha counters per time bucket. Metrics: `generated`, `passed`, `failed`, `validation_failed`, `replayed` (all by default). + +### GET /v2/projects/:pid/captcha/breakdown + +Captcha events grouped by `country`, `browser`, `os`, `device` (passed captchas only), or `captcha_event`, `captcha_difficulty`, `captcha_reason`, `solve_time`. + +## SEO endpoints + +SEO data comes from [Google Search Console](/integrations/google-search-console) rather than from your Swetrix events, so these endpoints behave a little differently from the rest of v2. The differences are all forced by the upstream API and are called out below. + +Search Console must be connected to the project and one of its properties linked. Until both are true, every `seo` endpoint except `/seo/status` returns `409 Conflict`: + +```json title="Response (409)" +{ + "statusCode": 409, + "message": "Google Search Console is not connected for this project" +} +``` + +Two further caveats worth knowing before you build against these: + +- **Data lags.** Search Console is typically 2–3 days behind, so recent days may be empty or incomplete. +- **Retention is ~16 months.** `period=all` resolves to the last 480 days rather than your project's full history. + +### GET /v2/projects/:pid/seo/status + +Whether the project can serve SEO data at all. Poll this before the others to tell "not connected" apart from "no data". + +```bash +curl 'https://api.swetrix.com/v2/projects/YOUR_PROJECT_ID/seo/status' \ + -H "X-Api-Key: ${SWETRIX_API_KEY}" +``` + +```json title="Response" +{ + "data": { "connected": true, "property": "sc-domain:example.com" }, + "meta": { "pid": "YOUR_PROJECT_ID" } +} +``` + +`connected` reports whether a Google account is linked; `property` is the Search Console property being queried, and is `null` when an account is connected but no property has been picked yet. Both must be set for the data endpoints to answer. + +### GET /v2/projects/:pid/seo/summary + +Clicks, impressions, CTR and average position for the selected period, the previous period of the same length, and the change between them. + +```bash +curl 'https://api.swetrix.com/v2/projects/YOUR_PROJECT_ID/seo/summary?period=7d' \ + -H "X-Api-Key: ${SWETRIX_API_KEY}" +``` + +```json title="Response" +{ + "data": { + "current": { "clicks": 412, "impressions": 18320, "ctr": 2.25, "position": 12.4 }, + "previous": { "clicks": 388, "impressions": 17010, "ctr": 2.28, "position": 13.1 }, + "change": { "clicks": 24, "impressions": 1310, "ctr": -0.03, "position": -0.7 } + }, + "meta": { + "pid": "YOUR_PROJECT_ID", + "metrics": ["clicks", "impressions", "ctr", "position"], + "period": "7d", + "timezone": "Etc/GMT", + "appliedFilters": [] + } +} +``` + +`change` is the absolute difference, as everywhere else in v2. Note that a _falling_ `position` is an improvement. + +### GET /v2/projects/:pid/seo/timeseries + +SEO metrics grouped by time bucket, as rows with ISO 8601 timestamps. + +Parameters: `period`/`from`+`to`, `timezone`, `timeBucket`, `metrics`, `filters`. + +Search Console reports nothing below hourly granularity, so `timeBucket` here accepts only `hour`, `day` (default), `month` and `year` — `minute` is rejected with a `422`. Hourly and daily rows come straight from Search Console; `month` and `year` are rolled up by Swetrix, summing clicks and impressions, recomputing CTR from those totals, and averaging position weighted by impressions (none of the three are additive). + +```bash +curl 'https://api.swetrix.com/v2/projects/YOUR_PROJECT_ID/seo/timeseries?period=7d&metrics=clicks,impressions&timezone=Europe/Kyiv' \ + -H "X-Api-Key: ${SWETRIX_API_KEY}" +``` + +```json title="Response" +{ + "data": [ + { "timestamp": "2026-07-01T00:00:00+03:00", "clicks": 61, "impressions": 2604 }, + { "timestamp": "2026-07-02T00:00:00+03:00", "clicks": 58, "impressions": 2511 } + ], + "meta": { + "pid": "YOUR_PROJECT_ID", + "metrics": ["clicks", "impressions"], + "timeBucket": "day", + "allowedTimeBuckets": ["hour", "day", "month", "year"], + "period": "7d", + "timezone": "Europe/Kyiv", + "appliedFilters": [] + } +} +``` + +### GET /v2/projects/:pid/seo/breakdown + +SEO metrics grouped by **one dimension** — `query`, `page`, `country` or `device`. + +Parameters: `dimension` (required), `metrics` (default: all four), `limit` (max 100), `offset`, plus the usual time range and `filters`. + +```bash +curl 'https://api.swetrix.com/v2/projects/YOUR_PROJECT_ID/seo/breakdown?dimension=query&period=4w&limit=2' \ + -H "X-Api-Key: ${SWETRIX_API_KEY}" +``` + +```json title="Response" +{ + "data": [ + { "value": "swetrix", "clicks": 184, "impressions": 2103, "ctr": 8.75, "position": 1.4 }, + { + "value": "google analytics alternative", + "clicks": 96, + "impressions": 8801, + "ctr": 1.09, + "position": 9.2 + } + ], + "meta": { + "pid": "YOUR_PROJECT_ID", + "dimension": "query", + "metrics": ["clicks", "impressions", "ctr", "position"], + "limit": 2, + "offset": 0, + "sort": "clicks:desc", + "period": "4w", + "timezone": "Etc/GMT", + "appliedFilters": [] + } +} +``` + +Notes: + +- **No `meta.total` and no `sort`.** Search Console neither counts matching rows nor lets you order them, so rows always come back by clicks descending. `sort` is accepted only as `clicks:desc`, so you can be explicit; anything else is a `422` rather than a silently ignored parameter. Page with `limit`/`offset` until a page comes back short. +- **`country` is alpha-2.** Search Console reports alpha-3 (`usa`); Swetrix normalises to the alpha-2 (`US`) that `country` means everywhere else in v2. +- Combine `dimension` with a filter to drill in — the queries a given page ranks for are `?dimension=query&filters=[{"dimension":"page","operator":"is","value":"https://example.com/blog"}]`, and the pages ranking for a query are the mirror image. + +### GET /v2/projects/:pid/seo/branded-traffic + +Clicks split by whether the search query contained one of the project's brand keywords (configured in project settings; Swetrix falls back to the project name and domain). + +```json title="Response (data)" +{ "branded": 260, "nonBranded": 152 } +``` + +Returns `data: null` with `meta.skipped: true` when the rollup was not run — see [skipped rollups](#skipped-rollups). + +### GET /v2/projects/:pid/seo/positions + +Impressions bucketed by search position, plus the daily count of ranking queries per position bucket. + +```json title="Response (data, truncated)" +{ + "impressionsByPosition": [ + { "key": "pos1To3", "label": "1-3", "impressions": 4120, "percentage": 22.5 }, + { "key": "pos4To10", "label": "4-10", "impressions": 7330, "percentage": 40 } + ], + "organicPositions": [ + { + "date": "2026-07-01", + "pos1To3": 12, + "pos4To10": 40, + "pos11To20": 61, + "pos21To50": 88, + "pos51Plus": 24 + } + ] +} +``` + +`impressionsByPosition` buckets are `pos1To3`, `pos4To10`, `pos11To20`, `pos21Plus`; `organicPositions` splits the tail further into `pos21To50` and `pos51Plus`. Returns `data: null` with `meta.skipped: true` when the rollup was not run. + +### Skipped rollups + +`/seo/branded-traffic` and `/seo/positions` have to page through every query in the range, which is slow and expensive against Search Console's quota. Rather than let them hang, Swetrix skips them and says so in `meta`: + +```json title="Response" +{ + "data": null, + "meta": { + "pid": "YOUR_PROJECT_ID", + "skipped": true, + "skippedReason": "range_too_large", + "maxRangeDays": 31, + "period": "3M", + "timezone": "Etc/GMT", + "appliedFilters": [] + } +} +``` + +- `skippedReason: "range_too_large"` — the range exceeds `meta.maxRangeDays` (31). Narrow the range. +- `skippedReason: "timeout"` — Search Console did not answer within `meta.timeoutMs`. Retry, or narrow the range. + +On a successful run `meta.skipped` is `false` and `data` is populated. A skip is not an error — an actual upstream failure still returns a `5xx`. + +## Error endpoints + +### GET /v2/projects/:pid/errors + +Paginated error groups: `data: [{ "eid", "name", "message", "filename", "count", "last_seen", "users", "sessions", "status" }, ...]`. `show_resolved=true` includes resolved groups. + +### GET /v2/projects/:pid/errors/overview + +Aggregated error statistics (occurrences, affected users/sessions and chart) for the period. + +### GET /v2/projects/:pid/errors/timeseries + +`occurrences` and `affected_users` per time bucket. + +### GET /v2/projects/:pid/errors/breakdown + +Error occurrences grouped by one dimension (`page`, `browser`, `os`, `country`, ...). Metrics: `occurrences` (default), `affected_users`. + +### GET /v2/projects/:pid/errors/:eid + +Details for one error group (metadata, occurrence chart, affected counts). Accepts the usual time range parameters and `timeBucket`. + +### GET /v2/projects/:pid/errors/:eid/sessions + +Paginated sessions affected by the error (`limit` max 50, default 10). + +## Session endpoints + +### GET /v2/projects/:pid/sessions + +Paginated sessions. `event_type` scopes the list to sessions with `traffic` (default), `performance`, or `error` events. Session rows use readable keys (`country`, `browser`, `os`, `duration`, ...): + +```json title="Response (data)" +[ + { + "psid": "14287087333426119778", + "country": "IS", + "os": "Windows", + "browser": "Firefox", + "pageviews": 4, + "sessionStart": "2026-07-05T16:09:06Z", + "lastActivity": "2026-07-05T17:22:02Z", + "isLive": 1, + "duration": 520 + } +] +``` + +### GET /v2/projects/:pid/sessions/:psid + +Details for one session: the page/event flow (`pages`), renamed session attributes (`details`), and an activity chart. + +## Profile endpoints + +### GET /v2/projects/:pid/profiles + +Paginated profiles. `profile_type` is `all` (default), `anonymous`, or `identified`. + +### GET /v2/projects/:pid/profiles/:profileId + +Profile details with `topPages` and `activityCalendar`. + +### GET /v2/projects/:pid/profiles/:profileId/sessions + +Paginated sessions for one profile. + +## Funnels + +### GET /v2/projects/:pid/funnel + +Funnel analysis. Provide `funnelId` (a saved funnel) **or** `steps` (a JSON array of pages / event names). Returns `data: { "steps": [...], "totalPageviews", "timeToConvert" }` where each step includes events, drop-off and per-step `breakdowns` (countries, devices, browsers, sources, campaigns, pages, profileTypes). + +```bash +curl 'https://api.swetrix.com/v2/projects/YOUR_PROJECT_ID/funnel?steps=["/","/pricing","/signup"]&period=7d' \ + -H "X-Api-Key: ${SWETRIX_API_KEY}" +``` + +### GET /v2/projects/:pid/funnel/sessions + +Paginated sessions that reached a funnel `step` (1-indexed). `dropoff=true` returns sessions that stopped at that step. + +## Live visitors + +### GET /v2/projects/:pid/live-visitors + +```json title="Response" +{ + "data": { + "count": 2, + "visitors": [ + { + "device": "desktop", + "browser": "Firefox", + "os": "Windows", + "country": "GB", + "psid": "9165978030580383830" + } + ] + }, + "meta": { "pid": "YOUR_PROJECT_ID", "windowMinutes": 5 } +} +``` + +## Discovery + +### GET /v2/projects/:pid/dimensions + +Machine-readable list of the dimensions and metrics available for a data type (`?type=traffic|performance|captcha|errors|seo`) — build dashboard UIs against this instead of hardcoding. + +```json title="Response (data, truncated)" +{ + "dimensions": [ + { + "name": "country", + "description": "Country (ISO 3166-1 alpha-2 code)", + "filterOnly": false, + "extraFields": [] + }, + { + "name": "region", + "description": "Region / subdivision", + "filterOnly": false, + "extraFields": ["country", "region_code"] + } + ], + "metrics": [ + { "name": "visitors", "description": "Unique sessions", "format": "integer", "default": true } + ] +} +``` + +### GET /v2/projects/:pid/dimensions/:dimension/values + +Distinct recorded values for a dimension — for filter autocompletion (`?type=traffic|errors`). `browser_version` and `os_version` return `{ "name", "version" }` pairs; everything else returns an array of strings. + +## Migrating from v1 + +| v1 | v2 | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| `GET /v1/log` (params panels) | `GET /v2/projects/:pid/traffic/breakdown?dimension=...` per panel | +| `GET /v1/log` / `/log/chart` (chart) | `GET /v2/projects/:pid/traffic/timeseries` | +| `GET /v1/log` (customs / properties) | `/traffic/custom-events`, `/traffic/page-properties` | +| `GET /v1/log/birdseye` | `GET /v2/projects/:pid/traffic/summary` (one project per request) | +| `GET /v1/log/performance` | `/performance/breakdown` + `/performance/timeseries` | +| `GET /v1/log/performance/birdseye` | `/performance/summary` | +| `GET /v1/log/captcha` | `/captcha/summary` + `/captcha/timeseries` + `/captcha/breakdown` | +| `GET /v1/log/meta` | `/traffic/custom-events/metadata` | +| `GET /v1/log/property` | `/traffic/page-properties/metadata` | +| `GET /v1/log/custom-events` | `/traffic/custom-events/timeseries` | +| `GET /v1/log/sessions` / `/log/session` | `/sessions` / `/sessions/:psid` | +| `GET /v1/log/profiles` / `/log/profile` / `/log/profile/sessions` | `/profiles` / `/profiles/:profileId` / `/profiles/:profileId/sessions` | +| `GET /v1/log/errors` / `/log/get-error` / `/log/error-overview` / `/log/error-sessions` | `/errors` / `/errors/:eid` / `/errors/overview` / `/errors/:eid/sessions` | +| `GET /v1/log/funnel` / `/log/funnel-sessions` | `/funnel` / `/funnel/sessions` | +| `GET /v1/log/live-visitors` + `/log/hb` | `/live-visitors` (merged) | +| `GET /v1/log/filters` / `/log/errors-filters` / `/log/filters/versions` | `/dimensions/:dimension/values` | +| `GET /v1/log/keywords` | `/seo/breakdown?dimension=query` | + +Key differences: + +1. **Filters** use `{ dimension, operator, value, key? }` with `is` / `is_not` / `contains` / `contains_not` instead of `{ column, filter, isExclusive, isContains }`. +2. **Names** are human-readable: `country` instead of `cc`, `visitors` instead of `uniques`, `pageviews` instead of `visits`, `session_duration` instead of `sdur`. +3. **Timeseries** return rows of objects with ISO 8601 timestamps instead of parallel arrays. +4. **Everything is wrapped** in `{ data, meta }` and breakdowns/lists are paginated server-side. +5. `take`/`skip` are now `limit`/`offset`. + +`GET /v1/log/keywords` still works, but it only ever returned the top queries. The `seo` endpoints cover the whole Search Console integration, so prefer them for new code. + +Still v1-only: bot statistics, session replays, `generalStats`, multi-project `birdseye` (`pids` array), and all data ingestion endpoints. diff --git a/docs/content/docs/api/stats.mdx b/docs/content/docs/api/stats.mdx index 495f1a420..03818bfba 100644 --- a/docs/content/docs/api/stats.mdx +++ b/docs/content/docs/api/stats.mdx @@ -3,6 +3,8 @@ title: Statistics API reference slug: /statistics-api --- +A newer, per-dimension [Statistics API v2](/statistics-api-v2) is available — it offers human-readable dimension and metric names, server-side pagination and a consistent response format. This v1 API keeps working as documented below. + Swetrix provides the ability to retrieve aggregated data not only through the UI, but also programmatically using API queries. The statistics API provides several GET endpoints. These endpoints require some query parameters and return standard HTTP responses and a JSON-encoded body. API requests without a valid API key will fail (except for the publicly available API endpoints). @@ -1255,19 +1257,6 @@ The number of sessions to return per page. The number of sessions to skip (for pagination). -### GET /v1/log/user-flow - -Returns the user flow diagram data, showing how users navigate through your website. - -```bash -curl 'https://api.swetrix.com/v1/log/user-flow?pid=YOUR_PROJECT_ID&period=7d'\ - -H "X-Api-Key: ${SWETRIX_API_KEY}" -``` - -#### Parameters - -The parameters are similar to the [`/log` endpoint](#get-v1log). - ### GET /v1/log/journeys Returns the most popular journeys — the ordered sequences of pages visitors viewed within a session. This is the data behind the Journeys tab of the dashboard. diff --git a/docs/tsconfig.tsbuildinfo b/docs/tsconfig.tsbuildinfo deleted file mode 100644 index 23fedb7e9..000000000 --- a/docs/tsconfig.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"fileNames":["./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/typescript/lib/lib.es2023.d.ts","./node_modules/typescript/lib/lib.es2024.d.ts","./node_modules/typescript/lib/lib.esnext.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/typescript/lib/lib.es2023.array.d.ts","./node_modules/typescript/lib/lib.es2023.collection.d.ts","./node_modules/typescript/lib/lib.es2023.intl.d.ts","./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2024.collection.d.ts","./node_modules/typescript/lib/lib.es2024.object.d.ts","./node_modules/typescript/lib/lib.es2024.promise.d.ts","./node_modules/typescript/lib/lib.es2024.regexp.d.ts","./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2024.string.d.ts","./node_modules/typescript/lib/lib.esnext.array.d.ts","./node_modules/typescript/lib/lib.esnext.collection.d.ts","./node_modules/typescript/lib/lib.esnext.intl.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.promise.d.ts","./node_modules/typescript/lib/lib.esnext.decorators.d.ts","./node_modules/typescript/lib/lib.esnext.iterator.d.ts","./node_modules/typescript/lib/lib.esnext.float16.d.ts","./node_modules/typescript/lib/lib.esnext.error.d.ts","./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/react/global.d.ts","./node_modules/csstype/index.d.ts","./node_modules/@types/react/index.d.ts","./node_modules/next/dist/styled-jsx/types/css.d.ts","./node_modules/next/dist/styled-jsx/types/macro.d.ts","./node_modules/next/dist/styled-jsx/types/style.d.ts","./node_modules/next/dist/styled-jsx/types/global.d.ts","./node_modules/next/dist/styled-jsx/types/index.d.ts","./node_modules/next/dist/server/get-page-files.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/blob.d.ts","./node_modules/@types/node/web-globals/console.d.ts","./node_modules/@types/node/web-globals/crypto.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/encoding.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/utility.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client-stats.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/round-robin-pool.d.ts","./node_modules/undici-types/h2c-client.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-call-history.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/snapshot-agent.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/cache-interceptor.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/importmeta.d.ts","./node_modules/@types/node/web-globals/messaging.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/performance.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/web-globals/streams.d.ts","./node_modules/@types/node/web-globals/timers.d.ts","./node_modules/@types/node/web-globals/url.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/inspector/promises.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/path/posix.d.ts","./node_modules/@types/node/path/win32.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/quic.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/test/reporters.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/util/types.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/@types/react/canary.d.ts","./node_modules/@types/react/experimental.d.ts","./node_modules/@types/react-dom/index.d.ts","./node_modules/@types/react-dom/canary.d.ts","./node_modules/@types/react-dom/experimental.d.ts","./node_modules/next/dist/lib/fallback.d.ts","./node_modules/next/dist/compiled/webpack/webpack.d.ts","./node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","./node_modules/next/dist/shared/lib/entry-constants.d.ts","./node_modules/next/dist/shared/lib/constants.d.ts","./node_modules/next/dist/lib/bundler.d.ts","./node_modules/next/dist/server/config.d.ts","./node_modules/next/dist/lib/load-custom-routes.d.ts","./node_modules/next/dist/shared/lib/image-config.d.ts","./node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","./node_modules/next/dist/server/body-streams.d.ts","./node_modules/next/dist/server/request/search-params.d.ts","./node_modules/next/dist/shared/lib/segment-cache/vary-params-decoding.d.ts","./node_modules/next/dist/server/app-render/vary-params.d.ts","./node_modules/next/dist/server/request/params.d.ts","./node_modules/next/dist/server/route-kind.d.ts","./node_modules/next/dist/server/route-definitions/route-definition.d.ts","./node_modules/next/dist/server/route-matches/route-match.d.ts","./node_modules/next/dist/client/components/app-router-headers.d.ts","./node_modules/next/dist/server/lib/cache-control.d.ts","./node_modules/next/dist/shared/lib/app-router-types.d.ts","./node_modules/next/dist/server/lib/cache-handlers/types.d.ts","./node_modules/next/dist/server/use-cache/use-cache-wrapper.d.ts","./node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","./node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","./node_modules/next/dist/lib/constants.d.ts","./node_modules/next/dist/server/render-result.d.ts","./node_modules/next/dist/server/response-cache/types.d.ts","./node_modules/next/dist/server/response-cache/index.d.ts","./node_modules/@types/react/jsx-runtime.d.ts","./node_modules/next/dist/next-devtools/userspace/pages/pages-dev-overlay-setup.d.ts","./node_modules/next/dist/build/static-paths/types.d.ts","./node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","./node_modules/next/dist/build/adapter/setup-node-env.external.d.ts","./node_modules/next/dist/server/instrumentation/types.d.ts","./node_modules/next/dist/lib/setup-exception-listeners.d.ts","./node_modules/next/dist/lib/worker.d.ts","./node_modules/next/dist/server/lib/experimental/ppr.d.ts","./node_modules/next/dist/lib/page-types.d.ts","./node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","./node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","./node_modules/next/dist/build/analysis/get-page-static-info.d.ts","./node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","./node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","./node_modules/next/dist/server/require-hook.d.ts","./node_modules/next/dist/server/node-polyfill-crypto.d.ts","./node_modules/next/dist/server/node-environment-baseline.d.ts","./node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-file.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-exit.d.ts","./node_modules/next/dist/server/node-environment-extensions/console-dim.external.d.ts","./node_modules/next/dist/server/node-environment-extensions/unhandled-rejection.external.d.ts","./node_modules/next/dist/server/node-environment-extensions/random.d.ts","./node_modules/next/dist/server/node-environment-extensions/date.d.ts","./node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","./node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","./node_modules/next/dist/server/node-environment-extensions/fast-set-immediate.external.d.ts","./node_modules/next/dist/server/node-environment.d.ts","./node_modules/next/dist/build/page-extensions-type.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","./node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","./node_modules/next/dist/server/lib/i18n-provider.d.ts","./node_modules/next/dist/server/web/next-url.d.ts","./node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","./node_modules/next/dist/server/web/spec-extension/cookies.d.ts","./node_modules/next/dist/server/web/spec-extension/request.d.ts","./node_modules/next/dist/shared/lib/deep-readonly.d.ts","./node_modules/next/dist/server/lib/incremental-cache/index.d.ts","./node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","./node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","./node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","./node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","./node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","./node_modules/next/dist/shared/lib/mitt.d.ts","./node_modules/next/dist/client/with-router.d.ts","./node_modules/next/dist/client/router.d.ts","./node_modules/next/dist/client/route-loader.d.ts","./node_modules/next/dist/client/page-loader.d.ts","./node_modules/next/dist/shared/lib/bloom-filter.d.ts","./node_modules/next/dist/shared/lib/router/router.d.ts","./node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","./node_modules/next/dist/client/components/readonly-url-search-params.d.ts","./node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","./node_modules/next/dist/client/flight-data-helpers.d.ts","./node_modules/next/dist/client/components/segment-cache/cache-key.d.ts","./node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","./node_modules/next/dist/client/components/segment-cache/types.d.ts","./node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.d.ts","./node_modules/next/dist/client/components/segment-cache/scheduler.d.ts","./node_modules/next/dist/client/components/segment-cache/cache-map.d.ts","./node_modules/next/dist/client/components/segment-cache/vary-path.d.ts","./node_modules/next/dist/client/components/segment-cache/cache.d.ts","./node_modules/next/dist/client/components/router-reducer/ppr-navigations.d.ts","./node_modules/next/dist/client/components/segment-cache/navigation.d.ts","./node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","./node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","./node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","./node_modules/next/dist/build/templates/pages.d.ts","./node_modules/next/dist/server/route-modules/pages/module.d.ts","./node_modules/next/dist/server/render.d.ts","./node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","./node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","./node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","./node_modules/next/dist/server/route-matchers/route-matcher.d.ts","./node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","./node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","./node_modules/next/dist/server/normalizers/normalizer.d.ts","./node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","./node_modules/next/dist/server/normalizers/request/suffix.d.ts","./node_modules/next/dist/server/normalizers/request/rsc.d.ts","./node_modules/next/dist/server/normalizers/request/next-data.d.ts","./node_modules/next/dist/server/after/builtin-request-context.d.ts","./node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.d.ts","./node_modules/next/dist/server/route-modules/pages/builtin/_error.d.ts","./node_modules/next/dist/server/load-default-error-components.d.ts","./node_modules/next/dist/server/base-server.d.ts","./node_modules/next/dist/server/after/after.d.ts","./node_modules/next/dist/server/after/after-context.d.ts","./node_modules/next/dist/server/use-cache/cache-life.d.ts","./node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","./node_modules/next/dist/server/lib/lazy-result.d.ts","./node_modules/next/dist/server/app-render/create-error-handler.d.ts","./node_modules/next/dist/shared/lib/action-revalidation-kind.d.ts","./node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","./node_modules/next/dist/server/async-storage/work-store.d.ts","./node_modules/next/dist/server/web/http.d.ts","./node_modules/next/dist/client/components/hooks-server-context.d.ts","./node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","./node_modules/next/dist/client/components/redirect-status-code.d.ts","./node_modules/next/dist/client/components/redirect-error.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","./node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","./node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","./node_modules/next/dist/server/app-render/cache-signal.d.ts","./node_modules/next/dist/server/app-render/instant-validation/boundary-tracking.d.ts","./node_modules/next/dist/server/app-render/instant-validation/instant-validation-error.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","./node_modules/next/dist/server/app-render/instant-validation/instant-samples.d.ts","./node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","./node_modules/next/dist/server/lib/implicit-tags.d.ts","./node_modules/next/dist/server/app-render/staged-rendering.d.ts","./node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","./node_modules/next/dist/build/templates/app-route.d.ts","./node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","./node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.d.ts","./node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","./node_modules/next/dist/build/segment-config/app/app-segments.d.ts","./node_modules/next/dist/build/get-supported-browsers.d.ts","./node_modules/next/dist/build/utils.d.ts","./node_modules/next/dist/build/rendering-mode.d.ts","./node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.d.ts","./node_modules/next/dist/server/lib/cpu-profile.d.ts","./node_modules/next/dist/build/turborepo-access-trace/types.d.ts","./node_modules/next/dist/build/turborepo-access-trace/result.d.ts","./node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","./node_modules/next/dist/build/turborepo-access-trace/index.d.ts","./node_modules/next/dist/export/routes/types.d.ts","./node_modules/next/dist/export/types.d.ts","./node_modules/next/dist/export/worker.d.ts","./node_modules/next/dist/build/worker.d.ts","./node_modules/next/dist/build/index.d.ts","./node_modules/next/dist/lib/coalesced-function.d.ts","./node_modules/next/dist/server/lib/router-utils/types.d.ts","./node_modules/next/dist/trace/types.d.ts","./node_modules/next/dist/trace/trace.d.ts","./node_modules/next/dist/trace/shared.d.ts","./node_modules/next/dist/trace/index.d.ts","./node_modules/next/dist/build/load-jsconfig.d.ts","./node_modules/@next/env/dist/index.d.ts","./node_modules/next/dist/build/webpack/plugins/telemetry-plugin/use-cache-tracker-utils.d.ts","./node_modules/next/dist/build/webpack/plugins/telemetry-plugin/telemetry-plugin.d.ts","./node_modules/next/dist/telemetry/storage.d.ts","./node_modules/next/dist/build/build-context.d.ts","./node_modules/next/dist/build/webpack-config.d.ts","./node_modules/next/dist/build/swc/generated-native.d.ts","./node_modules/next/dist/build/define-env.d.ts","./node_modules/next/dist/build/swc/index.d.ts","./node_modules/next/dist/build/swc/types.d.ts","./node_modules/next/dist/server/dev/parse-version-info.d.ts","./node_modules/next/dist/next-devtools/shared/types.d.ts","./node_modules/next/dist/server/dev/dev-indicator-server-state.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/cache-indicator.d.ts","./node_modules/next/dist/server/lib/parse-stack.d.ts","./node_modules/next/dist/next-devtools/server/shared.d.ts","./node_modules/next/dist/next-devtools/shared/stack-frame.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/utils/get-error-by-type.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/container/runtime-error/render-error.d.ts","./node_modules/next/dist/next-devtools/dev-overlay/shared.d.ts","./node_modules/next/dist/server/dev/debug-channel.d.ts","./node_modules/next/dist/server/dev/hot-reloader-types.d.ts","./node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","./node_modules/next/dist/server/web/spec-extension/response.d.ts","./node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","./node_modules/next/dist/server/web/types.d.ts","./node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","./node_modules/next/dist/server/base-http/node.d.ts","./node_modules/next/dist/server/lib/async-callback-set.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","./node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","./node_modules/sharp/lib/index.d.ts","./node_modules/next/dist/server/image-optimizer.d.ts","./node_modules/next/dist/server/next-server.d.ts","./node_modules/next/dist/server/lib/types.d.ts","./node_modules/next/dist/server/lib/lru-cache.d.ts","./node_modules/next/dist/server/lib/dev-bundler-service.d.ts","./node_modules/next/dist/server/dev/static-paths-worker.d.ts","./node_modules/next/dist/server/dev/next-dev-server.d.ts","./node_modules/next/dist/server/next.d.ts","./node_modules/next/dist/server/lib/render-server.d.ts","./node_modules/next/dist/server/lib/router-server.d.ts","./node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","./node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","./node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","./node_modules/next/dist/server/lib/router-utils/router-server-context.d.ts","./node_modules/next/dist/server/route-modules/route-module.d.ts","./node_modules/next/dist/server/load-components.d.ts","./node_modules/next/dist/server/web/adapter.d.ts","./node_modules/next/dist/server/app-render/types.d.ts","./node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","./node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","./node_modules/next/dist/server/lib/app-dir-module.d.ts","./node_modules/next/dist/server/app-render/app-render.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","./node_modules/next/dist/client/components/error-boundary.d.ts","./node_modules/next/dist/client/components/layout-router.d.ts","./node_modules/next/dist/client/components/render-from-template-context.d.ts","./node_modules/next/dist/client/components/client-page.d.ts","./node_modules/next/dist/client/components/client-segment.d.ts","./node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","./node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","./node_modules/next/dist/lib/metadata/types/extra-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","./node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","./node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","./node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","./node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","./node_modules/next/dist/lib/metadata/types/resolvers.d.ts","./node_modules/next/dist/lib/metadata/types/icons.d.ts","./node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","./node_modules/next/dist/lib/metadata/metadata.d.ts","./node_modules/next/dist/lib/framework/boundary-components.d.ts","./node_modules/next/dist/server/app-render/rsc/preloads.d.ts","./node_modules/next/dist/server/app-render/rsc/postpone.d.ts","./node_modules/next/dist/server/app-render/rsc/taint.d.ts","./node_modules/next/dist/server/app-render/collect-segment-data.d.ts","./node_modules/next/dist/server/app-render/instant-validation/instant-validation.d.ts","./node_modules/next/dist/next-devtools/userspace/app/segment-explorer-node.d.ts","./node_modules/next/dist/server/app-render/entry-base.d.ts","./node_modules/next/dist/build/templates/app-page.d.ts","./node_modules/next/dist/server/route-modules/app-page/helpers/prerender-manifest-matcher.d.ts","./node_modules/@types/react/jsx-dev-runtime.d.ts","./node_modules/@types/react/compiler-runtime.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.d.ts","./node_modules/@types/react-dom/client.d.ts","./node_modules/@types/react-dom/static.d.ts","./node_modules/@types/react-dom/server.d.ts","./node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.d.ts","./node_modules/next/dist/server/route-modules/app-page/module.d.ts","./node_modules/next/dist/server/request/fallback-params.d.ts","./node_modules/next/dist/server/web/spec-extension/image-response.d.ts","./node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","./node_modules/next/dist/server/web/spec-extension/url-pattern.d.ts","./node_modules/next/dist/server/after/index.d.ts","./node_modules/next/dist/server/request/connection.d.ts","./node_modules/next/dist/server/web/exports/index.d.ts","./node_modules/next/dist/server/request-meta.d.ts","./node_modules/next/dist/cli/next-test.d.ts","./node_modules/next/dist/shared/lib/size-limit.d.ts","./node_modules/next/dist/server/config-shared.d.ts","./node_modules/next/dist/server/base-http/index.d.ts","./node_modules/next/dist/server/api-utils/index.d.ts","./node_modules/next/dist/build/adapter/build-complete.d.ts","./node_modules/next/dist/types.d.ts","./node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","./node_modules/next/dist/shared/lib/utils.d.ts","./node_modules/next/dist/pages/_app.d.ts","./node_modules/next/app.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","./node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","./node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","./node_modules/next/dist/server/use-cache/cache-tag.d.ts","./node_modules/next/cache.d.ts","./node_modules/next/dist/pages/_document.d.ts","./node_modules/next/document.d.ts","./node_modules/next/dist/shared/lib/dynamic.d.ts","./node_modules/next/dynamic.d.ts","./node_modules/next/dist/pages/_error.d.ts","./node_modules/next/dist/client/components/catch-error.d.ts","./node_modules/next/dist/api/error.d.ts","./node_modules/next/error.d.ts","./node_modules/next/dist/shared/lib/head.d.ts","./node_modules/next/head.d.ts","./node_modules/next/dist/server/request/cookies.d.ts","./node_modules/next/dist/server/request/headers.d.ts","./node_modules/next/dist/server/request/draft-mode.d.ts","./node_modules/next/headers.d.ts","./node_modules/next/dist/shared/lib/get-img-props.d.ts","./node_modules/next/dist/client/image-component.d.ts","./node_modules/next/dist/shared/lib/image-external.d.ts","./node_modules/next/image.d.ts","./node_modules/next/dist/client/link.d.ts","./node_modules/next/link.d.ts","./node_modules/next/dist/client/components/unrecognized-action-error.d.ts","./node_modules/next/dist/client/components/redirect.d.ts","./node_modules/next/dist/client/components/not-found.d.ts","./node_modules/next/dist/client/components/forbidden.d.ts","./node_modules/next/dist/client/components/unauthorized.d.ts","./node_modules/next/dist/client/components/unstable-rethrow.server.d.ts","./node_modules/next/dist/client/components/unstable-rethrow.d.ts","./node_modules/next/dist/client/components/navigation.react-server.d.ts","./node_modules/next/dist/client/components/navigation.d.ts","./node_modules/next/navigation.d.ts","./node_modules/next/router.d.ts","./node_modules/next/dist/client/script.d.ts","./node_modules/next/script.d.ts","./node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","./node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","./node_modules/next/dist/compiled/@vercel/og/types.d.ts","./node_modules/next/server.d.ts","./node_modules/next/types/global.d.ts","./node_modules/next/types/compiled.d.ts","./node_modules/next/types.d.ts","./node_modules/next/index.d.ts","./node_modules/next/image-types/global.d.ts","./.next/dev/types/routes.d.ts","./next-env.d.ts","./node_modules/@types/unist/index.d.ts","./node_modules/@types/mdast/index.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/@types/estree-jsx/index.d.ts","./node_modules/@types/hast/index.d.ts","./node_modules/micromark-util-types/index.d.ts","./node_modules/mdast-util-from-markdown/lib/types.d.ts","./node_modules/mdast-util-from-markdown/lib/index.d.ts","./node_modules/mdast-util-from-markdown/index.d.ts","./node_modules/mdast-util-to-markdown/lib/types.d.ts","./node_modules/mdast-util-to-markdown/lib/index.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/blockquote.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/break.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/code.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/definition.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/emphasis.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/heading.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/html.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/image.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/image-reference.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/inline-code.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/link.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/link-reference.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/list.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/list-item.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/paragraph.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/root.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/strong.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/text.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/thematic-break.d.ts","./node_modules/mdast-util-to-markdown/lib/handle/index.d.ts","./node_modules/mdast-util-to-markdown/index.d.ts","./node_modules/mdast-util-mdx-expression/lib/index.d.ts","./node_modules/mdast-util-mdx-expression/index.d.ts","./node_modules/mdast-util-mdx-jsx/lib/index.d.ts","./node_modules/mdast-util-mdx-jsx/index.d.ts","./node_modules/mdast-util-mdxjs-esm/lib/index.d.ts","./node_modules/mdast-util-mdxjs-esm/index.d.ts","./node_modules/mdast-util-mdx/lib/index.d.ts","./node_modules/mdast-util-mdx/index.d.ts","./node_modules/vfile-message/lib/index.d.ts","./node_modules/vfile-message/index.d.ts","./node_modules/vfile/lib/index.d.ts","./node_modules/vfile/index.d.ts","./node_modules/unified/lib/callable-instance.d.ts","./node_modules/trough/lib/index.d.ts","./node_modules/trough/index.d.ts","./node_modules/unified/lib/index.d.ts","./node_modules/unified/index.d.ts","./node_modules/fumadocs-core/dist/stringifier-l_kdmdpb.d.ts","./node_modules/fumadocs-core/dist/remark-structure-c-gm2pcy.d.ts","./node_modules/fumadocs-core/dist/mdx-plugins/remark-structure.d.ts","./node_modules/fumadocs-core/dist/toc-cgnjpi4q.d.ts","./node_modules/fumadocs-core/dist/toc.d.ts","./node_modules/@types/mdx/types.d.ts","./node_modules/fumadocs-mdx/dist/types-drpz2vq2.d.ts","./node_modules/zod/v4/core/json-schema.d.cts","./node_modules/zod/v4/core/standard-schema.d.cts","./node_modules/zod/v4/core/registries.d.cts","./node_modules/zod/v4/core/to-json-schema.d.cts","./node_modules/zod/v4/core/util.d.cts","./node_modules/zod/v4/core/versions.d.cts","./node_modules/zod/v4/core/schemas.d.cts","./node_modules/zod/v4/core/checks.d.cts","./node_modules/zod/v4/core/errors.d.cts","./node_modules/zod/v4/core/core.d.cts","./node_modules/zod/v4/core/parse.d.cts","./node_modules/zod/v4/core/regexes.d.cts","./node_modules/zod/v4/locales/ar.d.cts","./node_modules/zod/v4/locales/az.d.cts","./node_modules/zod/v4/locales/be.d.cts","./node_modules/zod/v4/locales/bg.d.cts","./node_modules/zod/v4/locales/ca.d.cts","./node_modules/zod/v4/locales/cs.d.cts","./node_modules/zod/v4/locales/da.d.cts","./node_modules/zod/v4/locales/de.d.cts","./node_modules/zod/v4/locales/en.d.cts","./node_modules/zod/v4/locales/eo.d.cts","./node_modules/zod/v4/locales/es.d.cts","./node_modules/zod/v4/locales/fa.d.cts","./node_modules/zod/v4/locales/fi.d.cts","./node_modules/zod/v4/locales/fr.d.cts","./node_modules/zod/v4/locales/fr-ca.d.cts","./node_modules/zod/v4/locales/he.d.cts","./node_modules/zod/v4/locales/hu.d.cts","./node_modules/zod/v4/locales/hy.d.cts","./node_modules/zod/v4/locales/id.d.cts","./node_modules/zod/v4/locales/is.d.cts","./node_modules/zod/v4/locales/it.d.cts","./node_modules/zod/v4/locales/ja.d.cts","./node_modules/zod/v4/locales/ka.d.cts","./node_modules/zod/v4/locales/kh.d.cts","./node_modules/zod/v4/locales/km.d.cts","./node_modules/zod/v4/locales/ko.d.cts","./node_modules/zod/v4/locales/lt.d.cts","./node_modules/zod/v4/locales/mk.d.cts","./node_modules/zod/v4/locales/ms.d.cts","./node_modules/zod/v4/locales/nl.d.cts","./node_modules/zod/v4/locales/no.d.cts","./node_modules/zod/v4/locales/ota.d.cts","./node_modules/zod/v4/locales/ps.d.cts","./node_modules/zod/v4/locales/pl.d.cts","./node_modules/zod/v4/locales/pt.d.cts","./node_modules/zod/v4/locales/ru.d.cts","./node_modules/zod/v4/locales/sl.d.cts","./node_modules/zod/v4/locales/sv.d.cts","./node_modules/zod/v4/locales/ta.d.cts","./node_modules/zod/v4/locales/th.d.cts","./node_modules/zod/v4/locales/tr.d.cts","./node_modules/zod/v4/locales/ua.d.cts","./node_modules/zod/v4/locales/uk.d.cts","./node_modules/zod/v4/locales/ur.d.cts","./node_modules/zod/v4/locales/uz.d.cts","./node_modules/zod/v4/locales/vi.d.cts","./node_modules/zod/v4/locales/zh-cn.d.cts","./node_modules/zod/v4/locales/zh-tw.d.cts","./node_modules/zod/v4/locales/yo.d.cts","./node_modules/zod/v4/locales/index.d.cts","./node_modules/zod/v4/core/doc.d.cts","./node_modules/zod/v4/core/api.d.cts","./node_modules/zod/v4/core/json-schema-processors.d.cts","./node_modules/zod/v4/core/json-schema-generator.d.cts","./node_modules/zod/v4/core/index.d.cts","./node_modules/zod/v4/classic/errors.d.cts","./node_modules/zod/v4/classic/parse.d.cts","./node_modules/zod/v4/classic/schemas.d.cts","./node_modules/zod/v4/classic/checks.d.cts","./node_modules/zod/v4/classic/compat.d.cts","./node_modules/zod/v4/classic/from-json-schema.d.cts","./node_modules/zod/v4/classic/iso.d.cts","./node_modules/zod/v4/classic/coerce.d.cts","./node_modules/zod/v4/classic/external.d.cts","./node_modules/zod/index.d.cts","./node_modules/micromark-extension-gfm-footnote/lib/html.d.ts","./node_modules/micromark-extension-gfm-footnote/lib/syntax.d.ts","./node_modules/micromark-extension-gfm-footnote/index.d.ts","./node_modules/micromark-extension-gfm-strikethrough/lib/html.d.ts","./node_modules/micromark-extension-gfm-strikethrough/lib/syntax.d.ts","./node_modules/micromark-extension-gfm-strikethrough/index.d.ts","./node_modules/micromark-extension-gfm/index.d.ts","./node_modules/mdast-util-gfm-footnote/lib/index.d.ts","./node_modules/mdast-util-gfm-footnote/index.d.ts","./node_modules/markdown-table/index.d.ts","./node_modules/mdast-util-gfm-table/lib/index.d.ts","./node_modules/mdast-util-gfm-table/index.d.ts","./node_modules/mdast-util-gfm/lib/index.d.ts","./node_modules/mdast-util-gfm/index.d.ts","./node_modules/remark-gfm/lib/index.d.ts","./node_modules/remark-gfm/index.d.ts","./node_modules/fumadocs-core/dist/remark-gfm-dihbrjlx.d.ts","./node_modules/fumadocs-core/dist/types-dr0hqbxp.d.ts","./node_modules/@shikijs/vscode-textmate/dist/index.d.ts","./node_modules/@shikijs/types/dist/index.d.mts","./node_modules/shiki/dist/langs-bundle-full-c-zczmvu.d.mts","./node_modules/@shikijs/primitive/dist/index.d.mts","./node_modules/stringify-entities/lib/util/format-smart.d.ts","./node_modules/stringify-entities/lib/core.d.ts","./node_modules/stringify-entities/lib/index.d.ts","./node_modules/stringify-entities/index.d.ts","./node_modules/property-information/lib/util/info.d.ts","./node_modules/property-information/lib/find.d.ts","./node_modules/property-information/lib/hast-to-react.d.ts","./node_modules/property-information/lib/normalize.d.ts","./node_modules/property-information/index.d.ts","./node_modules/hast-util-to-html/lib/index.d.ts","./node_modules/hast-util-to-html/index.d.ts","./node_modules/@shikijs/core/dist/index.d.mts","./node_modules/shiki/dist/themes.d.mts","./node_modules/shiki/dist/bundle-full.d.mts","./node_modules/@shikijs/core/dist/types.d.mts","./node_modules/shiki/dist/types.d.mts","./node_modules/@shikijs/engine-oniguruma/dist/chunk-index.d.d.mts","./node_modules/@shikijs/engine-oniguruma/dist/index.d.mts","./node_modules/@shikijs/engine-javascript/dist/scanner-cfs1mv4a.d.mts","./node_modules/oniguruma-to-es/dist/esm/subclass.d.ts","./node_modules/oniguruma-to-es/dist/esm/index.d.ts","./node_modules/@shikijs/engine-javascript/dist/engine-compile.d.mts","./node_modules/@shikijs/engine-javascript/dist/engine-raw.d.mts","./node_modules/@shikijs/engine-javascript/dist/index.d.mts","./node_modules/shiki/dist/index.d.mts","./node_modules/fumadocs-core/dist/index-d-ukvvrz.d.ts","./node_modules/@shikijs/rehype/dist/types-wi2earpk.d.mts","./node_modules/@shikijs/rehype/dist/index.d.mts","./node_modules/fumadocs-core/dist/rehype-code.core-aamsxdqj.d.ts","./node_modules/fumadocs-core/dist/rehype-code-cmrth9lu.d.ts","./node_modules/fumadocs-core/dist/remark-image-dqljevny.d.ts","./node_modules/fumadocs-core/dist/remark-heading-95oqnnoh.d.ts","./node_modules/fumadocs-core/dist/remark-admonition-0gzm40zu.d.ts","./node_modules/fumadocs-core/dist/remark-directive-admonition-cxfikq_i.d.ts","./node_modules/fumadocs-core/dist/rehype-toc-iwxg6eli.d.ts","./node_modules/fumadocs-core/dist/remark-code-tab-dk79zzwn.d.ts","./node_modules/fumadocs-core/dist/remark-steps-dsnxm6xj.d.ts","./node_modules/fumadocs-core/dist/remark-npm-7sduqmc6.d.ts","./node_modules/fumadocs-core/dist/codeblock-utils-duhkfwet.d.ts","./node_modules/fumadocs-core/dist/remark-mdx-files-c50nfvny.d.ts","./node_modules/fumadocs-core/dist/remark-mdx-mermaid-bka2vurg.d.ts","./node_modules/fumadocs-core/dist/remark-feedback-block-cehhnqt0.d.ts","./node_modules/fumadocs-core/dist/remark-llms-bjyq1jdk.d.ts","./node_modules/fumadocs-core/dist/mdx-plugins/index.d.ts","./node_modules/source-map/source-map.d.ts","./node_modules/hast-util-to-estree/lib/handlers/comment.d.ts","./node_modules/hast-util-to-estree/lib/handlers/element.d.ts","./node_modules/hast-util-to-estree/lib/handlers/mdx-expression.d.ts","./node_modules/hast-util-to-estree/lib/handlers/mdx-jsx-element.d.ts","./node_modules/hast-util-to-estree/lib/handlers/mdxjs-esm.d.ts","./node_modules/hast-util-to-estree/lib/handlers/root.d.ts","./node_modules/hast-util-to-estree/lib/handlers/text.d.ts","./node_modules/hast-util-to-estree/lib/handlers/index.d.ts","./node_modules/hast-util-to-estree/lib/index.d.ts","./node_modules/hast-util-to-estree/lib/state.d.ts","./node_modules/hast-util-to-estree/index.d.ts","./node_modules/rehype-recma/lib/index.d.ts","./node_modules/rehype-recma/index.d.ts","./node_modules/mdast-util-to-hast/lib/state.d.ts","./node_modules/mdast-util-to-hast/lib/footer.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/blockquote.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/break.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/code.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/delete.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/emphasis.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/footnote-reference.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/heading.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/html.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/image-reference.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/image.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/inline-code.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/link-reference.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/link.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/list-item.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/list.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/paragraph.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/root.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/strong.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/table.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/table-cell.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/table-row.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/text.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/thematic-break.d.ts","./node_modules/mdast-util-to-hast/lib/handlers/index.d.ts","./node_modules/mdast-util-to-hast/lib/index.d.ts","./node_modules/mdast-util-to-hast/index.d.ts","./node_modules/remark-rehype/lib/index.d.ts","./node_modules/remark-rehype/index.d.ts","./node_modules/@mdx-js/mdx/lib/core.d.ts","./node_modules/@mdx-js/mdx/lib/node-types.d.ts","./node_modules/@mdx-js/mdx/lib/compile.d.ts","./node_modules/hast-util-to-jsx-runtime/lib/types.d.ts","./node_modules/hast-util-to-jsx-runtime/lib/index.d.ts","./node_modules/hast-util-to-jsx-runtime/index.d.ts","./node_modules/@mdx-js/mdx/lib/util/resolve-evaluate-options.d.ts","./node_modules/@mdx-js/mdx/lib/evaluate.d.ts","./node_modules/@mdx-js/mdx/lib/run.d.ts","./node_modules/@mdx-js/mdx/index.d.ts","./node_modules/readdirp/index.d.ts","./node_modules/chokidar/handler.d.ts","./node_modules/chokidar/index.d.ts","./node_modules/@standard-schema/spec/dist/index.d.ts","./node_modules/fumadocs-core/dist/definitions-j1yd3oze.d.ts","./node_modules/fumadocs-core/dist/index-dazunkmp.d.ts","./node_modules/fumadocs-core/dist/index-ca0yktyk2.d.ts","./node_modules/fumadocs-core/dist/index-dkqxqen8.d.ts","./node_modules/fumadocs-core/dist/source/index.d.ts","./node_modules/fumadocs-mdx/dist/core-clyeaiye.d.ts","./node_modules/fumadocs-mdx/dist/config/index.d.ts","./source.config.ts","./node_modules/fumadocs-mdx/dist/runtime/server.d.ts","./node_modules/fumadocs-mdx/dist/runtime/types.d.ts","./.source/server.ts","./node_modules/@phosphor-icons/react/dist/lib/types.d.ts","./node_modules/@phosphor-icons/react/dist/lib/context.d.ts","./node_modules/@phosphor-icons/react/dist/lib/iconbase.d.ts","./node_modules/@phosphor-icons/react/dist/lib/ssrbase.d.ts","./node_modules/@phosphor-icons/react/dist/lib/index.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/acorn.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/addressbook.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/addressbooktabs.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/airtrafficcontrol.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/airplane.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/airplaneinflight.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/airplanelanding.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/airplanetakeoff.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/airplanetaxiing.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/airplanetilt.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/airplay.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/alarm.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/alien.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/alignbottom.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/alignbottomsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/aligncenterhorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/aligncenterhorizontalsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/aligncentervertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/aligncenterverticalsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/alignleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/alignleftsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/alignright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/alignrightsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/aligntop.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/aligntopsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/amazonlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/ambulance.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/anchor.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/anchorsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/androidlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/angle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/angularlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/aperture.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/appstorelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/appwindow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/applelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/applepodcastslogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/approximateequals.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/archive.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/armchair.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowarcleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowarcright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowbenddoubleupleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowbenddoubleupright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowbenddownleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowbenddownright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowbendleftdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowbendleftup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowbendrightdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowbendrightup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowbendupleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowbendupright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowcircledown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowcircledownleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowcircledownright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowcircleleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowcircleright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowcircleup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowcircleupleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowcircleupright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowcounterclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowdownleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowdownright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowelbowdownleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowelbowdownright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowelbowleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowelbowleftdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowelbowleftup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowelbowright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowelbowrightdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowelbowrightup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowelbowupleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowelbowupright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatlinedown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatlineleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatlineright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatlineup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatlinesdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatlinesleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatlinesright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatlinesup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowfatup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowlinedown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowlinedownleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowlinedownright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowlineleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowlineright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowlineup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowlineupleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowlineupright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsquaredown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsquaredownleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsquaredownright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsquarein.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsquareleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsquareout.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsquareright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsquareup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsquareupleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsquareupright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowudownleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowudownright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowuleftdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowuleftup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowurightdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowurightup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowuupleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowuupright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowupleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowupright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowscounterclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsdownup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowshorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsin.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsincardinal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsinlinehorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsinlinevertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsinsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsleftright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsmerge.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsout.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsoutcardinal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsoutlinehorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsoutlinevertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsoutsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowssplit.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/arrowsvertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/article.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/articlemedium.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/articlenytimes.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/asclepius.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/asterisk.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/asterisksimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/at.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/atom.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/avocado.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/axe.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/baby.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/babycarriage.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/backpack.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/backspace.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bag.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bagsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/balloon.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bandaids.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bank.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/barbell.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/barcode.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/barn.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/barricade.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/baseball.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/baseballcap.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/baseballhelmet.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/basket.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/basketball.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bathtub.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batterycharging.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batterychargingvertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batteryempty.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batteryfull.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batteryhigh.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batterylow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batterymedium.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batteryplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batteryplusvertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batteryverticalempty.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batteryverticalfull.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batteryverticalhigh.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batteryverticallow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batteryverticalmedium.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batterywarning.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/batterywarningvertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/beachball.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/beanie.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bed.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/beerbottle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/beerstein.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/behancelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bell.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bellringing.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bellsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bellsimpleringing.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bellsimpleslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bellsimplez.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bellslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bellz.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/belt.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/beziercurve.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bicycle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/binary.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/binoculars.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/biohazard.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bird.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/blueprint.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bluetooth.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bluetoothconnected.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bluetoothslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bluetoothx.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/boat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bomb.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/book.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bookbookmark.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bookopen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bookopentext.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bookopenuser.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bookmark.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bookmarksimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bookmarks.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bookmarkssimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/books.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/boot.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/boules.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/boundingbox.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bowlfood.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bowlsteam.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bowlingball.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/boxarrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/boxarrowup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/boxingglove.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bracketsangle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bracketscurly.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bracketsround.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bracketssquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/brain.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/brandy.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bread.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bridge.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/briefcase.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/briefcasemetal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/broadcast.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/broom.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/browser.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/browsers.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bugbeetle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bug.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bugdroid.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/buildingapartment.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/building.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/buildingoffice.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/buildings.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bulldozer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/bus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/butterfly.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cablecar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cactus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cake.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calculator.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calendarblank.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calendar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calendarcheck.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calendardot.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calendardots.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calendarheart.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calendarminus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calendarplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calendarslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calendarstar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/calendarx.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/callbell.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/camera.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cameraplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/camerarotate.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cameraslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/campfire.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/carbattery.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/car.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/carprofile.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/carsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cardholder.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cards.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cardsthree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretcircledoubledown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretcircledoubleleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretcircledoubleright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretcircledoubleup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretcircledown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretcircleleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretcircleright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretcircleup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretcircleupdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretdoubledown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretdoubleleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretdoubleright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretdoubleup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretlinedown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretlineleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretlineright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretlineup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/caretupdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/carrot.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cashregister.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cassettetape.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/castleturret.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cellsignalfull.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cellsignalhigh.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cellsignallow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cellsignalmedium.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cellsignalnone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cellsignalslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cellsignalx.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/celltower.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/certificate.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chair.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chalkboard.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chalkboardsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chalkboardteacher.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/champagne.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chargingstation.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chartbar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chartbarhorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chartdonut.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chartline.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chartlinedown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chartlineup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chartpie.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chartpieslice.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chartpolar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chartscatter.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatcentered.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatcentereddots.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatcenteredslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatcenteredtext.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatcircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatcircledots.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatcircleslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatcircletext.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatdots.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatteardrop.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatteardropdots.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatteardropslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatteardroptext.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chattext.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chats.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatscircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chatsteardrop.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/check.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/checkcircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/checkfat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/checksquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/checksquareoffset.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/checkerboard.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/checks.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cheers.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cheese.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/chefhat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cherries.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/church.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cigarette.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cigaretteslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/circle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/circledashed.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/circlehalf.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/circlehalftilt.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/circlenotch.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/circlesfour.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/circlesthree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/circlesthreeplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/circuitry.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/city.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/clipboard.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/clipboardtext.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/clockafternoon.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/clock.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/clockclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/clockcountdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/clockcounterclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/clockuser.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/closedcaptioning.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudarrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudarrowup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloud.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudcheck.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudfog.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudlightning.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudmoon.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudrain.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudsnow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudsun.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudwarning.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cloudx.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/clover.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/club.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/coathanger.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/codalogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/codeblock.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/code.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/codesimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/codepenlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/codesandboxlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/coffeebean.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/coffee.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/coin.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/coinvertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/coins.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/columns.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/columnsplusleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/columnsplusright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/command.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/compass.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/compassrose.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/compasstool.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/computertower.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/confetti.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/contactlesspayment.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/control.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cookie.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cookingpot.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/copy.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/copysimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/copyleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/copyright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cornersin.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cornersout.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/couch.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/courtbasketball.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cowboyhat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cpu.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/crane.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cranetower.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/creditcard.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cricket.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/crop.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cross.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/crosshair.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/crosshairsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/crown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/crowncross.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/crownsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cube.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cubefocus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cubetransparent.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencybtc.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencycircledollar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencycny.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencydollar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencydollarsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencyeth.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencyeur.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencygbp.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencyinr.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencyjpy.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencykrw.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencykzt.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencyngn.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/currencyrub.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cursor.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cursorclick.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cursortext.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/cylinder.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/database.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/desk.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/desktop.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/desktoptower.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/detective.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/devtologo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/devicemobile.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/devicemobilecamera.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/devicemobileslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/devicemobilespeaker.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/devicerotate.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/devicetablet.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/devicetabletcamera.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/devicetabletspeaker.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/devices.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/diamond.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/diamondsfour.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dicefive.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dicefour.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/diceone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dicesix.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dicethree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dicetwo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/disc.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/discoball.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/discordlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/divide.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dna.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dog.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/door.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dooropen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dot.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dotoutline.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dotsnine.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dotssix.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dotssixvertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dotsthree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dotsthreecircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dotsthreecirclevertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dotsthreeoutline.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dotsthreeoutlinevertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dotsthreevertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/download.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/downloadsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dress.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dresser.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dribbblelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/drone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/drop.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/drophalf.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/drophalfbottom.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dropsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dropslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/dropboxlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/ear.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/earslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/egg.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/eggcrack.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/eject.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/ejectsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/elevator.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/empty.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/engine.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/envelope.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/envelopeopen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/envelopesimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/envelopesimpleopen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/equalizer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/equals.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/eraser.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/escalatordown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/escalatorup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/exam.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/exclamationmark.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/exclude.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/excludesquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/export.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/eye.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/eyeclosed.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/eyeslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/eyedropper.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/eyedroppersample.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/eyeglasses.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/eyes.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/facemask.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/facebooklogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/factory.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/faders.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fadershorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/falloutshelter.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fan.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/farm.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fastforward.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fastforwardcircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/feather.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fediverselogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/figmalogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filearchive.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filearrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filearrowup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fileaudio.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/file.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filec.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filecsharp.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filecloud.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filecode.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filecpp.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filecss.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filecsv.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filedashed.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filedoc.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filehtml.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fileimage.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fileini.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filejpg.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filejs.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filejsx.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filelock.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filemagnifyingglass.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filemd.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fileminus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filepdf.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fileplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filepng.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fileppt.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filepy.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filers.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filesql.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filesvg.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filetext.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filets.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filetsx.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filetxt.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filevideo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filevue.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filex.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filexls.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filezip.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/files.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filmreel.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filmscript.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filmslate.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/filmstrip.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fingerprint.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fingerprintsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/finnthehuman.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fire.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fireextinguisher.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/firesimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/firetruck.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/firstaid.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/firstaidkit.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fish.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fishsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flagbanner.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flagbannerfold.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flag.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flagcheckered.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flagpennant.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flame.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flashlight.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flask.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fliphorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flipvertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/floppydiskback.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/floppydisk.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flowarrow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flower.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flowerlotus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flowertulip.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/flyingsaucer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/folder.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/folderdashed.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/folderlock.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/folderminus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/folderopen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/folderplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/foldersimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/foldersimpledashed.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/foldersimplelock.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/foldersimpleminus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/foldersimpleplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/foldersimplestar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/foldersimpleuser.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/folderstar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/folderuser.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/folders.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/football.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/footballhelmet.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/footprints.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/forkknife.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/fourk.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/framecorners.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/framerlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/function.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/funnel.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/funnelsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/funnelsimplex.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/funnelx.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gamecontroller.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/garage.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gascan.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gaspump.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gauge.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gavel.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gear.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gearfine.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gearsix.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/genderfemale.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/genderintersex.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gendermale.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/genderneuter.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gendernonbinary.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gendertransgender.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/ghost.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gif.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gift.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gitbranch.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gitcommit.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gitdiff.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gitfork.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gitmerge.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gitpullrequest.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/githublogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gitlablogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gitlablogosimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/globe.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/globehemisphereeast.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/globehemispherewest.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/globesimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/globesimplex.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/globestand.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/globex.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/goggles.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/golf.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/goodreadslogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/googlecardboardlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/googlechromelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/googledrivelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/googlelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/googlephotoslogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/googleplaylogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/googlepodcastslogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gps.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gpsfix.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gpsslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gradient.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/graduationcap.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/grains.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/grainsslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/graph.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/graphicscard.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/greaterthan.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/greaterthanorequal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gridfour.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/gridnine.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/guitar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hairdryer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hamburger.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hammer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handarrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handarrowup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hand.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handcoins.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handdeposit.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handeye.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handfist.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handgrabbing.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handheart.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handpalm.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handpeace.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handpointing.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handsoap.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handswipeleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handswiperight.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handtap.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handwaving.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handwithdraw.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handbag.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handbagsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handsclapping.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handspraying.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/handshake.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/harddrive.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/harddrives.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hardhat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hashstraight.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/headcircuit.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/headlights.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/headphones.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/headset.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/heart.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/heartbreak.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hearthalf.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/heartstraight.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/heartstraightbreak.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/heartbeat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hexagon.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/highdefinition.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/highheel.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/highlighter.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/highlightercircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hockey.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hoodie.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/horse.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hospital.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hourglass.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hourglasshigh.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hourglasslow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hourglassmedium.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hourglasssimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hourglasssimplehigh.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hourglasssimplelow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hourglasssimplemedium.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/house.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/houseline.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/housesimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/hurricane.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/icecream.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/identificationbadge.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/identificationcard.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/image.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/imagebroken.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/imagesquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/images.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/imagessquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/infinity.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/info.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/instagramlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/intersect.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/intersectsquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/intersectthree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/intersection.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/invoice.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/island.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/jar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/jarlabel.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/jeep.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/joystick.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/kanban.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/key.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/keyreturn.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/keyboard.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/keyhole.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/knife.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/ladder.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/laddersimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lamp.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lamppendant.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/laptop.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lasso.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lastfmlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/layout.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/leaf.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lectern.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lego.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/legosmiley.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lessthan.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lessthanorequal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lettercircleh.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lettercirclep.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lettercirclev.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lifebuoy.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lightbulb.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lightbulbfilament.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lighthouse.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lightninga.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lightning.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lightningslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/linesegment.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/linesegments.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/linevertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/link.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/linkbreak.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/linksimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/linksimplebreak.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/linksimplehorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/linksimplehorizontalbreak.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/linkedinlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/linktreelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/linuxlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/list.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/listbullets.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/listchecks.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/listdashes.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/listheart.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/listmagnifyingglass.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/listnumbers.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/listplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/liststar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lock.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lockkey.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lockkeyopen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/locklaminated.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/locklaminatedopen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lockopen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/locksimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/locksimpleopen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/lockers.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/log.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/magicwand.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/magnet.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/magnetstraight.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/magnifyingglass.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/magnifyingglassminus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/magnifyingglassplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mailbox.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mappinarea.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mappin.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mappinline.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mappinplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mappinsimplearea.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mappinsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mappinsimpleline.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/maptrifold.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/markdownlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/markercircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/martini.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/maskhappy.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/masksad.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mastodonlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mathoperations.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/matrixlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/medal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/medalmilitary.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mediumlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/megaphone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/megaphonesimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/memberof.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/memory.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/messengerlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/metalogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/meteor.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/metronome.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/microphone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/microphoneslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/microphonestage.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/microscope.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/microsoftexcellogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/microsoftoutlooklogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/microsoftpowerpointlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/microsoftteamslogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/microsoftwordlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/minus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/minuscircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/minussquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/money.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/moneywavy.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/monitorarrowup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/monitor.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/monitorplay.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/moon.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/moonstars.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/moped.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mopedfront.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mosque.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/motorcycle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mountains.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mouse.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mouseleftclick.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mousemiddleclick.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mouserightclick.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mousescroll.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/mousesimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/musicnote.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/musicnotesimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/musicnotes.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/musicnotesminus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/musicnotesplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/musicnotessimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/navigationarrow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/needle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/network.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/networkslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/networkx.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/newspaper.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/newspaperclipping.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/notequals.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/notmemberof.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/notsubsetof.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/notsupersetof.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/notches.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/noteblank.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/note.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/notepencil.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/notebook.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/notepad.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/notification.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/notionlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/nuclearplant.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbercircleeight.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbercirclefive.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbercirclefour.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbercirclenine.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbercircleone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbercircleseven.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbercirclesix.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbercirclethree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbercircletwo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbercirclezero.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbereight.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numberfive.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numberfour.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbernine.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numberone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numberseven.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbersix.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbersquareeight.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbersquarefive.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbersquarefour.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbersquarenine.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbersquareone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbersquareseven.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbersquaresix.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbersquarethree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbersquaretwo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbersquarezero.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numberthree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numbertwo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numberzero.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/numpad.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/nut.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/nytimeslogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/octagon.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/officechair.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/onigiri.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/openailogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/option.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/orange.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/orangeslice.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/oven.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/package.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/paintbrush.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/paintbrushbroad.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/paintbrushhousehold.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/paintbucket.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/paintroller.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/palette.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/panorama.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pants.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/paperplane.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/paperplaneright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/paperplanetilt.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/paperclip.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/papercliphorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/parachute.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/paragraph.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/parallelogram.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/park.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/password.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/path.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/patreonlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pause.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pausecircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pawprint.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/paypallogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/peace.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pennib.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pennibstraight.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pencil.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pencilcircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pencilline.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pencilruler.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pencilsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pencilsimpleline.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pencilsimpleslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pencilslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pentagon.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pentagram.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pepper.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/percent.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personarmsspread.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/person.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personsimplebike.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personsimplecircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personsimplehike.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personsimplerun.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personsimpleski.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personsimplesnowboard.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personsimpleswim.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personsimpletaichi.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personsimplethrow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/personsimplewalk.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/perspective.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phonecall.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phonedisconnect.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phoneincoming.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phonelist.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phoneoutgoing.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phonepause.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phoneplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phoneslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phonetransfer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phonex.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/phosphorlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pi.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pianokeys.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/picnictable.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pictureinpicture.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/piggybank.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pill.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pingpong.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pintglass.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pinterestlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pinwheel.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pipe.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pipewrench.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pixlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pizza.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/placeholder.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/planet.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/plant.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/play.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/playcircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/playpause.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/playlist.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/plug.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/plugcharging.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/plugs.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/plugsconnected.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/plus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pluscircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/plusminus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/plussquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pokerchip.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/policecar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/polygon.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/popcorn.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/popsicle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pottedplant.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/power.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/prescription.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/presentation.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/presentationchart.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/printer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/prohibit.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/prohibitinset.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/projectorscreen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/projectorscreenchart.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pulse.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pushpin.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pushpinsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pushpinsimpleslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/pushpinslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/puzzlepiece.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/qrcode.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/question.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/questionmark.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/queue.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/quotes.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rabbit.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/racquet.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/radical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/radio.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/radiobutton.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/radioactive.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rainbow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rainbowcloud.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/ranking.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/readcvlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/receipt.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/receiptx.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/record.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rectangle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rectangledashed.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/recycle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/redditlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/repeat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/repeatonce.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/replitlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/resize.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rewind.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rewindcircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/roadhorizon.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/robot.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rocket.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rocketlaunch.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rows.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rowsplusbottom.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rowsplustop.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rss.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rsssimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/rug.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/ruler.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sailboat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/scales.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/scan.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/scansmiley.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/scissors.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/scooter.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/screencast.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/screwdriver.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/scribble.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/scribbleloop.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/scroll.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/seal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sealcheck.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sealpercent.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sealquestion.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sealwarning.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/seat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/seatbelt.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/securitycamera.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/selectionall.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/selectionbackground.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/selection.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/selectionforeground.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/selectioninverse.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/selectionplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/selectionslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shapes.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/share.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sharefat.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sharenetwork.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shield.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shieldcheck.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shieldcheckered.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shieldchevron.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shieldplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shieldslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shieldstar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shieldwarning.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shippingcontainer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shirtfolded.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shootingstar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shoppingbag.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shoppingbagopen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shoppingcart.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shoppingcartsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shovel.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shower.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shrimp.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shuffleangular.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shuffle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/shufflesimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sidebar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sidebarsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sigma.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/signin.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/signout.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/signature.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/signpost.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/simcard.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/siren.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sketchlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/skipback.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/skipbackcircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/skipforward.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/skipforwardcircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/skull.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/skypelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/slacklogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sliders.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/slidershorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/slideshow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/smileyangry.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/smileyblank.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/smiley.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/smileymeh.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/smileymelting.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/smileynervous.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/smileysad.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/smileysticker.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/smileywink.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/smileyxeyes.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/snapchatlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sneaker.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sneakermove.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/snowflake.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/soccerball.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sock.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/solarpanel.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/solarroof.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sortascending.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sortdescending.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/soundcloudlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/spade.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sparkle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speakerhifi.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speakerhigh.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speakerlow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speakernone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speakersimplehigh.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speakersimplelow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speakersimplenone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speakersimpleslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speakersimplex.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speakerslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speakerx.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/speedometer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sphere.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/spinnerball.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/spinner.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/spinnergap.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/spiral.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/splithorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/splitvertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/spotifylogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/spraybottle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/square.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/squarehalf.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/squarehalfbottom.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/squarelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/squaresplithorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/squaresplitvertical.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/squaresfour.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stack.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stackminus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stackoverflowlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stackplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stacksimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stairs.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stamp.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/standarddefinition.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/starandcrescent.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/star.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/starfour.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/starhalf.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/starofdavid.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/steamlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/steeringwheel.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/steps.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stethoscope.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sticker.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stool.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stop.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stopcircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/storefront.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/strategy.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/stripelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/student.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/subsetof.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/subsetproperof.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/subtitles.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/subtitlesslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/subtract.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/subtractsquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/subway.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/suitcase.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/suitcaserolling.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/suitcasesimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sun.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sundim.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sunhorizon.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sunglasses.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/supersetof.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/supersetproperof.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/swap.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/swatches.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/swimmingpool.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/sword.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/synagogue.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/syringe.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tshirt.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/table.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tabs.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tag.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tagchevron.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tagsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/target.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/taxi.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/teabag.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/telegramlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/television.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/televisionsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tennisball.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tent.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/terminal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/terminalwindow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/testtube.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textaunderline.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textaa.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textaligncenter.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textalignjustify.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textalignleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textalignright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textb.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textcolumns.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/texth.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/texthfive.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/texthfour.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/texthone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/texthsix.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/texththree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/texthtwo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textindent.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textitalic.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textoutdent.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textstrikethrough.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textsubscript.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textsuperscript.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textt.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/texttslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textunderline.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/textbox.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/thermometer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/thermometercold.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/thermometerhot.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/thermometersimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/threadslogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/threed.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/thumbsdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/thumbsup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/ticket.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tidallogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tiktoklogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tilde.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/timer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tipjar.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tipi.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tire.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/toggleleft.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/toggleright.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/toilet.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/toiletpaper.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/toolbox.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tooth.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tornado.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tote.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/totesimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/towel.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tractor.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trademark.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trademarkregistered.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trafficcone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trafficsign.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trafficsignal.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/train.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trainregional.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trainsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tram.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/translate.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trashsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trayarrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trayarrowup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tray.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/treasurechest.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/treeevergreen.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/treepalm.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/treestructure.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/treeview.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trenddown.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trendup.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/triangle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/triangledashed.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trolley.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trolleysuitcase.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trophy.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/truck.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/trucktrailer.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/tumblrlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/twitchlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/twitterlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/umbrella.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/umbrellasimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/union.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/unite.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/unitesquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/upload.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/uploadsimple.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usb.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/user.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usercheck.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usercircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usercirclecheck.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usercircledashed.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usercirclegear.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usercircleminus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usercircleplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/userfocus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usergear.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/userlist.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/userminus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/userplus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/userrectangle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usersound.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usersquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/userswitch.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/users.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usersfour.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/usersthree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/van.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/vault.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/vectorthree.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/vectortwo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/vibrate.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/video.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/videocamera.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/videocameraslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/videoconference.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/vignette.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/vinylrecord.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/virtualreality.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/virus.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/visor.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/voicemail.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/volleyball.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wall.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wallet.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/warehouse.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/warning.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/warningcircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/warningdiamond.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/warningoctagon.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/washingmachine.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/watch.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wavesawtooth.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wavesine.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wavesquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wavetriangle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/waveform.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/waveformslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/waves.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/webcam.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/webcamslash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/webhookslogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wechatlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/whatsapplogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wheelchair.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wheelchairmotion.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wifihigh.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wifilow.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wifimedium.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wifinone.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wifislash.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wifix.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wind.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/windmill.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/windowslogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wine.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/wrench.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/x.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/xcircle.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/xlogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/xsquare.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/yarn.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/yinyang.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/youtubelogo.d.ts","./node_modules/@phosphor-icons/react/dist/ssr/index.d.ts","./node_modules/@phosphor-icons/react/dist/csr/acorn.d.ts","./node_modules/@phosphor-icons/react/dist/csr/addressbook.d.ts","./node_modules/@phosphor-icons/react/dist/csr/addressbooktabs.d.ts","./node_modules/@phosphor-icons/react/dist/csr/airtrafficcontrol.d.ts","./node_modules/@phosphor-icons/react/dist/csr/airplane.d.ts","./node_modules/@phosphor-icons/react/dist/csr/airplaneinflight.d.ts","./node_modules/@phosphor-icons/react/dist/csr/airplanelanding.d.ts","./node_modules/@phosphor-icons/react/dist/csr/airplanetakeoff.d.ts","./node_modules/@phosphor-icons/react/dist/csr/airplanetaxiing.d.ts","./node_modules/@phosphor-icons/react/dist/csr/airplanetilt.d.ts","./node_modules/@phosphor-icons/react/dist/csr/airplay.d.ts","./node_modules/@phosphor-icons/react/dist/csr/alarm.d.ts","./node_modules/@phosphor-icons/react/dist/csr/alien.d.ts","./node_modules/@phosphor-icons/react/dist/csr/alignbottom.d.ts","./node_modules/@phosphor-icons/react/dist/csr/alignbottomsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/aligncenterhorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/aligncenterhorizontalsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/aligncentervertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/aligncenterverticalsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/alignleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/alignleftsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/alignright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/alignrightsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/aligntop.d.ts","./node_modules/@phosphor-icons/react/dist/csr/aligntopsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/amazonlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/ambulance.d.ts","./node_modules/@phosphor-icons/react/dist/csr/anchor.d.ts","./node_modules/@phosphor-icons/react/dist/csr/anchorsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/androidlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/angle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/angularlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/aperture.d.ts","./node_modules/@phosphor-icons/react/dist/csr/appstorelogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/appwindow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/applelogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/applepodcastslogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/approximateequals.d.ts","./node_modules/@phosphor-icons/react/dist/csr/archive.d.ts","./node_modules/@phosphor-icons/react/dist/csr/armchair.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowarcleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowarcright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowbenddoubleupleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowbenddoubleupright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowbenddownleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowbenddownright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowbendleftdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowbendleftup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowbendrightdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowbendrightup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowbendupleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowbendupright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowcircledown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowcircledownleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowcircledownright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowcircleleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowcircleright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowcircleup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowcircleupleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowcircleupright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowcounterclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowdownleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowdownright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowelbowdownleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowelbowdownright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowelbowleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowelbowleftdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowelbowleftup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowelbowright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowelbowrightdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowelbowrightup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowelbowupleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowelbowupright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatlinedown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatlineleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatlineright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatlineup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatlinesdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatlinesleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatlinesright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatlinesup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowfatup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowlinedown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowlinedownleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowlinedownright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowlineleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowlineright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowlineup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowlineupleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowlineupright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsquaredown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsquaredownleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsquaredownright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsquarein.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsquareleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsquareout.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsquareright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsquareup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsquareupleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsquareupright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowudownleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowudownright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowuleftdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowuleftup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowurightdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowurightup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowuupleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowuupright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowupleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowupright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowscounterclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsdownup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowshorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsin.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsincardinal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsinlinehorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsinlinevertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsinsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsleftright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsmerge.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsout.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsoutcardinal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsoutlinehorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsoutlinevertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsoutsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowssplit.d.ts","./node_modules/@phosphor-icons/react/dist/csr/arrowsvertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/article.d.ts","./node_modules/@phosphor-icons/react/dist/csr/articlemedium.d.ts","./node_modules/@phosphor-icons/react/dist/csr/articlenytimes.d.ts","./node_modules/@phosphor-icons/react/dist/csr/asclepius.d.ts","./node_modules/@phosphor-icons/react/dist/csr/asterisk.d.ts","./node_modules/@phosphor-icons/react/dist/csr/asterisksimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/at.d.ts","./node_modules/@phosphor-icons/react/dist/csr/atom.d.ts","./node_modules/@phosphor-icons/react/dist/csr/avocado.d.ts","./node_modules/@phosphor-icons/react/dist/csr/axe.d.ts","./node_modules/@phosphor-icons/react/dist/csr/baby.d.ts","./node_modules/@phosphor-icons/react/dist/csr/babycarriage.d.ts","./node_modules/@phosphor-icons/react/dist/csr/backpack.d.ts","./node_modules/@phosphor-icons/react/dist/csr/backspace.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bag.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bagsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/balloon.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bandaids.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bank.d.ts","./node_modules/@phosphor-icons/react/dist/csr/barbell.d.ts","./node_modules/@phosphor-icons/react/dist/csr/barcode.d.ts","./node_modules/@phosphor-icons/react/dist/csr/barn.d.ts","./node_modules/@phosphor-icons/react/dist/csr/barricade.d.ts","./node_modules/@phosphor-icons/react/dist/csr/baseball.d.ts","./node_modules/@phosphor-icons/react/dist/csr/baseballcap.d.ts","./node_modules/@phosphor-icons/react/dist/csr/baseballhelmet.d.ts","./node_modules/@phosphor-icons/react/dist/csr/basket.d.ts","./node_modules/@phosphor-icons/react/dist/csr/basketball.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bathtub.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batterycharging.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batterychargingvertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batteryempty.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batteryfull.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batteryhigh.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batterylow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batterymedium.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batteryplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batteryplusvertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batteryverticalempty.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batteryverticalfull.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batteryverticalhigh.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batteryverticallow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batteryverticalmedium.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batterywarning.d.ts","./node_modules/@phosphor-icons/react/dist/csr/batterywarningvertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/beachball.d.ts","./node_modules/@phosphor-icons/react/dist/csr/beanie.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bed.d.ts","./node_modules/@phosphor-icons/react/dist/csr/beerbottle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/beerstein.d.ts","./node_modules/@phosphor-icons/react/dist/csr/behancelogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bell.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bellringing.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bellsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bellsimpleringing.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bellsimpleslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bellsimplez.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bellslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bellz.d.ts","./node_modules/@phosphor-icons/react/dist/csr/belt.d.ts","./node_modules/@phosphor-icons/react/dist/csr/beziercurve.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bicycle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/binary.d.ts","./node_modules/@phosphor-icons/react/dist/csr/binoculars.d.ts","./node_modules/@phosphor-icons/react/dist/csr/biohazard.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bird.d.ts","./node_modules/@phosphor-icons/react/dist/csr/blueprint.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bluetooth.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bluetoothconnected.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bluetoothslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bluetoothx.d.ts","./node_modules/@phosphor-icons/react/dist/csr/boat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bomb.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/book.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bookbookmark.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bookopen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bookopentext.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bookopenuser.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bookmark.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bookmarksimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bookmarks.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bookmarkssimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/books.d.ts","./node_modules/@phosphor-icons/react/dist/csr/boot.d.ts","./node_modules/@phosphor-icons/react/dist/csr/boules.d.ts","./node_modules/@phosphor-icons/react/dist/csr/boundingbox.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bowlfood.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bowlsteam.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bowlingball.d.ts","./node_modules/@phosphor-icons/react/dist/csr/boxarrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/boxarrowup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/boxingglove.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bracketsangle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bracketscurly.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bracketsround.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bracketssquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/brain.d.ts","./node_modules/@phosphor-icons/react/dist/csr/brandy.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bread.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bridge.d.ts","./node_modules/@phosphor-icons/react/dist/csr/briefcase.d.ts","./node_modules/@phosphor-icons/react/dist/csr/briefcasemetal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/broadcast.d.ts","./node_modules/@phosphor-icons/react/dist/csr/broom.d.ts","./node_modules/@phosphor-icons/react/dist/csr/browser.d.ts","./node_modules/@phosphor-icons/react/dist/csr/browsers.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bugbeetle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bug.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bugdroid.d.ts","./node_modules/@phosphor-icons/react/dist/csr/buildingapartment.d.ts","./node_modules/@phosphor-icons/react/dist/csr/building.d.ts","./node_modules/@phosphor-icons/react/dist/csr/buildingoffice.d.ts","./node_modules/@phosphor-icons/react/dist/csr/buildings.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bulldozer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/bus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/butterfly.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cablecar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cactus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cake.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calculator.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calendarblank.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calendar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calendarcheck.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calendardot.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calendardots.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calendarheart.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calendarminus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calendarplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calendarslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calendarstar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/calendarx.d.ts","./node_modules/@phosphor-icons/react/dist/csr/callbell.d.ts","./node_modules/@phosphor-icons/react/dist/csr/camera.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cameraplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/camerarotate.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cameraslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/campfire.d.ts","./node_modules/@phosphor-icons/react/dist/csr/carbattery.d.ts","./node_modules/@phosphor-icons/react/dist/csr/car.d.ts","./node_modules/@phosphor-icons/react/dist/csr/carprofile.d.ts","./node_modules/@phosphor-icons/react/dist/csr/carsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cardholder.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cards.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cardsthree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretcircledoubledown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretcircledoubleleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretcircledoubleright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretcircledoubleup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretcircledown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretcircleleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretcircleright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretcircleup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretcircleupdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretdoubledown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretdoubleleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretdoubleright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretdoubleup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretlinedown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretlineleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretlineright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretlineup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/caretupdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/carrot.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cashregister.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cassettetape.d.ts","./node_modules/@phosphor-icons/react/dist/csr/castleturret.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cellsignalfull.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cellsignalhigh.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cellsignallow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cellsignalmedium.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cellsignalnone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cellsignalslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cellsignalx.d.ts","./node_modules/@phosphor-icons/react/dist/csr/celltower.d.ts","./node_modules/@phosphor-icons/react/dist/csr/certificate.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chair.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chalkboard.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chalkboardsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chalkboardteacher.d.ts","./node_modules/@phosphor-icons/react/dist/csr/champagne.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chargingstation.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chartbar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chartbarhorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chartdonut.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chartline.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chartlinedown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chartlineup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chartpie.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chartpieslice.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chartpolar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chartscatter.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatcentered.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatcentereddots.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatcenteredslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatcenteredtext.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatcircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatcircledots.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatcircleslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatcircletext.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatdots.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatteardrop.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatteardropdots.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatteardropslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatteardroptext.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chattext.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chats.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatscircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chatsteardrop.d.ts","./node_modules/@phosphor-icons/react/dist/csr/check.d.ts","./node_modules/@phosphor-icons/react/dist/csr/checkcircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/checkfat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/checksquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/checksquareoffset.d.ts","./node_modules/@phosphor-icons/react/dist/csr/checkerboard.d.ts","./node_modules/@phosphor-icons/react/dist/csr/checks.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cheers.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cheese.d.ts","./node_modules/@phosphor-icons/react/dist/csr/chefhat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cherries.d.ts","./node_modules/@phosphor-icons/react/dist/csr/church.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cigarette.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cigaretteslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/circle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/circledashed.d.ts","./node_modules/@phosphor-icons/react/dist/csr/circlehalf.d.ts","./node_modules/@phosphor-icons/react/dist/csr/circlehalftilt.d.ts","./node_modules/@phosphor-icons/react/dist/csr/circlenotch.d.ts","./node_modules/@phosphor-icons/react/dist/csr/circlesfour.d.ts","./node_modules/@phosphor-icons/react/dist/csr/circlesthree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/circlesthreeplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/circuitry.d.ts","./node_modules/@phosphor-icons/react/dist/csr/city.d.ts","./node_modules/@phosphor-icons/react/dist/csr/clipboard.d.ts","./node_modules/@phosphor-icons/react/dist/csr/clipboardtext.d.ts","./node_modules/@phosphor-icons/react/dist/csr/clockafternoon.d.ts","./node_modules/@phosphor-icons/react/dist/csr/clock.d.ts","./node_modules/@phosphor-icons/react/dist/csr/clockclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/csr/clockcountdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/clockcounterclockwise.d.ts","./node_modules/@phosphor-icons/react/dist/csr/clockuser.d.ts","./node_modules/@phosphor-icons/react/dist/csr/closedcaptioning.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudarrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudarrowup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloud.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudcheck.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudfog.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudlightning.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudmoon.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudrain.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudsnow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudsun.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudwarning.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cloudx.d.ts","./node_modules/@phosphor-icons/react/dist/csr/clover.d.ts","./node_modules/@phosphor-icons/react/dist/csr/club.d.ts","./node_modules/@phosphor-icons/react/dist/csr/coathanger.d.ts","./node_modules/@phosphor-icons/react/dist/csr/codalogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/codeblock.d.ts","./node_modules/@phosphor-icons/react/dist/csr/code.d.ts","./node_modules/@phosphor-icons/react/dist/csr/codesimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/codepenlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/codesandboxlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/coffeebean.d.ts","./node_modules/@phosphor-icons/react/dist/csr/coffee.d.ts","./node_modules/@phosphor-icons/react/dist/csr/coin.d.ts","./node_modules/@phosphor-icons/react/dist/csr/coinvertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/coins.d.ts","./node_modules/@phosphor-icons/react/dist/csr/columns.d.ts","./node_modules/@phosphor-icons/react/dist/csr/columnsplusleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/columnsplusright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/command.d.ts","./node_modules/@phosphor-icons/react/dist/csr/compass.d.ts","./node_modules/@phosphor-icons/react/dist/csr/compassrose.d.ts","./node_modules/@phosphor-icons/react/dist/csr/compasstool.d.ts","./node_modules/@phosphor-icons/react/dist/csr/computertower.d.ts","./node_modules/@phosphor-icons/react/dist/csr/confetti.d.ts","./node_modules/@phosphor-icons/react/dist/csr/contactlesspayment.d.ts","./node_modules/@phosphor-icons/react/dist/csr/control.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cookie.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cookingpot.d.ts","./node_modules/@phosphor-icons/react/dist/csr/copy.d.ts","./node_modules/@phosphor-icons/react/dist/csr/copysimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/copyleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/copyright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cornersin.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cornersout.d.ts","./node_modules/@phosphor-icons/react/dist/csr/couch.d.ts","./node_modules/@phosphor-icons/react/dist/csr/courtbasketball.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cowboyhat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cpu.d.ts","./node_modules/@phosphor-icons/react/dist/csr/crane.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cranetower.d.ts","./node_modules/@phosphor-icons/react/dist/csr/creditcard.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cricket.d.ts","./node_modules/@phosphor-icons/react/dist/csr/crop.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cross.d.ts","./node_modules/@phosphor-icons/react/dist/csr/crosshair.d.ts","./node_modules/@phosphor-icons/react/dist/csr/crosshairsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/crown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/crowncross.d.ts","./node_modules/@phosphor-icons/react/dist/csr/crownsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cube.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cubefocus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cubetransparent.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencybtc.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencycircledollar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencycny.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencydollar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencydollarsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencyeth.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencyeur.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencygbp.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencyinr.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencyjpy.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencykrw.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencykzt.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencyngn.d.ts","./node_modules/@phosphor-icons/react/dist/csr/currencyrub.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cursor.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cursorclick.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cursortext.d.ts","./node_modules/@phosphor-icons/react/dist/csr/cylinder.d.ts","./node_modules/@phosphor-icons/react/dist/csr/database.d.ts","./node_modules/@phosphor-icons/react/dist/csr/desk.d.ts","./node_modules/@phosphor-icons/react/dist/csr/desktop.d.ts","./node_modules/@phosphor-icons/react/dist/csr/desktoptower.d.ts","./node_modules/@phosphor-icons/react/dist/csr/detective.d.ts","./node_modules/@phosphor-icons/react/dist/csr/devtologo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/devicemobile.d.ts","./node_modules/@phosphor-icons/react/dist/csr/devicemobilecamera.d.ts","./node_modules/@phosphor-icons/react/dist/csr/devicemobileslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/devicemobilespeaker.d.ts","./node_modules/@phosphor-icons/react/dist/csr/devicerotate.d.ts","./node_modules/@phosphor-icons/react/dist/csr/devicetablet.d.ts","./node_modules/@phosphor-icons/react/dist/csr/devicetabletcamera.d.ts","./node_modules/@phosphor-icons/react/dist/csr/devicetabletspeaker.d.ts","./node_modules/@phosphor-icons/react/dist/csr/devices.d.ts","./node_modules/@phosphor-icons/react/dist/csr/diamond.d.ts","./node_modules/@phosphor-icons/react/dist/csr/diamondsfour.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dicefive.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dicefour.d.ts","./node_modules/@phosphor-icons/react/dist/csr/diceone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dicesix.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dicethree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dicetwo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/disc.d.ts","./node_modules/@phosphor-icons/react/dist/csr/discoball.d.ts","./node_modules/@phosphor-icons/react/dist/csr/discordlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/divide.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dna.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dog.d.ts","./node_modules/@phosphor-icons/react/dist/csr/door.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dooropen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dot.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dotoutline.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dotsnine.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dotssix.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dotssixvertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dotsthree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dotsthreecircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dotsthreecirclevertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dotsthreeoutline.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dotsthreeoutlinevertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dotsthreevertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/download.d.ts","./node_modules/@phosphor-icons/react/dist/csr/downloadsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dress.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dresser.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dribbblelogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/drone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/drop.d.ts","./node_modules/@phosphor-icons/react/dist/csr/drophalf.d.ts","./node_modules/@phosphor-icons/react/dist/csr/drophalfbottom.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dropsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dropslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/dropboxlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/ear.d.ts","./node_modules/@phosphor-icons/react/dist/csr/earslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/egg.d.ts","./node_modules/@phosphor-icons/react/dist/csr/eggcrack.d.ts","./node_modules/@phosphor-icons/react/dist/csr/eject.d.ts","./node_modules/@phosphor-icons/react/dist/csr/ejectsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/elevator.d.ts","./node_modules/@phosphor-icons/react/dist/csr/empty.d.ts","./node_modules/@phosphor-icons/react/dist/csr/engine.d.ts","./node_modules/@phosphor-icons/react/dist/csr/envelope.d.ts","./node_modules/@phosphor-icons/react/dist/csr/envelopeopen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/envelopesimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/envelopesimpleopen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/equalizer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/equals.d.ts","./node_modules/@phosphor-icons/react/dist/csr/eraser.d.ts","./node_modules/@phosphor-icons/react/dist/csr/escalatordown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/escalatorup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/exam.d.ts","./node_modules/@phosphor-icons/react/dist/csr/exclamationmark.d.ts","./node_modules/@phosphor-icons/react/dist/csr/exclude.d.ts","./node_modules/@phosphor-icons/react/dist/csr/excludesquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/export.d.ts","./node_modules/@phosphor-icons/react/dist/csr/eye.d.ts","./node_modules/@phosphor-icons/react/dist/csr/eyeclosed.d.ts","./node_modules/@phosphor-icons/react/dist/csr/eyeslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/eyedropper.d.ts","./node_modules/@phosphor-icons/react/dist/csr/eyedroppersample.d.ts","./node_modules/@phosphor-icons/react/dist/csr/eyeglasses.d.ts","./node_modules/@phosphor-icons/react/dist/csr/eyes.d.ts","./node_modules/@phosphor-icons/react/dist/csr/facemask.d.ts","./node_modules/@phosphor-icons/react/dist/csr/facebooklogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/factory.d.ts","./node_modules/@phosphor-icons/react/dist/csr/faders.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fadershorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/falloutshelter.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fan.d.ts","./node_modules/@phosphor-icons/react/dist/csr/farm.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fastforward.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fastforwardcircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/feather.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fediverselogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/figmalogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filearchive.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filearrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filearrowup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fileaudio.d.ts","./node_modules/@phosphor-icons/react/dist/csr/file.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filec.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filecsharp.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filecloud.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filecode.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filecpp.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filecss.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filecsv.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filedashed.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filedoc.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filehtml.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fileimage.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fileini.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filejpg.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filejs.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filejsx.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filelock.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filemagnifyingglass.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filemd.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fileminus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filepdf.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fileplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filepng.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fileppt.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filepy.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filers.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filesql.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filesvg.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filetext.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filets.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filetsx.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filetxt.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filevideo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filevue.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filex.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filexls.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filezip.d.ts","./node_modules/@phosphor-icons/react/dist/csr/files.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filmreel.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filmscript.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filmslate.d.ts","./node_modules/@phosphor-icons/react/dist/csr/filmstrip.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fingerprint.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fingerprintsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/finnthehuman.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fire.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fireextinguisher.d.ts","./node_modules/@phosphor-icons/react/dist/csr/firesimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/firetruck.d.ts","./node_modules/@phosphor-icons/react/dist/csr/firstaid.d.ts","./node_modules/@phosphor-icons/react/dist/csr/firstaidkit.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fish.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fishsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flagbanner.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flagbannerfold.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flag.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flagcheckered.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flagpennant.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flame.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flashlight.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flask.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fliphorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flipvertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/floppydiskback.d.ts","./node_modules/@phosphor-icons/react/dist/csr/floppydisk.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flowarrow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flower.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flowerlotus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flowertulip.d.ts","./node_modules/@phosphor-icons/react/dist/csr/flyingsaucer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/folder.d.ts","./node_modules/@phosphor-icons/react/dist/csr/folderdashed.d.ts","./node_modules/@phosphor-icons/react/dist/csr/folderlock.d.ts","./node_modules/@phosphor-icons/react/dist/csr/folderminus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/folderopen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/folderplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/foldersimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/foldersimpledashed.d.ts","./node_modules/@phosphor-icons/react/dist/csr/foldersimplelock.d.ts","./node_modules/@phosphor-icons/react/dist/csr/foldersimpleminus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/foldersimpleplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/foldersimplestar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/foldersimpleuser.d.ts","./node_modules/@phosphor-icons/react/dist/csr/folderstar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/folderuser.d.ts","./node_modules/@phosphor-icons/react/dist/csr/folders.d.ts","./node_modules/@phosphor-icons/react/dist/csr/football.d.ts","./node_modules/@phosphor-icons/react/dist/csr/footballhelmet.d.ts","./node_modules/@phosphor-icons/react/dist/csr/footprints.d.ts","./node_modules/@phosphor-icons/react/dist/csr/forkknife.d.ts","./node_modules/@phosphor-icons/react/dist/csr/fourk.d.ts","./node_modules/@phosphor-icons/react/dist/csr/framecorners.d.ts","./node_modules/@phosphor-icons/react/dist/csr/framerlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/function.d.ts","./node_modules/@phosphor-icons/react/dist/csr/funnel.d.ts","./node_modules/@phosphor-icons/react/dist/csr/funnelsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/funnelsimplex.d.ts","./node_modules/@phosphor-icons/react/dist/csr/funnelx.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gamecontroller.d.ts","./node_modules/@phosphor-icons/react/dist/csr/garage.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gascan.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gaspump.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gauge.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gavel.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gear.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gearfine.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gearsix.d.ts","./node_modules/@phosphor-icons/react/dist/csr/genderfemale.d.ts","./node_modules/@phosphor-icons/react/dist/csr/genderintersex.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gendermale.d.ts","./node_modules/@phosphor-icons/react/dist/csr/genderneuter.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gendernonbinary.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gendertransgender.d.ts","./node_modules/@phosphor-icons/react/dist/csr/ghost.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gif.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gift.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gitbranch.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gitcommit.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gitdiff.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gitfork.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gitmerge.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gitpullrequest.d.ts","./node_modules/@phosphor-icons/react/dist/csr/githublogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gitlablogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gitlablogosimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/globe.d.ts","./node_modules/@phosphor-icons/react/dist/csr/globehemisphereeast.d.ts","./node_modules/@phosphor-icons/react/dist/csr/globehemispherewest.d.ts","./node_modules/@phosphor-icons/react/dist/csr/globesimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/globesimplex.d.ts","./node_modules/@phosphor-icons/react/dist/csr/globestand.d.ts","./node_modules/@phosphor-icons/react/dist/csr/globex.d.ts","./node_modules/@phosphor-icons/react/dist/csr/goggles.d.ts","./node_modules/@phosphor-icons/react/dist/csr/golf.d.ts","./node_modules/@phosphor-icons/react/dist/csr/goodreadslogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/googlecardboardlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/googlechromelogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/googledrivelogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/googlelogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/googlephotoslogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/googleplaylogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/googlepodcastslogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gps.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gpsfix.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gpsslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gradient.d.ts","./node_modules/@phosphor-icons/react/dist/csr/graduationcap.d.ts","./node_modules/@phosphor-icons/react/dist/csr/grains.d.ts","./node_modules/@phosphor-icons/react/dist/csr/grainsslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/graph.d.ts","./node_modules/@phosphor-icons/react/dist/csr/graphicscard.d.ts","./node_modules/@phosphor-icons/react/dist/csr/greaterthan.d.ts","./node_modules/@phosphor-icons/react/dist/csr/greaterthanorequal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gridfour.d.ts","./node_modules/@phosphor-icons/react/dist/csr/gridnine.d.ts","./node_modules/@phosphor-icons/react/dist/csr/guitar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hairdryer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hamburger.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hammer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handarrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handarrowup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hand.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handcoins.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handdeposit.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handeye.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handfist.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handgrabbing.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handheart.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handpalm.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handpeace.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handpointing.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handsoap.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handswipeleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handswiperight.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handtap.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handwaving.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handwithdraw.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handbag.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handbagsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handsclapping.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handspraying.d.ts","./node_modules/@phosphor-icons/react/dist/csr/handshake.d.ts","./node_modules/@phosphor-icons/react/dist/csr/harddrive.d.ts","./node_modules/@phosphor-icons/react/dist/csr/harddrives.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hardhat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hashstraight.d.ts","./node_modules/@phosphor-icons/react/dist/csr/headcircuit.d.ts","./node_modules/@phosphor-icons/react/dist/csr/headlights.d.ts","./node_modules/@phosphor-icons/react/dist/csr/headphones.d.ts","./node_modules/@phosphor-icons/react/dist/csr/headset.d.ts","./node_modules/@phosphor-icons/react/dist/csr/heart.d.ts","./node_modules/@phosphor-icons/react/dist/csr/heartbreak.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hearthalf.d.ts","./node_modules/@phosphor-icons/react/dist/csr/heartstraight.d.ts","./node_modules/@phosphor-icons/react/dist/csr/heartstraightbreak.d.ts","./node_modules/@phosphor-icons/react/dist/csr/heartbeat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hexagon.d.ts","./node_modules/@phosphor-icons/react/dist/csr/highdefinition.d.ts","./node_modules/@phosphor-icons/react/dist/csr/highheel.d.ts","./node_modules/@phosphor-icons/react/dist/csr/highlighter.d.ts","./node_modules/@phosphor-icons/react/dist/csr/highlightercircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hockey.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hoodie.d.ts","./node_modules/@phosphor-icons/react/dist/csr/horse.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hospital.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hourglass.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hourglasshigh.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hourglasslow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hourglassmedium.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hourglasssimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hourglasssimplehigh.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hourglasssimplelow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hourglasssimplemedium.d.ts","./node_modules/@phosphor-icons/react/dist/csr/house.d.ts","./node_modules/@phosphor-icons/react/dist/csr/houseline.d.ts","./node_modules/@phosphor-icons/react/dist/csr/housesimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/hurricane.d.ts","./node_modules/@phosphor-icons/react/dist/csr/icecream.d.ts","./node_modules/@phosphor-icons/react/dist/csr/identificationbadge.d.ts","./node_modules/@phosphor-icons/react/dist/csr/identificationcard.d.ts","./node_modules/@phosphor-icons/react/dist/csr/image.d.ts","./node_modules/@phosphor-icons/react/dist/csr/imagebroken.d.ts","./node_modules/@phosphor-icons/react/dist/csr/imagesquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/images.d.ts","./node_modules/@phosphor-icons/react/dist/csr/imagessquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/infinity.d.ts","./node_modules/@phosphor-icons/react/dist/csr/info.d.ts","./node_modules/@phosphor-icons/react/dist/csr/instagramlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/intersect.d.ts","./node_modules/@phosphor-icons/react/dist/csr/intersectsquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/intersectthree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/intersection.d.ts","./node_modules/@phosphor-icons/react/dist/csr/invoice.d.ts","./node_modules/@phosphor-icons/react/dist/csr/island.d.ts","./node_modules/@phosphor-icons/react/dist/csr/jar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/jarlabel.d.ts","./node_modules/@phosphor-icons/react/dist/csr/jeep.d.ts","./node_modules/@phosphor-icons/react/dist/csr/joystick.d.ts","./node_modules/@phosphor-icons/react/dist/csr/kanban.d.ts","./node_modules/@phosphor-icons/react/dist/csr/key.d.ts","./node_modules/@phosphor-icons/react/dist/csr/keyreturn.d.ts","./node_modules/@phosphor-icons/react/dist/csr/keyboard.d.ts","./node_modules/@phosphor-icons/react/dist/csr/keyhole.d.ts","./node_modules/@phosphor-icons/react/dist/csr/knife.d.ts","./node_modules/@phosphor-icons/react/dist/csr/ladder.d.ts","./node_modules/@phosphor-icons/react/dist/csr/laddersimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lamp.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lamppendant.d.ts","./node_modules/@phosphor-icons/react/dist/csr/laptop.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lasso.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lastfmlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/layout.d.ts","./node_modules/@phosphor-icons/react/dist/csr/leaf.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lectern.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lego.d.ts","./node_modules/@phosphor-icons/react/dist/csr/legosmiley.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lessthan.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lessthanorequal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lettercircleh.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lettercirclep.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lettercirclev.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lifebuoy.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lightbulb.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lightbulbfilament.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lighthouse.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lightninga.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lightning.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lightningslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/linesegment.d.ts","./node_modules/@phosphor-icons/react/dist/csr/linesegments.d.ts","./node_modules/@phosphor-icons/react/dist/csr/linevertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/link.d.ts","./node_modules/@phosphor-icons/react/dist/csr/linkbreak.d.ts","./node_modules/@phosphor-icons/react/dist/csr/linksimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/linksimplebreak.d.ts","./node_modules/@phosphor-icons/react/dist/csr/linksimplehorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/linksimplehorizontalbreak.d.ts","./node_modules/@phosphor-icons/react/dist/csr/linkedinlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/linktreelogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/linuxlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/list.d.ts","./node_modules/@phosphor-icons/react/dist/csr/listbullets.d.ts","./node_modules/@phosphor-icons/react/dist/csr/listchecks.d.ts","./node_modules/@phosphor-icons/react/dist/csr/listdashes.d.ts","./node_modules/@phosphor-icons/react/dist/csr/listheart.d.ts","./node_modules/@phosphor-icons/react/dist/csr/listmagnifyingglass.d.ts","./node_modules/@phosphor-icons/react/dist/csr/listnumbers.d.ts","./node_modules/@phosphor-icons/react/dist/csr/listplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/liststar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lock.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lockkey.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lockkeyopen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/locklaminated.d.ts","./node_modules/@phosphor-icons/react/dist/csr/locklaminatedopen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lockopen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/locksimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/locksimpleopen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/lockers.d.ts","./node_modules/@phosphor-icons/react/dist/csr/log.d.ts","./node_modules/@phosphor-icons/react/dist/csr/magicwand.d.ts","./node_modules/@phosphor-icons/react/dist/csr/magnet.d.ts","./node_modules/@phosphor-icons/react/dist/csr/magnetstraight.d.ts","./node_modules/@phosphor-icons/react/dist/csr/magnifyingglass.d.ts","./node_modules/@phosphor-icons/react/dist/csr/magnifyingglassminus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/magnifyingglassplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mailbox.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mappinarea.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mappin.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mappinline.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mappinplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mappinsimplearea.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mappinsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mappinsimpleline.d.ts","./node_modules/@phosphor-icons/react/dist/csr/maptrifold.d.ts","./node_modules/@phosphor-icons/react/dist/csr/markdownlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/markercircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/martini.d.ts","./node_modules/@phosphor-icons/react/dist/csr/maskhappy.d.ts","./node_modules/@phosphor-icons/react/dist/csr/masksad.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mastodonlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mathoperations.d.ts","./node_modules/@phosphor-icons/react/dist/csr/matrixlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/medal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/medalmilitary.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mediumlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/megaphone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/megaphonesimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/memberof.d.ts","./node_modules/@phosphor-icons/react/dist/csr/memory.d.ts","./node_modules/@phosphor-icons/react/dist/csr/messengerlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/metalogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/meteor.d.ts","./node_modules/@phosphor-icons/react/dist/csr/metronome.d.ts","./node_modules/@phosphor-icons/react/dist/csr/microphone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/microphoneslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/microphonestage.d.ts","./node_modules/@phosphor-icons/react/dist/csr/microscope.d.ts","./node_modules/@phosphor-icons/react/dist/csr/microsoftexcellogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/microsoftoutlooklogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/microsoftpowerpointlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/microsoftteamslogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/microsoftwordlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/minus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/minuscircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/minussquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/money.d.ts","./node_modules/@phosphor-icons/react/dist/csr/moneywavy.d.ts","./node_modules/@phosphor-icons/react/dist/csr/monitorarrowup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/monitor.d.ts","./node_modules/@phosphor-icons/react/dist/csr/monitorplay.d.ts","./node_modules/@phosphor-icons/react/dist/csr/moon.d.ts","./node_modules/@phosphor-icons/react/dist/csr/moonstars.d.ts","./node_modules/@phosphor-icons/react/dist/csr/moped.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mopedfront.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mosque.d.ts","./node_modules/@phosphor-icons/react/dist/csr/motorcycle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mountains.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mouse.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mouseleftclick.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mousemiddleclick.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mouserightclick.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mousescroll.d.ts","./node_modules/@phosphor-icons/react/dist/csr/mousesimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/musicnote.d.ts","./node_modules/@phosphor-icons/react/dist/csr/musicnotesimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/musicnotes.d.ts","./node_modules/@phosphor-icons/react/dist/csr/musicnotesminus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/musicnotesplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/musicnotessimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/navigationarrow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/needle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/network.d.ts","./node_modules/@phosphor-icons/react/dist/csr/networkslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/networkx.d.ts","./node_modules/@phosphor-icons/react/dist/csr/newspaper.d.ts","./node_modules/@phosphor-icons/react/dist/csr/newspaperclipping.d.ts","./node_modules/@phosphor-icons/react/dist/csr/notequals.d.ts","./node_modules/@phosphor-icons/react/dist/csr/notmemberof.d.ts","./node_modules/@phosphor-icons/react/dist/csr/notsubsetof.d.ts","./node_modules/@phosphor-icons/react/dist/csr/notsupersetof.d.ts","./node_modules/@phosphor-icons/react/dist/csr/notches.d.ts","./node_modules/@phosphor-icons/react/dist/csr/noteblank.d.ts","./node_modules/@phosphor-icons/react/dist/csr/note.d.ts","./node_modules/@phosphor-icons/react/dist/csr/notepencil.d.ts","./node_modules/@phosphor-icons/react/dist/csr/notebook.d.ts","./node_modules/@phosphor-icons/react/dist/csr/notepad.d.ts","./node_modules/@phosphor-icons/react/dist/csr/notification.d.ts","./node_modules/@phosphor-icons/react/dist/csr/notionlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/nuclearplant.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbercircleeight.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbercirclefive.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbercirclefour.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbercirclenine.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbercircleone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbercircleseven.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbercirclesix.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbercirclethree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbercircletwo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbercirclezero.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbereight.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numberfive.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numberfour.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbernine.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numberone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numberseven.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbersix.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbersquareeight.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbersquarefive.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbersquarefour.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbersquarenine.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbersquareone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbersquareseven.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbersquaresix.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbersquarethree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbersquaretwo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbersquarezero.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numberthree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numbertwo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numberzero.d.ts","./node_modules/@phosphor-icons/react/dist/csr/numpad.d.ts","./node_modules/@phosphor-icons/react/dist/csr/nut.d.ts","./node_modules/@phosphor-icons/react/dist/csr/nytimeslogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/octagon.d.ts","./node_modules/@phosphor-icons/react/dist/csr/officechair.d.ts","./node_modules/@phosphor-icons/react/dist/csr/onigiri.d.ts","./node_modules/@phosphor-icons/react/dist/csr/openailogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/option.d.ts","./node_modules/@phosphor-icons/react/dist/csr/orange.d.ts","./node_modules/@phosphor-icons/react/dist/csr/orangeslice.d.ts","./node_modules/@phosphor-icons/react/dist/csr/oven.d.ts","./node_modules/@phosphor-icons/react/dist/csr/package.d.ts","./node_modules/@phosphor-icons/react/dist/csr/paintbrush.d.ts","./node_modules/@phosphor-icons/react/dist/csr/paintbrushbroad.d.ts","./node_modules/@phosphor-icons/react/dist/csr/paintbrushhousehold.d.ts","./node_modules/@phosphor-icons/react/dist/csr/paintbucket.d.ts","./node_modules/@phosphor-icons/react/dist/csr/paintroller.d.ts","./node_modules/@phosphor-icons/react/dist/csr/palette.d.ts","./node_modules/@phosphor-icons/react/dist/csr/panorama.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pants.d.ts","./node_modules/@phosphor-icons/react/dist/csr/paperplane.d.ts","./node_modules/@phosphor-icons/react/dist/csr/paperplaneright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/paperplanetilt.d.ts","./node_modules/@phosphor-icons/react/dist/csr/paperclip.d.ts","./node_modules/@phosphor-icons/react/dist/csr/papercliphorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/parachute.d.ts","./node_modules/@phosphor-icons/react/dist/csr/paragraph.d.ts","./node_modules/@phosphor-icons/react/dist/csr/parallelogram.d.ts","./node_modules/@phosphor-icons/react/dist/csr/park.d.ts","./node_modules/@phosphor-icons/react/dist/csr/password.d.ts","./node_modules/@phosphor-icons/react/dist/csr/path.d.ts","./node_modules/@phosphor-icons/react/dist/csr/patreonlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pause.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pausecircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pawprint.d.ts","./node_modules/@phosphor-icons/react/dist/csr/paypallogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/peace.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pennib.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pennibstraight.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pencil.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pencilcircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pencilline.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pencilruler.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pencilsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pencilsimpleline.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pencilsimpleslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pencilslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pentagon.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pentagram.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pepper.d.ts","./node_modules/@phosphor-icons/react/dist/csr/percent.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personarmsspread.d.ts","./node_modules/@phosphor-icons/react/dist/csr/person.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personsimplebike.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personsimplecircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personsimplehike.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personsimplerun.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personsimpleski.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personsimplesnowboard.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personsimpleswim.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personsimpletaichi.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personsimplethrow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/personsimplewalk.d.ts","./node_modules/@phosphor-icons/react/dist/csr/perspective.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phonecall.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phonedisconnect.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phoneincoming.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phonelist.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phoneoutgoing.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phonepause.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phoneplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phoneslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phonetransfer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phonex.d.ts","./node_modules/@phosphor-icons/react/dist/csr/phosphorlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pi.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pianokeys.d.ts","./node_modules/@phosphor-icons/react/dist/csr/picnictable.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pictureinpicture.d.ts","./node_modules/@phosphor-icons/react/dist/csr/piggybank.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pill.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pingpong.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pintglass.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pinterestlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pinwheel.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pipe.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pipewrench.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pixlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pizza.d.ts","./node_modules/@phosphor-icons/react/dist/csr/placeholder.d.ts","./node_modules/@phosphor-icons/react/dist/csr/planet.d.ts","./node_modules/@phosphor-icons/react/dist/csr/plant.d.ts","./node_modules/@phosphor-icons/react/dist/csr/play.d.ts","./node_modules/@phosphor-icons/react/dist/csr/playcircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/playpause.d.ts","./node_modules/@phosphor-icons/react/dist/csr/playlist.d.ts","./node_modules/@phosphor-icons/react/dist/csr/plug.d.ts","./node_modules/@phosphor-icons/react/dist/csr/plugcharging.d.ts","./node_modules/@phosphor-icons/react/dist/csr/plugs.d.ts","./node_modules/@phosphor-icons/react/dist/csr/plugsconnected.d.ts","./node_modules/@phosphor-icons/react/dist/csr/plus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pluscircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/plusminus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/plussquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pokerchip.d.ts","./node_modules/@phosphor-icons/react/dist/csr/policecar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/polygon.d.ts","./node_modules/@phosphor-icons/react/dist/csr/popcorn.d.ts","./node_modules/@phosphor-icons/react/dist/csr/popsicle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pottedplant.d.ts","./node_modules/@phosphor-icons/react/dist/csr/power.d.ts","./node_modules/@phosphor-icons/react/dist/csr/prescription.d.ts","./node_modules/@phosphor-icons/react/dist/csr/presentation.d.ts","./node_modules/@phosphor-icons/react/dist/csr/presentationchart.d.ts","./node_modules/@phosphor-icons/react/dist/csr/printer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/prohibit.d.ts","./node_modules/@phosphor-icons/react/dist/csr/prohibitinset.d.ts","./node_modules/@phosphor-icons/react/dist/csr/projectorscreen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/projectorscreenchart.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pulse.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pushpin.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pushpinsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pushpinsimpleslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/pushpinslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/puzzlepiece.d.ts","./node_modules/@phosphor-icons/react/dist/csr/qrcode.d.ts","./node_modules/@phosphor-icons/react/dist/csr/question.d.ts","./node_modules/@phosphor-icons/react/dist/csr/questionmark.d.ts","./node_modules/@phosphor-icons/react/dist/csr/queue.d.ts","./node_modules/@phosphor-icons/react/dist/csr/quotes.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rabbit.d.ts","./node_modules/@phosphor-icons/react/dist/csr/racquet.d.ts","./node_modules/@phosphor-icons/react/dist/csr/radical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/radio.d.ts","./node_modules/@phosphor-icons/react/dist/csr/radiobutton.d.ts","./node_modules/@phosphor-icons/react/dist/csr/radioactive.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rainbow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rainbowcloud.d.ts","./node_modules/@phosphor-icons/react/dist/csr/ranking.d.ts","./node_modules/@phosphor-icons/react/dist/csr/readcvlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/receipt.d.ts","./node_modules/@phosphor-icons/react/dist/csr/receiptx.d.ts","./node_modules/@phosphor-icons/react/dist/csr/record.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rectangle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rectangledashed.d.ts","./node_modules/@phosphor-icons/react/dist/csr/recycle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/redditlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/repeat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/repeatonce.d.ts","./node_modules/@phosphor-icons/react/dist/csr/replitlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/resize.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rewind.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rewindcircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/roadhorizon.d.ts","./node_modules/@phosphor-icons/react/dist/csr/robot.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rocket.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rocketlaunch.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rows.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rowsplusbottom.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rowsplustop.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rss.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rsssimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/rug.d.ts","./node_modules/@phosphor-icons/react/dist/csr/ruler.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sailboat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/scales.d.ts","./node_modules/@phosphor-icons/react/dist/csr/scan.d.ts","./node_modules/@phosphor-icons/react/dist/csr/scansmiley.d.ts","./node_modules/@phosphor-icons/react/dist/csr/scissors.d.ts","./node_modules/@phosphor-icons/react/dist/csr/scooter.d.ts","./node_modules/@phosphor-icons/react/dist/csr/screencast.d.ts","./node_modules/@phosphor-icons/react/dist/csr/screwdriver.d.ts","./node_modules/@phosphor-icons/react/dist/csr/scribble.d.ts","./node_modules/@phosphor-icons/react/dist/csr/scribbleloop.d.ts","./node_modules/@phosphor-icons/react/dist/csr/scroll.d.ts","./node_modules/@phosphor-icons/react/dist/csr/seal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sealcheck.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sealpercent.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sealquestion.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sealwarning.d.ts","./node_modules/@phosphor-icons/react/dist/csr/seat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/seatbelt.d.ts","./node_modules/@phosphor-icons/react/dist/csr/securitycamera.d.ts","./node_modules/@phosphor-icons/react/dist/csr/selectionall.d.ts","./node_modules/@phosphor-icons/react/dist/csr/selectionbackground.d.ts","./node_modules/@phosphor-icons/react/dist/csr/selection.d.ts","./node_modules/@phosphor-icons/react/dist/csr/selectionforeground.d.ts","./node_modules/@phosphor-icons/react/dist/csr/selectioninverse.d.ts","./node_modules/@phosphor-icons/react/dist/csr/selectionplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/selectionslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shapes.d.ts","./node_modules/@phosphor-icons/react/dist/csr/share.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sharefat.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sharenetwork.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shield.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shieldcheck.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shieldcheckered.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shieldchevron.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shieldplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shieldslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shieldstar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shieldwarning.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shippingcontainer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shirtfolded.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shootingstar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shoppingbag.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shoppingbagopen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shoppingcart.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shoppingcartsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shovel.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shower.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shrimp.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shuffleangular.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shuffle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/shufflesimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sidebar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sidebarsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sigma.d.ts","./node_modules/@phosphor-icons/react/dist/csr/signin.d.ts","./node_modules/@phosphor-icons/react/dist/csr/signout.d.ts","./node_modules/@phosphor-icons/react/dist/csr/signature.d.ts","./node_modules/@phosphor-icons/react/dist/csr/signpost.d.ts","./node_modules/@phosphor-icons/react/dist/csr/simcard.d.ts","./node_modules/@phosphor-icons/react/dist/csr/siren.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sketchlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/skipback.d.ts","./node_modules/@phosphor-icons/react/dist/csr/skipbackcircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/skipforward.d.ts","./node_modules/@phosphor-icons/react/dist/csr/skipforwardcircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/skull.d.ts","./node_modules/@phosphor-icons/react/dist/csr/skypelogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/slacklogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sliders.d.ts","./node_modules/@phosphor-icons/react/dist/csr/slidershorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/slideshow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/smileyangry.d.ts","./node_modules/@phosphor-icons/react/dist/csr/smileyblank.d.ts","./node_modules/@phosphor-icons/react/dist/csr/smiley.d.ts","./node_modules/@phosphor-icons/react/dist/csr/smileymeh.d.ts","./node_modules/@phosphor-icons/react/dist/csr/smileymelting.d.ts","./node_modules/@phosphor-icons/react/dist/csr/smileynervous.d.ts","./node_modules/@phosphor-icons/react/dist/csr/smileysad.d.ts","./node_modules/@phosphor-icons/react/dist/csr/smileysticker.d.ts","./node_modules/@phosphor-icons/react/dist/csr/smileywink.d.ts","./node_modules/@phosphor-icons/react/dist/csr/smileyxeyes.d.ts","./node_modules/@phosphor-icons/react/dist/csr/snapchatlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sneaker.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sneakermove.d.ts","./node_modules/@phosphor-icons/react/dist/csr/snowflake.d.ts","./node_modules/@phosphor-icons/react/dist/csr/soccerball.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sock.d.ts","./node_modules/@phosphor-icons/react/dist/csr/solarpanel.d.ts","./node_modules/@phosphor-icons/react/dist/csr/solarroof.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sortascending.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sortdescending.d.ts","./node_modules/@phosphor-icons/react/dist/csr/soundcloudlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/spade.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sparkle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speakerhifi.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speakerhigh.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speakerlow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speakernone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speakersimplehigh.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speakersimplelow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speakersimplenone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speakersimpleslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speakersimplex.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speakerslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speakerx.d.ts","./node_modules/@phosphor-icons/react/dist/csr/speedometer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sphere.d.ts","./node_modules/@phosphor-icons/react/dist/csr/spinnerball.d.ts","./node_modules/@phosphor-icons/react/dist/csr/spinner.d.ts","./node_modules/@phosphor-icons/react/dist/csr/spinnergap.d.ts","./node_modules/@phosphor-icons/react/dist/csr/spiral.d.ts","./node_modules/@phosphor-icons/react/dist/csr/splithorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/splitvertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/spotifylogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/spraybottle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/square.d.ts","./node_modules/@phosphor-icons/react/dist/csr/squarehalf.d.ts","./node_modules/@phosphor-icons/react/dist/csr/squarehalfbottom.d.ts","./node_modules/@phosphor-icons/react/dist/csr/squarelogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/squaresplithorizontal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/squaresplitvertical.d.ts","./node_modules/@phosphor-icons/react/dist/csr/squaresfour.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stack.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stackminus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stackoverflowlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stackplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stacksimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stairs.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stamp.d.ts","./node_modules/@phosphor-icons/react/dist/csr/standarddefinition.d.ts","./node_modules/@phosphor-icons/react/dist/csr/starandcrescent.d.ts","./node_modules/@phosphor-icons/react/dist/csr/star.d.ts","./node_modules/@phosphor-icons/react/dist/csr/starfour.d.ts","./node_modules/@phosphor-icons/react/dist/csr/starhalf.d.ts","./node_modules/@phosphor-icons/react/dist/csr/starofdavid.d.ts","./node_modules/@phosphor-icons/react/dist/csr/steamlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/steeringwheel.d.ts","./node_modules/@phosphor-icons/react/dist/csr/steps.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stethoscope.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sticker.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stool.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stop.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stopcircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/storefront.d.ts","./node_modules/@phosphor-icons/react/dist/csr/strategy.d.ts","./node_modules/@phosphor-icons/react/dist/csr/stripelogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/student.d.ts","./node_modules/@phosphor-icons/react/dist/csr/subsetof.d.ts","./node_modules/@phosphor-icons/react/dist/csr/subsetproperof.d.ts","./node_modules/@phosphor-icons/react/dist/csr/subtitles.d.ts","./node_modules/@phosphor-icons/react/dist/csr/subtitlesslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/subtract.d.ts","./node_modules/@phosphor-icons/react/dist/csr/subtractsquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/subway.d.ts","./node_modules/@phosphor-icons/react/dist/csr/suitcase.d.ts","./node_modules/@phosphor-icons/react/dist/csr/suitcaserolling.d.ts","./node_modules/@phosphor-icons/react/dist/csr/suitcasesimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sun.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sundim.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sunhorizon.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sunglasses.d.ts","./node_modules/@phosphor-icons/react/dist/csr/supersetof.d.ts","./node_modules/@phosphor-icons/react/dist/csr/supersetproperof.d.ts","./node_modules/@phosphor-icons/react/dist/csr/swap.d.ts","./node_modules/@phosphor-icons/react/dist/csr/swatches.d.ts","./node_modules/@phosphor-icons/react/dist/csr/swimmingpool.d.ts","./node_modules/@phosphor-icons/react/dist/csr/sword.d.ts","./node_modules/@phosphor-icons/react/dist/csr/synagogue.d.ts","./node_modules/@phosphor-icons/react/dist/csr/syringe.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tshirt.d.ts","./node_modules/@phosphor-icons/react/dist/csr/table.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tabs.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tag.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tagchevron.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tagsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/target.d.ts","./node_modules/@phosphor-icons/react/dist/csr/taxi.d.ts","./node_modules/@phosphor-icons/react/dist/csr/teabag.d.ts","./node_modules/@phosphor-icons/react/dist/csr/telegramlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/television.d.ts","./node_modules/@phosphor-icons/react/dist/csr/televisionsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tennisball.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tent.d.ts","./node_modules/@phosphor-icons/react/dist/csr/terminal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/terminalwindow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/testtube.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textaunderline.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textaa.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textaligncenter.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textalignjustify.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textalignleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textalignright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textb.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textcolumns.d.ts","./node_modules/@phosphor-icons/react/dist/csr/texth.d.ts","./node_modules/@phosphor-icons/react/dist/csr/texthfive.d.ts","./node_modules/@phosphor-icons/react/dist/csr/texthfour.d.ts","./node_modules/@phosphor-icons/react/dist/csr/texthone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/texthsix.d.ts","./node_modules/@phosphor-icons/react/dist/csr/texththree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/texthtwo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textindent.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textitalic.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textoutdent.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textstrikethrough.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textsubscript.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textsuperscript.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textt.d.ts","./node_modules/@phosphor-icons/react/dist/csr/texttslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textunderline.d.ts","./node_modules/@phosphor-icons/react/dist/csr/textbox.d.ts","./node_modules/@phosphor-icons/react/dist/csr/thermometer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/thermometercold.d.ts","./node_modules/@phosphor-icons/react/dist/csr/thermometerhot.d.ts","./node_modules/@phosphor-icons/react/dist/csr/thermometersimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/threadslogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/threed.d.ts","./node_modules/@phosphor-icons/react/dist/csr/thumbsdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/thumbsup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/ticket.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tidallogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tiktoklogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tilde.d.ts","./node_modules/@phosphor-icons/react/dist/csr/timer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tipjar.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tipi.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tire.d.ts","./node_modules/@phosphor-icons/react/dist/csr/toggleleft.d.ts","./node_modules/@phosphor-icons/react/dist/csr/toggleright.d.ts","./node_modules/@phosphor-icons/react/dist/csr/toilet.d.ts","./node_modules/@phosphor-icons/react/dist/csr/toiletpaper.d.ts","./node_modules/@phosphor-icons/react/dist/csr/toolbox.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tooth.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tornado.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tote.d.ts","./node_modules/@phosphor-icons/react/dist/csr/totesimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/towel.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tractor.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trademark.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trademarkregistered.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trafficcone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trafficsign.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trafficsignal.d.ts","./node_modules/@phosphor-icons/react/dist/csr/train.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trainregional.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trainsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tram.d.ts","./node_modules/@phosphor-icons/react/dist/csr/translate.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trashsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trayarrowdown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trayarrowup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tray.d.ts","./node_modules/@phosphor-icons/react/dist/csr/treasurechest.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/treeevergreen.d.ts","./node_modules/@phosphor-icons/react/dist/csr/treepalm.d.ts","./node_modules/@phosphor-icons/react/dist/csr/treestructure.d.ts","./node_modules/@phosphor-icons/react/dist/csr/treeview.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trenddown.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trendup.d.ts","./node_modules/@phosphor-icons/react/dist/csr/triangle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/triangledashed.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trolley.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trolleysuitcase.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trophy.d.ts","./node_modules/@phosphor-icons/react/dist/csr/truck.d.ts","./node_modules/@phosphor-icons/react/dist/csr/trucktrailer.d.ts","./node_modules/@phosphor-icons/react/dist/csr/tumblrlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/twitchlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/twitterlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/umbrella.d.ts","./node_modules/@phosphor-icons/react/dist/csr/umbrellasimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/union.d.ts","./node_modules/@phosphor-icons/react/dist/csr/unite.d.ts","./node_modules/@phosphor-icons/react/dist/csr/unitesquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/upload.d.ts","./node_modules/@phosphor-icons/react/dist/csr/uploadsimple.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usb.d.ts","./node_modules/@phosphor-icons/react/dist/csr/user.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usercheck.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usercircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usercirclecheck.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usercircledashed.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usercirclegear.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usercircleminus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usercircleplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/userfocus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usergear.d.ts","./node_modules/@phosphor-icons/react/dist/csr/userlist.d.ts","./node_modules/@phosphor-icons/react/dist/csr/userminus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/userplus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/userrectangle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usersound.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usersquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/userswitch.d.ts","./node_modules/@phosphor-icons/react/dist/csr/users.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usersfour.d.ts","./node_modules/@phosphor-icons/react/dist/csr/usersthree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/van.d.ts","./node_modules/@phosphor-icons/react/dist/csr/vault.d.ts","./node_modules/@phosphor-icons/react/dist/csr/vectorthree.d.ts","./node_modules/@phosphor-icons/react/dist/csr/vectortwo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/vibrate.d.ts","./node_modules/@phosphor-icons/react/dist/csr/video.d.ts","./node_modules/@phosphor-icons/react/dist/csr/videocamera.d.ts","./node_modules/@phosphor-icons/react/dist/csr/videocameraslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/videoconference.d.ts","./node_modules/@phosphor-icons/react/dist/csr/vignette.d.ts","./node_modules/@phosphor-icons/react/dist/csr/vinylrecord.d.ts","./node_modules/@phosphor-icons/react/dist/csr/virtualreality.d.ts","./node_modules/@phosphor-icons/react/dist/csr/virus.d.ts","./node_modules/@phosphor-icons/react/dist/csr/visor.d.ts","./node_modules/@phosphor-icons/react/dist/csr/voicemail.d.ts","./node_modules/@phosphor-icons/react/dist/csr/volleyball.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wall.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wallet.d.ts","./node_modules/@phosphor-icons/react/dist/csr/warehouse.d.ts","./node_modules/@phosphor-icons/react/dist/csr/warning.d.ts","./node_modules/@phosphor-icons/react/dist/csr/warningcircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/warningdiamond.d.ts","./node_modules/@phosphor-icons/react/dist/csr/warningoctagon.d.ts","./node_modules/@phosphor-icons/react/dist/csr/washingmachine.d.ts","./node_modules/@phosphor-icons/react/dist/csr/watch.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wavesawtooth.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wavesine.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wavesquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wavetriangle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/waveform.d.ts","./node_modules/@phosphor-icons/react/dist/csr/waveformslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/waves.d.ts","./node_modules/@phosphor-icons/react/dist/csr/webcam.d.ts","./node_modules/@phosphor-icons/react/dist/csr/webcamslash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/webhookslogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wechatlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/whatsapplogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wheelchair.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wheelchairmotion.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wifihigh.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wifilow.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wifimedium.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wifinone.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wifislash.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wifix.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wind.d.ts","./node_modules/@phosphor-icons/react/dist/csr/windmill.d.ts","./node_modules/@phosphor-icons/react/dist/csr/windowslogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wine.d.ts","./node_modules/@phosphor-icons/react/dist/csr/wrench.d.ts","./node_modules/@phosphor-icons/react/dist/csr/x.d.ts","./node_modules/@phosphor-icons/react/dist/csr/xcircle.d.ts","./node_modules/@phosphor-icons/react/dist/csr/xlogo.d.ts","./node_modules/@phosphor-icons/react/dist/csr/xsquare.d.ts","./node_modules/@phosphor-icons/react/dist/csr/yarn.d.ts","./node_modules/@phosphor-icons/react/dist/csr/yinyang.d.ts","./node_modules/@phosphor-icons/react/dist/csr/youtubelogo.d.ts","./node_modules/@phosphor-icons/react/dist/index.d.ts","./components/icon.tsx","./lib/source.ts","./app/sitemap.ts","./node_modules/fumadocs-core/dist/index-cq5tbozh.d.ts","./node_modules/@orama/orama/dist/esm/methods/insert.d.ts","./node_modules/@orama/orama/dist/esm/constants.d.ts","./node_modules/@orama/orama/dist/esm/components/internal-document-id-store.d.ts","./node_modules/@orama/orama/dist/esm/trees/radix.d.ts","./node_modules/@orama/orama/dist/esm/trees/avl.d.ts","./node_modules/@orama/orama/dist/esm/trees/flat.d.ts","./node_modules/@orama/orama/dist/esm/trees/bkd.d.ts","./node_modules/@orama/orama/dist/esm/trees/bool.d.ts","./node_modules/@orama/orama/dist/esm/trees/vector.d.ts","./node_modules/@orama/orama/dist/esm/components/index.d.ts","./node_modules/@orama/orama/dist/esm/components/sorter.d.ts","./node_modules/@orama/orama/dist/esm/components/tokenizer/languages.d.ts","./node_modules/@orama/orama/dist/esm/methods/answer-session.d.ts","./node_modules/@orama/orama/dist/esm/components/tokenizer/index.d.ts","./node_modules/@orama/orama/dist/esm/types.d.ts","./node_modules/@orama/orama/dist/esm/components/documents-store.d.ts","./node_modules/@orama/orama/dist/esm/methods/create.d.ts","./node_modules/@orama/orama/dist/esm/methods/docs.d.ts","./node_modules/@orama/orama/dist/esm/components/pinning.d.ts","./node_modules/@orama/orama/dist/esm/methods/pinning.d.ts","./node_modules/@orama/orama/dist/esm/methods/remove.d.ts","./node_modules/@orama/orama/dist/esm/methods/search.d.ts","./node_modules/@orama/orama/dist/esm/methods/search-vector.d.ts","./node_modules/@orama/orama/dist/esm/methods/serialization.d.ts","./node_modules/@orama/orama/dist/esm/methods/update.d.ts","./node_modules/@orama/orama/dist/esm/methods/upsert.d.ts","./node_modules/@orama/orama/dist/esm/utils.d.ts","./node_modules/@orama/orama/dist/esm/components/defaults.d.ts","./node_modules/@orama/orama/dist/esm/components.d.ts","./node_modules/@orama/orama/dist/esm/components/levenshtein.d.ts","./node_modules/@orama/orama/dist/esm/internals.d.ts","./node_modules/@orama/orama/dist/esm/index.d.ts","./node_modules/fumadocs-core/dist/server-b89im_4v.d.ts","./node_modules/fumadocs-core/dist/search/server.d.ts","./app/api/search/route.ts","./lib/get-llm-text.ts","./app/llms-full.txt/route.ts","./app/llms.mdx/[[...slug]]/route.ts","./node_modules/tailwind-merge/dist/types.d.ts","./lib/cn.ts","./node_modules/fumadocs-ui/dist/components/callout.d.ts","./node_modules/fumadocs-ui/dist/components/card.d.ts","./node_modules/@radix-ui/react-context/dist/index.d.mts","./node_modules/@radix-ui/react-primitive/dist/index.d.mts","./node_modules/@radix-ui/react-roving-focus/dist/index.d.mts","./node_modules/@radix-ui/react-tabs/dist/index.d.mts","./node_modules/fumadocs-ui/dist/components/ui/tabs.d.ts","./node_modules/fumadocs-ui/dist/components/codeblock.d.ts","./node_modules/fumadocs-ui/dist/mdx.server.d.ts","./node_modules/fumadocs-ui/dist/mdx.d.ts","./node_modules/fumadocs-ui/dist/components/steps.d.ts","./node_modules/fumadocs-ui/dist/components/tabs.d.ts","./components/provider-icons.tsx","./components/channel-icons.tsx","./node_modules/@radix-ui/react-collapsible/dist/index.d.mts","./node_modules/@radix-ui/react-accordion/dist/index.d.mts","./node_modules/fumadocs-ui/dist/components/ui/accordion.d.ts","./node_modules/fumadocs-ui/dist/components/accordion.d.ts","./node_modules/fumadocs-ui/dist/components/files.d.ts","./mdx-components.tsx","./node_modules/fumadocs-ui/dist/contexts/search.d.ts","./node_modules/fumadocs-ui/dist/components/dialog/search-default.d.ts","./node_modules/fumadocs-ui/dist/contexts/i18n.d.ts","./node_modules/next-themes/dist/index.d.ts","./node_modules/fumadocs-ui/dist/provider/base.d.ts","./node_modules/fumadocs-core/dist/framework/index.d.ts","./node_modules/fumadocs-ui/dist/provider/next.d.ts","./node_modules/clsx/clsx.d.mts","./node_modules/class-variance-authority/dist/types.d.ts","./node_modules/class-variance-authority/dist/index.d.ts","./node_modules/fumadocs-ui/dist/components/ui/button.d.ts","./node_modules/fumadocs-ui/dist/layouts/shared/slots/search-trigger.d.ts","./node_modules/fumadocs-ui/dist/layouts/shared/slots/theme-switch.d.ts","./node_modules/fumadocs-ui/dist/layouts/shared/slots/language-select.d.ts","./node_modules/fumadocs-ui/dist/layouts/shared/client.d.ts","./node_modules/fumadocs-core/dist/index-d2xxhp9t.d.ts","./node_modules/fumadocs-core/dist/page-tree/index.d.ts","./node_modules/fumadocs-core/dist/i18n/index.d.ts","./node_modules/fumadocs-ui/dist/layouts/shared/index.d.ts","./node_modules/fumadocs-ui/dist/components/ui/collapsible.d.ts","./node_modules/@radix-ui/react-scroll-area/dist/index.d.mts","./node_modules/fumadocs-ui/dist/components/ui/scroll-area.d.ts","./node_modules/fumadocs-core/dist/link.d.ts","./node_modules/fumadocs-ui/dist/components/sidebar/base.d.ts","./node_modules/fumadocs-ui/dist/components/sidebar/page-tree.d.ts","./node_modules/fumadocs-ui/dist/layouts/docs/slots/sidebar.d.ts","./node_modules/fumadocs-ui/dist/layouts/docs/client.d.ts","./node_modules/fumadocs-ui/dist/layouts/docs/index.d.ts","./node_modules/fumadocs-ui/dist/components/github-info.d.ts","./app/components/footer.tsx","./node_modules/swetrix/dist/esnext/lib.d.ts","./node_modules/swetrix/dist/esnext/index.d.ts","./app/components/analytics.tsx","./app/layout.tsx","./node_modules/fumadocs-core/dist/breadcrumb.d.ts","./node_modules/fumadocs-ui/dist/layouts/docs/page/slots/breadcrumb.d.ts","./node_modules/fumadocs-ui/dist/components/toc/index.d.ts","./node_modules/fumadocs-ui/dist/layouts/docs/page/slots/toc.d.ts","./node_modules/fumadocs-ui/dist/layouts/docs/page/slots/footer.d.ts","./node_modules/@radix-ui/react-dismissable-layer/dist/index.d.mts","./node_modules/@radix-ui/react-focus-scope/dist/index.d.mts","./node_modules/@radix-ui/react-arrow/dist/index.d.mts","./node_modules/@radix-ui/rect/dist/index.d.mts","./node_modules/@radix-ui/react-popper/dist/index.d.mts","./node_modules/@radix-ui/react-portal/dist/index.d.mts","./node_modules/@radix-ui/react-popover/dist/index.d.mts","./node_modules/fumadocs-ui/dist/components/ui/popover.d.ts","./node_modules/fumadocs-ui/dist/layouts/shared/page-actions.d.ts","./node_modules/fumadocs-ui/dist/layouts/docs/page/index.d.ts","./node_modules/fumadocs-ui/dist/page.d.ts","./components/feedbackwidget.tsx","./node_modules/simple-icons/data/simple-icons.d.ts","./node_modules/simple-icons/types.d.ts","./node_modules/simple-icons/index.d.ts","./components/googlegicon.tsx","./components/integrationsgrid.tsx","./node_modules/fumadocs-ui/dist/utils/use-copy-button.d.ts","./components/ui/button.tsx","./components/ui/popover.tsx","./components/page-actions.tsx","./app/[[...slug]]/page.tsx","./node_modules/fumadocs-mdx/dist/build-mdx-caxqh1_c.d.ts","./node_modules/fumadocs-mdx/dist/runtime/browser.d.ts","./.source/browser.ts","./node_modules/fumadocs-mdx/dist/runtime/dynamic.d.ts","./.source/dynamic.ts","./.next/types/routes.d.ts","./.next/types/validator.ts","./.next/dev/types/cache-life.d.ts","./.next/dev/types/validator.ts","./node_modules/@types/ms/index.d.ts","./node_modules/@types/debug/index.d.ts","./node_modules/@types/mdx/index.d.ts"],"fileIdsList":[[94,157,165,169,172,174,175,176,188,505,506,507,508],[94,157,165,169,172,174,175,176,188],[94,157,165,169,172,174,175,176,188,248,546,549,552,3891,3893,3894,3950,3977],[94,157,165,169,172,174,175,176,188,248,546,549,3891,3893,3894,3950,3977,3983],[94,157,165,169,172,174,175,176,188,248,818,820,3979],[94,157,165,169,172,174,175,176,188,248,818,820,3981],[94,157,165,169,172,174,175,176,188,248,818,819,820],[94,157,165,169,172,174,175,176,188,248,539,3854,3916,3966,3967,3972,3976],[94,157,165,169,172,174,175,176,188,248,3854,3890],[85,94,157,165,169,172,174,175,176,188,248,3948],[85,94,157,165,169,172,174,175,176,188,248,3852],[85,94,157,165,169,172,174,175,176,188,248,547,3854,3923,3944,3945,3946,3949],[94,157,165,169,172,174,175,176,188,248,3854,3892],[94,157,165,169,172,174,175,176,188,248,539,3854,3892],[94,157,165,169,172,174,175,176,188,248,550,3854],[94,157,165,169,172,174,175,176,188,248,2339],[85,94,157,165,169,172,174,175,176,188,248,529,539,3852,3948],[85,94,157,165,169,172,174,175,176,188,248],[85,94,157,165,169,172,174,175,176,188,248,529,3898,3970,3971],[85,94,157,165,169,172,174,175,176,188,248,3852,3896,3973,3974,3975],[85,94,157,165,169,172,174,175,176,188,248,2339],[94,157,165,169,172,174,175,176,188,248,3926],[85,94,157,165,169,172,174,175,176,188,248,3896,3962],[94,157,165,169,172,174,175,176,188,248,3895],[94,157,165,169,172,174,175,176,188,248,815,3854],[85,94,157,165,169,172,174,175,176,188,248,815,821,3853],[94,157,165,169,172,174,175,176,188,248,608,2339,3897,3906,3907,3908,3909,3910,3914,3915],[94,157,165,169,172,174,175,176,188,550,551,552],[94,157,165,169,172,174,175,176,188,797,798,799,802,803,804,805],[94,157,165,169,172,174,175,176,188,597,604,740,743,797,3978],[94,157,165,169,172,174,175,176,188,555,557,587,589,591,602,603,604,740,744,753,766,794,796],[94,157,165,169,172,174,175,176,188,597,604,608,740,743,803,3906,3978],[94,157,165,169,172,174,175,176,188,608,803,3906],[94,157,165,169,172,174,175,176,188,608,799,802,3906],[94,157,165,169,172,174,175,176,188,3859,3866,3867,3870,3872,3884],[94,157,165,169,172,174,175,176,188,3863,3871,3883],[94,157,165,169,172,174,175,176,188,3859,3871],[94,157,165,169,172,174,175,176,188,3857,3859,3860,3861,3862,3863,3864,3865,3871],[94,157,165,169,172,174,175,176,188,3871],[94,157,165,169,172,174,175,176,188,3859],[94,157,165,169,172,174,175,176,188,3868,3871],[94,157,165,169,172,174,175,176,188,3857,3868,3869,3871,3873,3874,3875,3876,3877,3878,3879,3880,3881,3882,3885,3887],[94,157,165,169,172,174,175,176,188,3870,3883,3886],[94,157,165,169,172,174,175,176,188,3866,3867,3871,3872],[94,157,165,169,172,174,175,176,188,3871,3875],[94,157,165,169,172,174,175,176,188,3865,3871,3888],[94,157,165,169,172,174,175,176,188,3871,3888],[94,157,165,169,172,174,175,176,188,3857,3871],[94,157,165,169,172,174,175,176,188,3857,3858,3859,3863,3865,3866,3867,3868,3869,3870,3872],[94,157,165,169,172,174,175,176,188,822],[94,157,165,169,172,174,175,176,188,826,2339,2340,2341,2342,2343,2344,2345,2346,2347,2348,2349,2350,2351,2352,2353,2354,2355,2356,2357,2358,2359,2360,2361,2362,2363,2364,2365,2366,2367,2368,2369,2370,2371,2372,2373,2374,2375,2376,2377,2378,2379,2380,2381,2382,2383,2384,2385,2386,2387,2388,2389,2390,2391,2392,2393,2394,2395,2396,2397,2398,2399,2400,2401,2402,2403,2404,2405,2406,2407,2408,2409,2410,2411,2412,2413,2414,2415,2416,2417,2418,2419,2420,2421,2422,2423,2424,2425,2426,2427,2428,2429,2430,2431,2432,2433,2434,2435,2436,2437,2438,2439,2440,2441,2442,2443,2444,2445,2446,2447,2448,2449,2450,2451,2452,2453,2454,2455,2456,2457,2458,2459,2460,2461,2462,2463,2464,2465,2466,2467,2468,2469,2470,2471,2472,2473,2474,2475,2476,2477,2478,2479,2480,2481,2482,2483,2484,2485,2486,2487,2488,2489,2490,2491,2492,2493,2494,2495,2496,2497,2498,2499,2500,2501,2502,2503,2504,2505,2506,2507,2508,2509,2510,2511,2512,2513,2514,2515,2516,2517,2518,2519,2520,2521,2522,2523,2524,2525,2526,2527,2528,2529,2530,2531,2532,2533,2534,2535,2536,2537,2538,2539,2540,2541,2542,2543,2544,2545,2546,2547,2548,2549,2550,2551,2552,2553,2554,2555,2556,2557,2558,2559,2560,2561,2562,2563,2564,2565,2566,2567,2568,2569,2570,2571,2572,2573,2574,2575,2576,2577,2578,2579,2580,2581,2582,2583,2584,2585,2586,2587,2588,2589,2590,2591,2592,2593,2594,2595,2596,2597,2598,2599,2600,2601,2602,2603,2604,2605,2606,2607,2608,2609,2610,2611,2612,2613,2614,2615,2616,2617,2618,2619,2620,2621,2622,2623,2624,2625,2626,2627,2628,2629,2630,2631,2632,2633,2634,2635,2636,2637,2638,2639,2640,2641,2642,2643,2644,2645,2646,2647,2648,2649,2650,2651,2652,2653,2654,2655,2656,2657,2658,2659,2660,2661,2662,2663,2664,2665,2666,2667,2668,2669,2670,2671,2672,2673,2674,2675,2676,2677,2678,2679,2680,2681,2682,2683,2684,2685,2686,2687,2688,2689,2690,2691,2692,2693,2694,2695,2696,2697,2698,2699,2700,2701,2702,2703,2704,2705,2706,2707,2708,2709,2710,2711,2712,2713,2714,2715,2716,2717,2718,2719,2720,2721,2722,2723,2724,2725,2726,2727,2728,2729,2730,2731,2732,2733,2734,2735,2736,2737,2738,2739,2740,2741,2742,2743,2744,2745,2746,2747,2748,2749,2750,2751,2752,2753,2754,2755,2756,2757,2758,2759,2760,2761,2762,2763,2764,2765,2766,2767,2768,2769,2770,2771,2772,2773,2774,2775,2776,2777,2778,2779,2780,2781,2782,2783,2784,2785,2786,2787,2788,2789,2790,2791,2792,2793,2794,2795,2796,2797,2798,2799,2800,2801,2802,2803,2804,2805,2806,2807,2808,2809,2810,2811,2812,2813,2814,2815,2816,2817,2818,2819,2820,2821,2822,2823,2824,2825,2826,2827,2828,2829,2830,2831,2832,2833,2834,2835,2836,2837,2838,2839,2840,2841,2842,2843,2844,2845,2846,2847,2848,2849,2850,2851,2852,2853,2854,2855,2856,2857,2858,2859,2860,2861,2862,2863,2864,2865,2866,2867,2868,2869,2870,2871,2872,2873,2874,2875,2876,2877,2878,2879,2880,2881,2882,2883,2884,2885,2886,2887,2888,2889,2890,2891,2892,2893,2894,2895,2896,2897,2898,2899,2900,2901,2902,2903,2904,2905,2906,2907,2908,2909,2910,2911,2912,2913,2914,2915,2916,2917,2918,2919,2920,2921,2922,2923,2924,2925,2926,2927,2928,2929,2930,2931,2932,2933,2934,2935,2936,2937,2938,2939,2940,2941,2942,2943,2944,2945,2946,2947,2948,2949,2950,2951,2952,2953,2954,2955,2956,2957,2958,2959,2960,2961,2962,2963,2964,2965,2966,2967,2968,2969,2970,2971,2972,2973,2974,2975,2976,2977,2978,2979,2980,2981,2982,2983,2984,2985,2986,2987,2988,2989,2990,2991,2992,2993,2994,2995,2996,2997,2998,2999,3000,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023,3024,3025,3026,3027,3028,3029,3030,3031,3032,3033,3034,3035,3036,3037,3038,3039,3040,3041,3042,3043,3044,3045,3046,3047,3048,3049,3050,3051,3052,3053,3054,3055,3056,3057,3058,3059,3060,3061,3062,3063,3064,3065,3066,3067,3068,3069,3070,3071,3072,3073,3074,3075,3076,3077,3078,3079,3080,3081,3082,3083,3084,3085,3086,3087,3088,3089,3090,3091,3092,3093,3094,3095,3096,3097,3098,3099,3100,3101,3102,3103,3104,3105,3106,3107,3108,3109,3110,3111,3112,3113,3114,3115,3116,3117,3118,3119,3120,3121,3122,3123,3124,3125,3126,3127,3128,3129,3130,3131,3132,3133,3134,3135,3136,3137,3138,3139,3140,3141,3142,3143,3144,3145,3146,3147,3148,3149,3150,3151,3152,3153,3154,3155,3156,3157,3158,3159,3160,3161,3162,3163,3164,3165,3166,3167,3168,3169,3170,3171,3172,3173,3174,3175,3176,3177,3178,3179,3180,3181,3182,3183,3184,3185,3186,3187,3188,3189,3190,3191,3192,3193,3194,3195,3196,3197,3198,3199,3200,3201,3202,3203,3204,3205,3206,3207,3208,3209,3210,3211,3212,3213,3214,3215,3216,3217,3218,3219,3220,3221,3222,3223,3224,3225,3226,3227,3228,3229,3230,3231,3232,3233,3234,3235,3236,3237,3238,3239,3240,3241,3242,3243,3244,3245,3246,3247,3248,3249,3250,3251,3252,3253,3254,3255,3256,3257,3258,3259,3260,3261,3262,3263,3264,3265,3266,3267,3268,3269,3270,3271,3272,3273,3274,3275,3276,3277,3278,3279,3280,3281,3282,3283,3284,3285,3286,3287,3288,3289,3290,3291,3292,3293,3294,3295,3296,3297,3298,3299,3300,3301,3302,3303,3304,3305,3306,3307,3308,3309,3310,3311,3312,3313,3314,3315,3316,3317,3318,3319,3320,3321,3322,3323,3324,3325,3326,3327,3328,3329,3330,3331,3332,3333,3334,3335,3336,3337,3338,3339,3340,3341,3342,3343,3344,3345,3346,3347,3348,3349,3350,3351,3352,3353,3354,3355,3356,3357,3358,3359,3360,3361,3362,3363,3364,3365,3366,3367,3368,3369,3370,3371,3372,3373,3374,3375,3376,3377,3378,3379,3380,3381,3382,3383,3384,3385,3386,3387,3388,3389,3390,3391,3392,3393,3394,3395,3396,3397,3398,3399,3400,3401,3402,3403,3404,3405,3406,3407,3408,3409,3410,3411,3412,3413,3414,3415,3416,3417,3418,3419,3420,3421,3422,3423,3424,3425,3426,3427,3428,3429,3430,3431,3432,3433,3434,3435,3436,3437,3438,3439,3440,3441,3442,3443,3444,3445,3446,3447,3448,3449,3450,3451,3452,3453,3454,3455,3456,3457,3458,3459,3460,3461,3462,3463,3464,3465,3466,3467,3468,3469,3470,3471,3472,3473,3474,3475,3476,3477,3478,3479,3480,3481,3482,3483,3484,3485,3486,3487,3488,3489,3490,3491,3492,3493,3494,3495,3496,3497,3498,3499,3500,3501,3502,3503,3504,3505,3506,3507,3508,3509,3510,3511,3512,3513,3514,3515,3516,3517,3518,3519,3520,3521,3522,3523,3524,3525,3526,3527,3528,3529,3530,3531,3532,3533,3534,3535,3536,3537,3538,3539,3540,3541,3542,3543,3544,3545,3546,3547,3548,3549,3550,3551,3552,3553,3554,3555,3556,3557,3558,3559,3560,3561,3562,3563,3564,3565,3566,3567,3568,3569,3570,3571,3572,3573,3574,3575,3576,3577,3578,3579,3580,3581,3582,3583,3584,3585,3586,3587,3588,3589,3590,3591,3592,3593,3594,3595,3596,3597,3598,3599,3600,3601,3602,3603,3604,3605,3606,3607,3608,3609,3610,3611,3612,3613,3614,3615,3616,3617,3618,3619,3620,3621,3622,3623,3624,3625,3626,3627,3628,3629,3630,3631,3632,3633,3634,3635,3636,3637,3638,3639,3640,3641,3642,3643,3644,3645,3646,3647,3648,3649,3650,3651,3652,3653,3654,3655,3656,3657,3658,3659,3660,3661,3662,3663,3664,3665,3666,3667,3668,3669,3670,3671,3672,3673,3674,3675,3676,3677,3678,3679,3680,3681,3682,3683,3684,3685,3686,3687,3688,3689,3690,3691,3692,3693,3694,3695,3696,3697,3698,3699,3700,3701,3702,3703,3704,3705,3706,3707,3708,3709,3710,3711,3712,3713,3714,3715,3716,3717,3718,3719,3720,3721,3722,3723,3724,3725,3726,3727,3728,3729,3730,3731,3732,3733,3734,3735,3736,3737,3738,3739,3740,3741,3742,3743,3744,3745,3746,3747,3748,3749,3750,3751,3752,3753,3754,3755,3756,3757,3758,3759,3760,3761,3762,3763,3764,3765,3766,3767,3768,3769,3770,3771,3772,3773,3774,3775,3776,3777,3778,3779,3780,3781,3782,3783,3784,3785,3786,3787,3788,3789,3790,3791,3792,3793,3794,3795,3796,3797,3798,3799,3800,3801,3802,3803,3804,3805,3806,3807,3808,3809,3810,3811,3812,3813,3814,3815,3816,3817,3818,3819,3820,3821,3822,3823,3824,3825,3826,3827,3828,3829,3830,3831,3832,3833,3834,3835,3836,3837,3838,3839,3840,3841,3842,3843,3844,3845,3846,3847,3848,3849,3850,3851],[85,94,157,165,169,172,174,175,176,188,822],[94,157,165,169,172,174,175,176,188,822,823,824,825],[85,94,157,165,169,172,174,175,176,188],[94,157,165,169,172,174,175,176,188,825,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119,1120,1121,1122,1123,1124,1125,1126,1127,1128,1129,1130,1131,1132,1133,1134,1135,1136,1137,1138,1139,1140,1141,1142,1143,1144,1145,1146,1147,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186,1187,1188,1189,1190,1191,1192,1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205,1206,1207,1208,1209,1210,1211,1212,1213,1214,1215,1216,1217,1218,1219,1220,1221,1222,1223,1224,1225,1226,1227,1228,1229,1230,1231,1232,1233,1234,1235,1236,1237,1238,1239,1240,1241,1242,1243,1244,1245,1246,1247,1248,1249,1250,1251,1252,1253,1254,1255,1256,1257,1258,1259,1260,1261,1262,1263,1264,1265,1266,1267,1268,1269,1270,1271,1272,1273,1274,1275,1276,1277,1278,1279,1280,1281,1282,1283,1284,1285,1286,1287,1288,1289,1290,1291,1292,1293,1294,1295,1296,1297,1298,1299,1300,1301,1302,1303,1304,1305,1306,1307,1308,1309,1310,1311,1312,1313,1314,1315,1316,1317,1318,1319,1320,1321,1322,1323,1324,1325,1326,1327,1328,1329,1330,1331,1332,1333,1334,1335,1336,1337,1338,1339,1340,1341,1342,1343,1344,1345,1346,1347,1348,1349,1350,1351,1352,1353,1354,1355,1356,1357,1358,1359,1360,1361,1362,1363,1364,1365,1366,1367,1368,1369,1370,1371,1372,1373,1374,1375,1376,1377,1378,1379,1380,1381,1382,1383,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412,1413,1414,1415,1416,1417,1418,1419,1420,1421,1422,1423,1424,1425,1426,1427,1428,1429,1430,1431,1432,1433,1434,1435,1436,1437,1438,1439,1440,1441,1442,1443,1444,1445,1446,1447,1448,1449,1450,1451,1452,1453,1454,1455,1456,1457,1458,1459,1460,1461,1462,1463,1464,1465,1466,1467,1468,1469,1470,1471,1472,1473,1474,1475,1476,1477,1478,1479,1480,1481,1482,1483,1484,1485,1486,1487,1488,1489,1490,1491,1492,1493,1494,1495,1496,1497,1498,1499,1500,1501,1502,1503,1504,1505,1506,1507,1508,1509,1510,1511,1512,1513,1514,1515,1516,1517,1518,1519,1520,1521,1522,1523,1524,1525,1526,1527,1528,1529,1530,1531,1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543,1544,1545,1546,1547,1548,1549,1550,1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562,1563,1564,1565,1566,1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578,1579,1580,1581,1582,1583,1584,1585,1586,1587,1588,1589,1590,1591,1592,1593,1594,1595,1596,1597,1598,1599,1600,1601,1602,1603,1604,1605,1606,1607,1608,1609,1610,1611,1612,1613,1614,1615,1616,1617,1618,1619,1620,1621,1622,1623,1624,1625,1626,1627,1628,1629,1630,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640,1641,1642,1643,1644,1645,1646,1647,1648,1649,1650,1651,1652,1653,1654,1655,1656,1657,1658,1659,1660,1661,1662,1663,1664,1665,1666,1667,1668,1669,1670,1671,1672,1673,1674,1675,1676,1677,1678,1679,1680,1681,1682,1683,1684,1685,1686,1687,1688,1689,1690,1691,1692,1693,1694,1695,1696,1697,1698,1699,1700,1701,1702,1703,1704,1705,1706,1707,1708,1709,1710,1711,1712,1713,1714,1715,1716,1717,1718,1719,1720,1721,1722,1723,1724,1725,1726,1727,1728,1729,1730,1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741,1742,1743,1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755,1756,1757,1758,1759,1760,1761,1762,1763,1764,1765,1766,1767,1768,1769,1770,1771,1772,1773,1774,1775,1776,1777,1778,1779,1780,1781,1782,1783,1784,1785,1786,1787,1788,1789,1790,1791,1792,1793,1794,1795,1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806,1807,1808,1809,1810,1811,1812,1813,1814,1815,1816,1817,1818,1819,1820,1821,1822,1823,1824,1825,1826,1827,1828,1829,1830,1831,1832,1833,1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844,1845,1846,1847,1848,1849,1850,1851,1852,1853,1854,1855,1856,1857,1858,1859,1860,1861,1862,1863,1864,1865,1866,1867,1868,1869,1870,1871,1872,1873,1874,1875,1876,1877,1878,1879,1880,1881,1882,1883,1884,1885,1886,1887,1888,1889,1890,1891,1892,1893,1894,1895,1896,1897,1898,1899,1900,1901,1902,1903,1904,1905,1906,1907,1908,1909,1910,1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922,1923,1924,1925,1926,1927,1928,1929,1930,1931,1932,1933,1934,1935,1936,1937,1938,1939,1940,1941,1942,1943,1944,1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955,1956,1957,1958,1959,1960,1961,1962,1963,1964,1965,1966,1967,1968,1969,1970,1971,1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986,1987,1988,1989,1990,1991,1992,1993,1994,1995,1996,1997,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,2029,2030,2031,2032,2033,2034,2035,2036,2037,2038,2039,2040,2041,2042,2043,2044,2045,2046,2047,2048,2049,2050,2051,2052,2053,2054,2055,2056,2057,2058,2059,2060,2061,2062,2063,2064,2065,2066,2067,2068,2069,2070,2071,2072,2073,2074,2075,2076,2077,2078,2079,2080,2081,2082,2083,2084,2085,2086,2087,2088,2089,2090,2091,2092,2093,2094,2095,2096,2097,2098,2099,2100,2101,2102,2103,2104,2105,2106,2107,2108,2109,2110,2111,2112,2113,2114,2115,2116,2117,2118,2119,2120,2121,2122,2123,2124,2125,2126,2127,2128,2129,2130,2131,2132,2133,2134,2135,2136,2137,2138,2139,2140,2141,2142,2143,2144,2145,2146,2147,2148,2149,2150,2151,2152,2153,2154,2155,2156,2157,2158,2159,2160,2161,2162,2163,2164,2165,2166,2167,2168,2169,2170,2171,2172,2173,2174,2175,2176,2177,2178,2179,2180,2181,2182,2183,2184,2185,2186,2187,2188,2189,2190,2191,2192,2193,2194,2195,2196,2197,2198,2199,2200,2201,2202,2203,2204,2205,2206,2207,2208,2209,2210,2211,2212,2213,2214,2215,2216,2217,2218,2219,2220,2221,2222,2223,2224,2225,2226,2227,2228,2229,2230,2231,2232,2233,2234,2235,2236,2237,2238,2239,2240,2241,2242,2243,2244,2245,2246,2247,2248,2249,2250,2251,2252,2253,2254,2255,2256,2257,2258,2259,2260,2261,2262,2263,2264,2265,2266,2267,2268,2269,2270,2271,2272,2273,2274,2275,2276,2277,2278,2279,2280,2281,2282,2283,2284,2285,2286,2287,2288,2289,2290,2291,2292,2293,2294,2295,2296,2297,2298,2299,2300,2301,2302,2303,2304,2305,2306,2307,2308,2309,2310,2311,2312,2313,2314,2315,2316,2317,2318,2319,2320,2321,2322,2323,2324,2325,2326,2327,2328,2329,2330,2331,2332,2333,2334,2335,2336,2337,2338],[85,94,157,165,169,172,174,175,176,188,3899,3900,3911],[85,94,157,165,169,172,174,175,176,188,3900],[85,94,157,165,169,172,174,175,176,188,3899,3900],[85,94,157,165,169,172,174,175,176,188,3899,3900,3956,3957,3960,3961],[85,94,157,165,169,172,174,175,176,188,3899,3900,3958,3959],[85,94,157,165,169,172,174,175,176,188,3899,3900,3901],[94,157,165,169,172,174,175,176,188,558,587,589,591,706,708,719,794],[94,157,165,169,172,174,175,176,188,706],[94,157,165,169,172,174,175,176,188,706,727,729],[94,157,165,169,172,174,175,176,188,727,730,731],[94,157,165,169,172,174,175,176,188,705,706],[94,157,165,169,172,174,175,176,188,725],[94,157,165,169,172,174,175,176,188,558,587,589,591,705,706,794],[94,157,165,169,172,174,175,176,188,558,587,589,591,602,706,733,735,794],[94,157,165,169,172,174,175,176,188,558,587,589,591,733,794],[94,157,165,169,172,174,175,176,188,558,587,589,591,705,794],[94,157,165,169,172,174,175,176,188,3987],[94,157,165,169,172,174,175,176,188,556,557],[94,157,165,169,172,174,175,176,188,554],[94,157,165,169,172,174,175,176,188,608,3906,3989],[94,154,155,157,165,169,172,174,175,176,188],[94,156,157,165,169,172,174,175,176,188],[157,165,169,172,174,175,176,188],[94,157,165,169,172,174,175,176,188,196],[94,157,158,163,165,168,169,172,174,175,176,178,188,193,205],[94,157,158,159,165,168,169,172,174,175,176,188],[94,157,160,165,169,172,174,175,176,188,206],[94,157,161,162,165,169,172,174,175,176,179,188],[94,157,162,165,169,172,174,175,176,188,193,202],[94,157,163,165,168,169,172,174,175,176,178,188],[94,156,157,164,165,169,172,174,175,176,188],[94,157,165,166,169,172,174,175,176,188],[94,157,165,167,168,169,172,174,175,176,188],[94,156,157,165,168,169,172,174,175,176,188],[94,157,165,168,169,170,172,174,175,176,188,193,205],[94,157,165,168,169,170,172,174,175,176,188,193,196],[94,144,157,165,168,169,171,172,174,175,176,178,188,193,205],[94,157,165,168,169,171,172,174,175,176,178,188,193,202,205],[94,157,165,169,171,172,173,174,175,176,188,193,202,205],[92,93,94,95,96,97,98,99,100,101,102,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212],[94,157,165,168,169,172,174,175,176,188],[94,157,165,169,172,174,176,188],[94,157,165,169,172,174,175,176,177,188,205],[94,157,165,168,169,172,174,175,176,178,188,193],[94,157,165,169,172,174,175,176,179,188],[94,157,165,169,172,174,175,176,180,188],[94,157,165,168,169,172,174,175,176,183,188],[94,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212],[94,157,165,169,172,174,175,176,185,188],[94,157,165,169,172,174,175,176,186,188],[94,157,162,165,169,172,174,175,176,178,188,196],[94,157,165,168,169,172,174,175,176,188,189],[94,157,165,169,172,174,175,176,188,190,206,209],[94,157,165,168,169,172,174,175,176,188,193,195,196],[94,157,165,169,172,174,175,176,188,194,196],[94,157,165,169,172,174,175,176,188,196,206],[94,157,165,169,172,174,175,176,188,197],[94,154,157,165,169,172,174,175,176,188,193,199,205],[94,157,165,169,172,174,175,176,188,193,198],[94,157,165,168,169,172,174,175,176,188,200,201],[94,157,165,169,172,174,175,176,188,200,201],[94,157,162,165,169,172,174,175,176,178,188,193,202],[94,157,165,169,172,174,175,176,188,203],[94,157,165,169,172,174,175,176,178,188,204],[94,157,165,169,171,172,174,175,176,186,188,205],[94,157,165,169,172,174,175,176,188,206,207],[94,157,162,165,169,172,174,175,176,188,207],[94,157,165,169,172,174,175,176,188,193,208],[94,157,165,169,172,174,175,176,177,188,209],[94,157,165,169,172,174,175,176,188,210],[94,157,160,165,169,172,174,175,176,188],[94,157,162,165,169,172,174,175,176,188],[94,157,165,169,172,174,175,176,188,206],[94,144,157,165,169,172,174,175,176,188],[94,157,165,169,172,174,175,176,188,205],[94,157,165,169,172,174,175,176,188,211],[94,157,165,169,172,174,175,176,183,188],[94,157,165,169,172,174,175,176,188,201],[94,144,157,165,168,169,170,172,174,175,176,183,188,193,196,205,208,209,211],[94,157,165,169,172,174,175,176,188,193,212],[85,89,94,157,165,169,172,174,175,176,188,214,215,216,218,500,545],[85,89,94,157,165,169,172,174,175,176,188,214,215,216,217,481,500,545],[85,89,94,157,165,169,172,174,175,176,188,214,215,217,218,500,545],[85,94,157,165,169,172,174,175,176,188,218,481,482],[85,94,157,165,169,172,174,175,176,188,218,481],[85,89,94,157,165,169,172,174,175,176,188,215,216,217,218,500,545],[85,89,94,157,165,169,172,174,175,176,188,214,216,217,218,500,545],[83,84,94,157,165,169,172,174,175,176,188],[94,157,165,169,172,174,175,176,188,807,809],[94,157,165,168,169,172,174,175,176,188,807,808],[94,157,165,169,172,174,175,176,188,3924,3925],[94,157,165,169,172,174,175,176,188,3924],[85,94,157,165,169,172,174,175,176,188,811],[94,157,165,169,172,174,175,176,188,555,587,589,591,593,603,604,740,744,794],[85,94,157,165,169,172,174,175,176,188,248,524],[94,157,165,169,172,174,175,176,188,812],[94,157,165,169,172,174,175,176,188,811],[94,157,165,169,172,174,175,176,188,558,587,589,591,704,733,794],[85,94,157,165,169,172,174,175,176,188,811,812,813],[94,157,165,169,172,174,175,176,188,604,703,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751],[94,157,165,169,172,174,175,176,188,604],[94,157,165,169,172,174,175,176,188,811,3932],[94,157,165,169,172,174,175,176,188,558,587,589,591,602,737,794],[94,157,165,169,172,174,175,176,188,558,587,589,591,602,704,733,734,736,794],[94,157,165,169,172,174,175,176,188,558,587,589,591,597,602,604,740,794,3978],[94,157,165,169,172,174,175,176,188,555,587,589,591,602,603,604,740,744,794],[94,157,165,169,172,174,175,176,188,555,587,589,591,593,602,603,604,740,744,794],[94,157,165,169,172,174,175,176,188,702],[94,157,165,169,172,174,175,176,188,555,587,589,591,597,602,603,604,606,740,743,744,794,3978],[94,157,165,169,172,174,175,176,188,555,585,587,589,591,593,602,603,604,695,698,740,744,794],[94,157,165,169,172,174,175,176,188,555,587,589,591,593,597,602,603,604,740,743,744,794,3978],[94,157,165,169,172,174,175,176,188,3889],[94,157,165,169,172,174,175,176,188,604,704,812,814,3856,3888],[94,157,165,169,172,174,175,176,188,814],[94,157,165,169,172,174,175,176,188,606],[85,94,157,165,169,172,174,175,176,188,597,604,607,608,740,743,752,806,3906,3978],[94,157,165,169,172,174,175,176,188,816],[94,157,165,169,172,174,175,176,188,555,587,589,591,597,602,603,604,609,686,740,743,744,752,794,806,809,810,815,3978],[85,94,157,165,169,172,174,175,176,188,609,810,816,3978],[94,157,165,169,172,174,175,176,188,609,810,815,816],[94,157,165,169,172,174,175,176,188,609],[94,157,165,169,172,174,175,176,188,555,587,589,591,603,604,605,607,608,740,744,794,3906],[85,94,157,165,169,172,174,175,176,188,248,3913],[85,94,157,165,169,172,174,175,176,188,248,3903],[85,94,157,165,169,172,174,175,176,188,248,3917],[85,94,157,165,169,172,174,175,176,188,248,3936,3938,3939],[85,94,157,165,169,172,174,175,176,188,248,3933,3940],[85,94,157,165,169,172,174,175,176,188,248,607],[85,94,157,165,169,172,174,175,176,188,248,3912],[94,157,165,169,172,174,175,176,188,3925,3926],[85,94,157,165,169,172,174,175,176,188,248,3911],[85,94,157,165,169,172,174,175,176,188,248,3962],[85,94,157,165,169,172,174,175,176,188,248,3937],[85,94,157,165,169,172,174,175,176,188,248,3902],[85,94,157,165,169,172,174,175,176,188,3931,3935,3942,3944],[85,94,157,165,169,172,174,175,176,188,248,3933,3935,3942,3943],[85,94,157,165,169,172,174,175,176,188,248,607,3952,3954,3955,3964],[85,94,157,165,169,172,174,175,176,188,248,3951],[85,94,157,165,169,172,174,175,176,188,248,3933],[85,94,157,165,169,172,174,175,176,188,248,3953],[85,94,157,165,169,172,174,175,176,188,248,3940,3941],[85,94,157,165,169,172,174,175,176,188,248,3928,3929,3930,3935],[85,94,157,165,169,172,174,175,176,188,3928,3929,3931,3933,3934],[85,94,157,165,169,172,174,175,176,188,248,3963],[85,94,157,165,169,172,174,175,176,188,248,3926,3927],[85,94,157,165,169,172,174,175,176,188,248,3927],[85,94,157,165,169,172,174,175,176,188,248,608,3897,3898,3904,3905],[85,94,157,165,169,172,174,175,176,188,815,3906],[85,94,157,165,169,172,174,175,176,188,248,3952,3965],[85,94,157,165,169,172,174,175,176,188,248,3917,3918,3919,3920],[85,94,157,165,169,172,174,175,176,188,248,3921,3922],[94,157,165,169,172,174,175,176,188,761,762,763],[94,157,165,169,172,174,175,176,188,557,558,587,589,591,764,794],[94,157,165,169,172,174,175,176,188,754,755,756,757,758,759,760],[94,157,165,169,172,174,175,176,188,557,587,764],[94,157,165,169,172,174,175,176,188,557,589,764],[94,157,165,169,172,174,175,176,188,591,764],[94,157,165,169,172,174,175,176,188,556,557,558,587,589,591,764,794],[94,157,165,169,172,174,175,176,188,556,557,558,587,589,591,717,794],[94,157,165,169,172,174,175,176,188,718],[94,157,165,169,172,174,175,176,188,558,587,589,591,712,717,794],[94,157,165,169,172,174,175,176,188,800,801],[94,157,165,169,172,174,175,176,188,558,587,589,591,794,800,802],[94,157,165,169,172,174,175,176,188,559,560,561,562,689,692],[94,157,165,169,172,174,175,176,188,555,559,560,562,587,589,591,603,604,689,692,740,744,794],[94,157,165,169,172,174,175,176,188,555,559,562,587,589,591,603,604,689,692,740,744,794],[94,157,165,169,172,174,175,176,188,585,589,694,698],[94,157,165,169,172,174,175,176,188,562,585,589,695,698],[94,157,165,169,172,174,175,176,188,562,585,589,695,697],[94,157,165,169,172,174,175,176,188,555,562,585,587,589,591,603,604,695,696,698,740,744,794],[94,157,165,169,172,174,175,176,188,695,698,699],[94,157,165,169,172,174,175,176,188,562,585,589,695,698,700],[94,157,165,169,172,174,175,176,188,555,557,558,586,587,589,591,603,604,740,744,794],[94,157,165,169,172,174,175,176,188,554,555,557,558,562,585,587,588,589,591,603,604,695,698,740,744,794],[94,157,165,169,172,174,175,176,188,587,589,591,592],[94,157,165,169,172,174,175,176,188,555,557,558,587,589,590,591,603,604,740,744,794],[94,157,165,169,172,174,175,176,188,562,585,589,591,695,698],[94,157,165,169,172,174,175,176,188,555,558,587,589,591,603,604,740,744,767,768,792,793,794],[94,157,165,169,172,174,175,176,188,558,587,589,591,767,794],[94,157,165,169,172,174,175,176,188,555,558,587,589,591,603,604,740,744,767,794],[94,157,165,169,172,174,175,176,188,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791],[94,157,165,169,172,174,175,176,188,555,558,587,589,591,597,603,604,740,743,744,768,794,3978],[94,157,165,169,172,174,175,176,188,563,564,584],[94,157,165,169,172,174,175,176,188,555,585,587,589,591,603,604,695,698,740,744,794],[94,157,165,169,172,174,175,176,188,555,587,589,591,603,604,740,744,794],[94,157,165,169,172,174,175,176,188,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583],[94,157,165,169,172,174,175,176,188,554,555,587,589,591,603,604,740,744,794],[94,157,165,169,172,174,175,176,188,559,562,687,688,692],[94,157,165,169,172,174,175,176,188,559,562,689,692],[94,157,165,169,172,174,175,176,188,559,562,689,690,691],[94,157,165,169,172,174,175,176,188,503],[94,157,165,169,172,174,175,176,188,451,514,515],[94,157,165,169,172,174,175,176,188,223,224,226,238,262,377,388,496],[94,157,165,169,172,174,175,176,188,226,257,258,259,261,496],[94,157,165,169,172,174,175,176,188,226,394,396,398,399,401,496,498],[94,157,165,169,172,174,175,176,188,226,260,297,496],[94,157,165,169,172,174,175,176,188,224,226,237,238,244,250,255,376,377,378,387,496,498],[94,157,165,169,172,174,175,176,188,496],[94,157,165,169,172,174,175,176,188,233,239,258,278,373],[94,157,165,169,172,174,175,176,188,226],[94,157,165,169,172,174,175,176,188,219,233,239],[94,157,165,169,172,174,175,176,188,405],[94,157,165,169,172,174,175,176,188,402,403,405],[94,157,165,169,172,174,175,176,188,402,404,496],[94,157,165,169,171,172,174,175,176,188,278,475,493],[94,157,165,169,171,172,174,175,176,188,349,352,368,373,493],[94,157,165,169,171,172,174,175,176,188,321,493],[94,157,165,169,172,174,175,176,188,381],[94,157,165,169,172,174,175,176,188,380,381,382],[94,157,165,169,172,174,175,176,188,380],[91,94,157,165,169,171,172,174,175,176,188,219,226,238,244,250,256,258,262,263,276,277,344,374,375,388,496,500],[94,157,165,169,172,174,175,176,188,223,226,260,297,394,395,400,496,548],[94,157,165,169,172,174,175,176,188,260,548],[94,157,165,169,172,174,175,176,188,223,277,446,496,548],[94,157,165,169,172,174,175,176,188,548],[94,157,165,169,172,174,175,176,188,226,260,261,548],[94,157,165,169,172,174,175,176,188,397,548],[94,157,165,169,172,174,175,176,188,263,376,379,386],[85,94,157,165,169,172,174,175,176,188,451],[94,157,165,169,172,174,175,176,186,188,233,248],[94,157,165,169,172,174,175,176,188,233,248],[85,94,157,165,169,172,174,175,176,188,318],[85,94,157,165,169,172,174,175,176,188,239,248,451],[94,157,165,169,172,174,175,176,188,233,304,318,319,530,537],[94,157,165,169,172,174,175,176,188,303,531,532,533,534,536],[94,157,165,169,172,174,175,176,188,354],[94,157,165,169,172,174,175,176,188,354,355],[94,157,165,169,172,174,175,176,188,237,239,306,307],[94,157,165,169,172,174,175,176,188,239,313,314],[94,157,165,169,172,174,175,176,188,239,308,316],[94,157,165,169,172,174,175,176,188,313],[94,157,165,169,172,174,175,176,188,231,239,306,307,308,309,310,311,312,313,316],[94,157,165,169,172,174,175,176,188,239,306,313,314,315,317],[94,157,165,169,172,174,175,176,188,239,307,309,310],[94,157,165,169,172,174,175,176,188,307,309,312,314],[94,157,165,169,172,174,175,176,188,535],[94,157,165,169,172,174,175,176,188,239],[85,94,157,165,169,172,174,175,176,188,227,524],[85,94,157,165,169,172,174,175,176,188,205],[85,94,157,165,169,172,174,175,176,188,260,295],[85,94,157,165,169,172,174,175,176,188,260,388],[94,157,165,169,172,174,175,176,188,293,298],[85,94,157,165,169,172,174,175,176,188,294,502],[85,89,94,157,165,169,171,172,174,175,176,188,214,215,216,217,218,500,544],[94,157,165,169,171,172,174,175,176,188,239],[94,157,165,169,171,172,174,175,176,188,238,243,324,341,383,384,388,443,445,496,497],[94,157,165,169,172,174,175,176,188,276,385],[94,157,165,169,172,174,175,176,188,500],[94,157,165,169,172,174,175,176,188,225],[85,94,157,165,169,172,174,175,176,188,230,233,448,464,466],[94,157,165,169,172,174,175,176,186,188,233,448,463,464,465,547],[94,157,165,169,172,174,175,176,188,457,458,459,460,461,462],[94,157,165,169,172,174,175,176,188,459],[94,157,165,169,172,174,175,176,188,463],[94,157,165,169,172,174,175,176,188,248,412,413,415],[85,94,157,165,169,172,174,175,176,188,239,406,407,408,409,414],[94,157,165,169,172,174,175,176,188,412,414],[94,157,165,169,172,174,175,176,188,410],[94,157,165,169,172,174,175,176,188,411],[85,94,157,165,169,172,174,175,176,188,248,294,502],[85,94,157,165,169,172,174,175,176,188,248,501,502],[85,94,157,165,169,172,174,175,176,188,248,502],[94,157,165,169,172,174,175,176,188,341,342],[94,157,165,169,172,174,175,176,188,342],[94,157,165,169,171,172,174,175,176,188,497,502],[94,157,165,169,172,174,175,176,188,371],[94,156,157,165,169,172,174,175,176,188,370],[94,157,165,169,172,174,175,176,188,233,239,245,247,349,362,366,368,445,448,485,486,493,497],[94,157,165,169,172,174,175,176,188,239,288,310],[94,157,165,169,172,174,175,176,188,349,360,363,368],[85,94,157,165,169,172,174,175,176,188,230,233,349,352,368,371,405,452,453,454,455,456,467,468,469,470,471,472,473,474,548],[94,157,165,169,172,174,175,176,188,230,233,258,349,356,357,358,361,362],[94,157,165,169,172,174,175,176,188,193,239,258,360,367,448,449,493],[94,157,165,169,172,174,175,176,188,364],[94,157,165,169,171,172,174,175,176,186,188,227,239,243,253,285,286,289,341,344,409,443,444,485,496,497,498,500,548],[94,157,165,169,172,174,175,176,188,230,231,233],[94,157,165,169,172,174,175,176,188,349],[94,156,157,165,169,172,174,175,176,188,258,285,286,343,344,345,346,347,348,497],[94,157,165,169,172,174,175,176,188,368],[94,156,157,165,169,172,174,175,176,188,232,233,243,247,283,349,356,357,358,359,360,363,364,365,366,367,486],[94,157,165,169,171,172,174,175,176,188,283,284,356,497,498],[94,157,165,169,172,174,175,176,188,258,286,341,344,349,445,497],[94,157,165,169,171,172,174,175,176,188,496,498],[94,157,165,169,171,172,174,175,176,188,193,493,497,498],[94,157,165,169,171,172,174,175,176,186,188,219,233,238,245,247,250,253,260,280,285,286,287,288,289,324,325,327,330,332,335,336,337,338,340,388,443,445,493,496,497,498],[94,157,165,169,171,172,174,175,176,188,193],[94,157,165,169,172,174,175,176,188,226,227,228,256,493,494,495,500,502,548],[94,157,165,169,172,174,175,176,188,223,224,496],[94,157,165,169,172,174,175,176,188,417],[94,157,165,169,171,172,174,175,176,188,193,205,235,401,405,406,407,408,409,415,416,548],[94,157,165,169,172,174,175,176,186,188,205,219,233,235,247,250,286,325,330,340,341,394,421,422,423,429,432,433,443,445,493,496],[94,157,165,169,172,174,175,176,188,250,256,263,276,286,344,496],[94,157,165,169,171,172,174,175,176,188,205,227,238,247,286,427,493,496],[94,157,165,169,172,174,175,176,188,447],[94,157,165,169,171,172,174,175,176,188,417,430,431,440],[94,157,165,169,172,174,175,176,188,493,496],[94,157,165,169,172,174,175,176,188,346,486],[94,157,165,169,172,174,175,176,188,247,285,388,502],[94,157,165,169,171,172,174,175,176,186,188,225,330,390,394,423,429,432,435,493],[94,157,165,169,171,172,174,175,176,188,263,276,394,436],[94,157,165,169,172,174,175,176,188,226,287,388,438,496,498],[94,157,165,169,171,172,174,175,176,188,205,409,496],[94,157,165,169,171,172,174,175,176,188,260,287,388,389,390,399,417,437,439,496],[91,94,157,165,169,171,172,174,175,176,188,285,442,500,502],[94,157,165,169,172,174,175,176,188,339,443],[94,157,165,169,171,172,174,175,176,186,188,233,236,238,239,245,247,253,262,263,276,286,289,325,327,337,340,341,388,421,422,423,424,426,428,443,445,493,502],[94,157,165,169,171,172,174,175,176,188,193,263,429,434,440,493],[94,157,165,169,172,174,175,176,188,266,267,268,269,270,271,272,273,274,275],[94,157,165,169,172,174,175,176,188,280,331],[94,157,165,169,172,174,175,176,188,333],[94,157,165,169,172,174,175,176,188,331],[94,157,165,169,172,174,175,176,188,333,334],[94,157,165,169,171,172,174,175,176,188,237,238,239,243,244,497],[94,157,165,169,171,172,174,175,176,186,188,225,227,245,249,285,288,289,323,443,493,498,500,502],[94,157,165,169,171,172,174,175,176,186,188,205,229,236,237,247,249,286,441,486,492,497],[94,157,165,169,172,174,175,176,188,356],[94,157,165,169,172,174,175,176,188,357],[94,157,165,169,172,174,175,176,188,239,250,485],[94,157,165,169,172,174,175,176,188,358],[94,157,165,169,172,174,175,176,188,232],[94,157,165,169,172,174,175,176,188,234,246],[94,157,165,169,171,172,174,175,176,188,234,238,245],[94,157,165,169,172,174,175,176,188,241,246],[94,157,165,169,172,174,175,176,188,242],[94,157,165,169,172,174,175,176,188,234,235],[94,157,165,169,172,174,175,176,188,234,290],[94,157,165,169,172,174,175,176,188,234],[94,157,165,169,172,174,175,176,188,236,280,329],[94,157,165,169,172,174,175,176,188,328],[94,157,165,169,172,174,175,176,188,233,235,236],[94,157,165,169,172,174,175,176,188,236,326],[94,157,165,169,172,174,175,176,188,233,235],[94,157,165,169,172,174,175,176,188,285,388],[94,157,165,169,172,174,175,176,188,485],[94,157,165,169,171,172,174,175,176,188,205,245,247,251,285,388,442,445,448,449,450,476,477,480,484,486,493,497],[94,157,165,169,172,174,175,176,188,299,302,304,305,318,319],[85,94,157,165,169,172,174,175,176,188,216,218,248,478,479],[85,94,157,165,169,172,174,175,176,188,216,218,248,478,479,483],[94,157,165,169,172,174,175,176,188,372],[94,157,165,169,172,174,175,176,188,258,279,284,285,349,350,351,352,353,355,368,369,371,374,442,445,496,498],[94,157,165,169,172,174,175,176,188,318],[94,157,165,169,171,172,174,175,176,188,323,493],[94,157,165,169,172,174,175,176,188,323],[94,157,165,169,171,172,174,175,176,188,245,291,320,322,324,442,493,500,502],[94,157,165,169,172,174,175,176,188,299,300,301,302,304,305,318,319,501],[91,94,157,165,169,171,172,174,175,176,186,188,205,234,235,247,253,285,286,289,388,440,441,443,493,496,497,500],[94,157,165,169,172,174,175,176,188,230,233,240],[94,157,165,169,172,174,175,176,188,284,286,418,421],[94,157,165,169,172,174,175,176,188,284,419,487,488,489,490,491],[94,157,165,169,171,172,174,175,176,188,280,496],[94,157,165,169,171,172,174,175,176,188],[94,157,165,169,172,174,175,176,188,283,368],[94,157,165,169,172,174,175,176,188,282],[94,157,165,169,172,174,175,176,188,284,337],[94,157,165,169,172,174,175,176,188,281,283,496],[94,157,165,169,171,172,174,175,176,188,229,284,418,419,420,493,496,497],[85,94,157,165,169,172,174,175,176,188,233,239,317],[85,94,157,165,169,172,174,175,176,188,231],[94,157,165,169,172,174,175,176,188,221,222],[85,94,157,165,169,172,174,175,176,188,227],[85,94,157,165,169,172,174,175,176,188,233,303],[85,91,94,157,165,169,172,174,175,176,188,285,289,500,502],[94,157,165,169,172,174,175,176,188,227,524,525],[85,94,157,165,169,172,174,175,176,188,298],[85,94,157,165,169,172,174,175,176,186,188,205,225,292,294,296,297,502],[94,157,165,169,172,174,175,176,188,233,260,497],[94,157,165,169,172,174,175,176,188,233,425],[85,94,157,165,169,171,172,174,175,176,186,188,223,225,298,396,500,501],[85,94,157,165,169,172,174,175,176,188,214,215,216,217,218,500,545],[85,86,87,88,89,94,157,165,169,172,174,175,176,188],[94,157,165,169,172,174,175,176,188,391,392,393],[94,157,165,169,172,174,175,176,188,391],[85,89,94,157,165,169,171,172,173,174,175,176,186,188,213,214,215,216,217,218,219,225,253,258,435,463,498,499,502,545],[94,157,165,169,172,174,175,176,188,510],[94,157,165,169,172,174,175,176,188,512],[94,157,165,169,172,174,175,176,188,516],[94,157,165,169,172,174,175,176,188,518],[94,157,165,169,172,174,175,176,188,520,521,522],[94,157,165,169,172,174,175,176,188,526],[90,94,157,165,169,172,174,175,176,188,504,509,511,513,517,519,523,527,529,539,540,542,546,547,548,549],[94,157,165,169,172,174,175,176,188,528],[94,157,165,169,172,174,175,176,188,538],[94,157,165,169,172,174,175,176,188,294],[94,157,165,169,172,174,175,176,188,541],[94,156,157,165,169,172,174,175,176,188,284,418,419,421,487,488,490,491,543,545],[94,157,165,169,172,174,175,176,188,213],[94,157,165,169,172,174,175,176,188,728],[94,157,165,169,172,174,175,176,188,714,715,716],[94,157,165,169,172,174,175,176,188,713,717],[94,157,165,169,172,174,175,176,188,717],[94,157,165,169,172,174,175,176,188,193],[94,157,165,169,172,174,175,176,188,764,765],[94,157,165,169,172,174,175,176,188,556,557,558,587,589,591,766,794],[94,157,165,169,172,174,175,176,188,693,700,701],[94,157,165,169,172,174,175,176,188,794,795],[94,157,165,169,172,174,175,176,188,555,558,587,589,591,597,602,603,604,740,743,744,794,3978],[94,157,165,169,172,174,175,176,188,193,213],[94,157,165,169,172,174,175,176,188,558,587,589,591,706,707,720,721,794],[94,157,165,169,172,174,175,176,188,707,720,721,722,724,726,732],[94,157,165,169,172,174,175,176,188,720],[94,157,165,169,172,174,175,176,188,707,721,723],[94,157,165,169,172,174,175,176,188,3969],[94,157,165,169,172,174,175,176,188,3968],[94,157,165,169,172,174,175,176,188,711],[94,157,165,169,172,174,175,176,188,709],[94,157,165,169,172,174,175,176,188,709,710],[94,157,165,169,172,174,175,176,188,3947],[94,157,165,169,172,174,175,176,188,599],[94,109,112,115,116,157,165,169,172,174,175,176,188,205],[94,112,157,165,169,172,174,175,176,188,193,205],[94,112,116,157,165,169,172,174,175,176,188,205],[94,106,157,165,169,172,174,175,176,188],[94,110,157,165,169,172,174,175,176,188],[94,108,109,112,157,165,169,172,174,175,176,188,205],[94,157,165,169,172,174,175,176,178,188,202],[94,106,157,165,169,172,174,175,176,188,213],[94,108,112,157,165,169,172,174,175,176,178,188,205],[94,103,104,105,107,111,157,165,168,169,172,174,175,176,188,193,205],[94,112,121,129,157,165,169,172,174,175,176,188],[94,104,110,157,165,169,172,174,175,176,188],[94,112,138,139,157,165,169,172,174,175,176,188],[94,104,107,112,157,165,169,172,174,175,176,188,196,205,213],[94,112,157,165,169,172,174,175,176,188],[94,108,112,157,165,169,172,174,175,176,188,205],[94,103,157,165,169,172,174,175,176,188],[94,106,107,108,110,111,112,113,114,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,139,140,141,142,143,157,165,169,172,174,175,176,188],[94,112,131,134,157,165,169,172,174,175,176,188],[94,112,121,122,123,157,165,169,172,174,175,176,188],[94,110,112,122,124,157,165,169,172,174,175,176,188],[94,111,157,165,169,172,174,175,176,188],[94,104,106,112,157,165,169,172,174,175,176,188],[94,112,116,122,124,157,165,169,172,174,175,176,188],[94,116,157,165,169,172,174,175,176,188],[94,110,112,115,157,165,169,172,174,175,176,188,205],[94,104,108,112,121,157,165,169,172,174,175,176,188],[94,112,131,157,165,169,172,174,175,176,188],[94,124,157,165,169,172,174,175,176,188],[94,106,112,138,157,165,169,172,174,175,176,188,196,211,213],[94,157,165,169,172,174,175,176,188,597,601,604,740,743,3978],[94,157,165,169,172,174,175,176,188,554,597,598,600,602,604,740,743,3978],[94,157,165,169,172,174,175,176,188,594],[94,157,165,169,172,174,175,176,188,595,596],[94,157,165,169,172,174,175,176,188,554,595,597,604,740,743,3978],[94,157,165,169,172,174,175,176,188,685],[94,157,165,169,172,174,175,176,188,676],[94,157,165,169,172,174,175,176,188,676,679],[94,157,165,169,172,174,175,176,188,671,674,676,677,678,679,680,681,682,683,684],[94,157,165,169,172,174,175,176,188,610,612,679],[94,157,165,169,172,174,175,176,188,676,677],[94,157,165,169,172,174,175,176,188,611,676,678],[94,157,165,169,172,174,175,176,188,612,614,616,617,618,619],[94,157,165,169,172,174,175,176,188,614,616,618,619],[94,157,165,169,172,174,175,176,188,614,616,618],[94,157,165,169,172,174,175,176,188,611,614,616,617,619],[94,157,165,169,172,174,175,176,188,610,612,613,614,615,616,617,618,619,620,621,671,672,673,674,675],[94,157,165,169,172,174,175,176,188,610,612,613,616],[94,157,165,169,172,174,175,176,188,612,613,616],[94,157,165,169,172,174,175,176,188,616,619],[94,157,165,169,172,174,175,176,188,610,611,613,614,615,617,618,619],[94,157,165,169,172,174,175,176,188,610,611,612,616,676],[94,157,165,169,172,174,175,176,188,616,617,618,619],[94,157,165,169,172,174,175,176,188,618],[94,157,165,169,172,174,175,176,188,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670],[94,157,165,169,172,174,175,176,188,248,817]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"2ab096661c711e4a81cc464fa1e6feb929a54f5340b46b0a07ac6bbf857471f0","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","affectsGlobalScope":true,"impliedFormat":1},{"version":"73f78680d4c08509933daf80947902f6ff41b6230f94dd002ae372620adb0f60","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5239f5c01bcfa9cd32f37c496cf19c61d69d37e48be9de612b541aac915805b","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"dc0a7f107690ee5cd8afc8dbf05c4df78085471ce16bdd9881642ec738bc81fe","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"21da358700a3893281ce0c517a7a30cbd46be020d9f0c3f2834d0a8ad1f5fc75","impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"438b41419b1df9f1fbe33b5e1b18f5853432be205991d1b19f5b7f351675541e","affectsGlobalScope":true,"impliedFormat":1},{"version":"096116f8fedc1765d5bd6ef360c257b4a9048e5415054b3bf3c41b07f8951b0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"074de5b2fdead0165a2757e3aaef20f27a6347b1c36adea27d51456795b37682","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"24371e69a38fc33e268d4a8716dbcda430d6c2c414a99ff9669239c4b8f40dea","impliedFormat":1},{"version":"ccab02f3920fc75c01174c47fcf67882a11daf16baf9e81701d0a94636e94556","impliedFormat":1},{"version":"3e11fce78ad8c0e1d1db4ba5f0652285509be3acdd519529bc8fcef85f7dafd9","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"c63b9ada8c72f95aac5db92aea07e5e87ec810353cdf63b2d78f49a58662cf6c","impliedFormat":1},{"version":"1cc2a09e1a61a5222d4174ab358a9f9de5e906afe79dbf7363d871a7edda3955","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"b64d4d1c5f877f9c666e98e833f0205edb9384acc46e98a1fef344f64d6aba44","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"c9381908473a1c92cb8c516b184e75f4d226dad95c3a85a5af35f670064d9a2f","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"3fad5618174d74a34ee006406d4eb37e8d07dd62eb1315dbf52f48d31a337547","impliedFormat":1},{"version":"7e49f52a159435fc8df4de9dc377ef5860732ca2dc9efec1640531d3cf5da7a3","impliedFormat":1},{"version":"dd4bde4bdc2e5394aed6855e98cf135dfdf5dd6468cad842e03116d31bbcc9bc","impliedFormat":1},{"version":"4d4e879009a84a47c05350b8dca823036ba3a29a3038efed1be76c9f81e45edf","affectsGlobalScope":true,"impliedFormat":1},{"version":"8b50a819485ffe0d237bf0d131e92178d14d11e2aa873d73615a9ec578b341f5","impliedFormat":1},{"version":"9ba13b47cb450a438e3076c4a3f6afb9dc85e17eae50f26d4b2d72c0688c9251","impliedFormat":1},{"version":"b64cd4401633ea4ecadfd700ddc8323a13b63b106ac7127c1d2726f32424622c","impliedFormat":1},{"version":"37c6e5fe5715814412b43cc9b50b24c67a63c4e04e753e0d1305970d65417a60","impliedFormat":1},{"version":"1d024184fb57c58c5c91823f9d10b4915a4867b7934e89115fd0d861a9df27c8","impliedFormat":1},{"version":"ee0e4946247f842c6dd483cbb60a5e6b484fee07996e3a7bc7343dfb68a04c5d","impliedFormat":1},{"version":"ef051f42b7e0ef5ca04552f54c4552eac84099d64b6c5ad0ef4033574b6035b8","impliedFormat":1},{"version":"853a43154f1d01b0173d9cbd74063507ece57170bad7a3b68f3fa1229ad0a92f","impliedFormat":1},{"version":"56231e3c39a031bfb0afb797690b20ed4537670c93c0318b72d5180833d98b72","impliedFormat":1},{"version":"5cc7c39031bfd8b00ad58f32143d59eb6ffc24f5d41a20931269011dccd36c5e","impliedFormat":1},{"version":"12d602a8fe4c2f2ba4f7804f5eda8ba07e0c83bf5cf0cda8baffa2e9967bfb77","affectsGlobalScope":true,"impliedFormat":1},{"version":"a856ab781967b62b288dfd85b860bef0e62f005ed4b1b8fa25c53ce17856acaf","impliedFormat":1},{"version":"cc25940cfb27aa538e60d465f98bb5068d4d7d33131861ace43f04fe6947d68f","impliedFormat":1},{"version":"8db46b61a690f15b245cf16270db044dc047dce9f93b103a59f50262f677ea1f","impliedFormat":1},{"version":"01ff95aa1443e3f7248974e5a771f513cb2ac158c8898f470a1792f817bee497","impliedFormat":1},{"version":"757227c8b345c57d76f7f0e3bbad7a91ffca23f1b2547cbed9e10025816c9cb7","impliedFormat":1},{"version":"959d0327c96dd9bb5521f3ed6af0c435996504cc8dd46baa8e12cb3b3518cef1","impliedFormat":1},{"version":"e1c1a0b4d1ead0de9eca52203aeb1f771f21e6238d6fcd15aa56ac2a02f1b7bf","impliedFormat":1},{"version":"101f482fd48cb4c7c0468dcc6d62c843d842977aea6235644b1edd05e81fbf22","impliedFormat":1},{"version":"266bee0a41e9c3ba335583e21e9277ae03822402cf5e8e1d99f5196853613b98","affectsGlobalScope":true,"impliedFormat":1},{"version":"ee96415bb64198cc13555da26474825a638e48e5a3c03cb33dd82b7e68fcc417","impliedFormat":1},{"version":"3ef397f12387eff17f550bc484ea7c27d21d43816bbe609d495107f44b97e933","impliedFormat":1},{"version":"1023282e2ba810bc07905d3668349fbd37a26411f0c8f94a70ef3c05fe523fcf","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"e236b5eba291f51bdf32c231673e6cab81b5410850e61f51a7a524dddadc0f95","impliedFormat":1},{"version":"ce8653341224f8b45ff46d2a06f2cacb96f841f768a886c9d8dd8ec0878b11bd","affectsGlobalScope":true,"impliedFormat":1},{"version":"7f2c62938251b45715fd2a9887060ec4fbc8724727029d1cbce373747252bdd7","impliedFormat":1},{"version":"e3ace08b6bbd84655d41e244677b474fd995923ffef7149ddb68af8848b60b05","impliedFormat":1},{"version":"132580b0e86c48fab152bab850fc57a4b74fe915c8958d2ccb052b809a44b61c","impliedFormat":1},{"version":"90a278f5fab7557e69e97056c0841adf269c42697194f0bd5c5e69152637d4b3","impliedFormat":1},{"version":"69c9a5a9392e8564bd81116e1ed93b13205201fb44cb35a7fde8c9f9e21c4b23","impliedFormat":1},{"version":"5f8fc37f8434691ffac1bfd8fc2634647da2c0e84253ab5d2dd19a7718915b35","impliedFormat":1},{"version":"5981c2340fd8b076cae8efbae818d42c11ffc615994cb060b1cd390795f1be2b","impliedFormat":1},{"version":"f263485c9ca90df9fe7bb3a906db9701997dc6cae86ace1f8106ac8d2f7f677b","impliedFormat":1},{"version":"4c64e7fa79f96cac57f4e22899805f88d22d69ac673b63fe4fa14e5229200bba","affectsGlobalScope":true,"impliedFormat":1},{"version":"0250da3eb85c99624f974e77ef355cdf86f43980251bc371475c2b397ba55bcd","impliedFormat":1},{"version":"f1c93e046fb3d9b7f8249629f4b63dc068dd839b824dd0aa39a5e68476dc9420","impliedFormat":1},{"version":"eab2f3179607acb3d44b2db2a76dd7d621c5039b145dc160a1ee733963f9d2f5","impliedFormat":1},{"version":"12806f9f085598ef930edaf2467a5fa1789a878fba077cd27e85dc5851e11834","impliedFormat":1},{"version":"1dbca38aa4b0db1f4f9e6edacc2780af7e028b733d2a98dd3598cd235ca0c97d","impliedFormat":1},{"version":"a43fe41c33d0a192a0ecaf9b92e87bef3709c9972e6d53c42c49251ccb962d69","impliedFormat":1},{"version":"a177959203c017fad3ecc4f3d96c8757a840957a4959a3ae00dab9d35961ca6c","affectsGlobalScope":true,"impliedFormat":1},{"version":"6fc727ccf9b36e257ff982ea0badeffbfc2c151802f741bddff00c6af3b784cf","impliedFormat":1},{"version":"19143c930aef7ccf248549f3e78992f2f1049118ec5d4622e95025057d8e392b","impliedFormat":1},{"version":"4844a4c9b4b1e812b257676ed8a80b3f3be0e29bf05e742cc2ea9c3c6865e6c6","impliedFormat":1},{"version":"064878a60367e0407c42fb7ba02a2ea4d83257357dc20088e549bd4d89433e9c","impliedFormat":1},{"version":"cca8917838a876e2d7016c9b6af57cbf11fdf903c5fdd8e613fa31840b2957bf","impliedFormat":1},{"version":"d91ae55e4282c22b9c21bc26bd3ef637d3fe132507b10529ae68bf76f5de785b","impliedFormat":1},{"version":"b484ec11ba00e3a2235562a41898d55372ccabe607986c6fa4f4aba72093749f","impliedFormat":1},{"version":"7e8a671604329e178bb479c8f387715ebd40a091fc4a7552a0a75c2f3a21c65c","impliedFormat":1},{"version":"41ef7992c555671a8fe54db302788adefa191ded810a50329b79d20a6772d14c","impliedFormat":1},{"version":"041a7781b9127ab568d2cdcce62c58fdea7c7407f40b8c50045d7866a2727130","impliedFormat":1},{"version":"4c5e90ddbcd177ad3f2ffc909ae217c87820f1e968f6959e4b6ba38a8cec935e","impliedFormat":1},{"version":"b70dd9a44e1ac42f030bb12e7d79117eac7cb74170d72d381a1e7913320af23a","impliedFormat":1},{"version":"55cdbeebe76a1fa18bbd7e7bf73350a2173926bd3085bb050cf5a5397025ee4e","impliedFormat":1},{"version":"2beff543f6e9a9701df88daeee3cdd70a34b4a1c11cb4c734472195a5cb2af54","impliedFormat":1},{"version":"2e07abf27aa06353d46f4448c0bbac73431f6065eef7113128a5cd804d0c384d","impliedFormat":1},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1},{"version":"42bc0e1a903408137c3df2b06dfd7e402cdab5bbfa5fcfb871b22ebfdb30bd0b","impliedFormat":1},{"version":"9894dafe342b976d251aac58e616ac6df8db91fb9d98934ff9dd103e9e82578f","impliedFormat":1},{"version":"413df52d4ea14472c2fa5bee62f7a40abd1eb49be0b9722ee01ee4e52e63beb2","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"446a50749b24d14deac6f8843e057a6355dd6437d1fac4f9e5ce4a5071f34bff","impliedFormat":1},{"version":"182e9fcbe08ac7c012e0a6e2b5798b4352470be29a64fdc114d23c2bab7d5106","impliedFormat":1},{"version":"2f4e6b4d39426a1b85ecf4bdeb9dddbf4d9b3397d95d8555d46f925c9519ec7d","impliedFormat":1},{"version":"78a2869ad0cbf3f9045dda08c0d4562b7e1b2bfe07b19e0db072f5c3c56e9584","impliedFormat":1},{"version":"89d5d28d4f57e000b836ac273079be1b75710e28ce14750d081fb420d37e2ca5","impliedFormat":1},{"version":"fd4e24ccff3966390600d7f5d6aa1fed5a512e92ada735ea5fbc933d313ad3d3","impliedFormat":1},{"version":"b7cddfe1aa6b86b5fad3c9ccb30d05b3ccb165aebbf112f48d2d8a5f69dd98b1","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"ad0d1d75d129b1c80f911be438d6b61bfa8703930a8ff2be2f0e1f8a91841c64","impliedFormat":1},{"version":"bd2c7ada3dee03653d3f601011d30072194bc3970cd93208f9588fbdc0c69347","impliedFormat":1},{"version":"e480da45d32313e7174b265674da504f075f59ef326852f0c5a5d863b438ae85","impliedFormat":1},{"version":"ad54850f61fcf5d014e11be80d2f46fea9265cfa7e77456da876f7833ef81769","impliedFormat":1},{"version":"6f7c9e8bd2b5b6a080b07080065f94900bd3c7e5ebbd3047bc33fcce2fab1dd8","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"da5950ee2a90721df6f3fba45f5d05308f7e4c35835392215dd2cd404505e2de","impliedFormat":1},{"version":"ce75b1aebb33d510ff28af960a9221410a3eaf7f18fc5f21f9404075fba77256","impliedFormat":1},{"version":"f42d5fed19610d485c646a0c430e768115567d078c7fc855c57b0c578b3d6cd3","impliedFormat":1},{"version":"ee8df1cb8d0faaca4013a1b442e99130769ce06f438d18d510fed95890067563","impliedFormat":1},{"version":"d5630f2ad9b4541e5ce891648121022f9412ecdca1820baa1f0104f70fd7eff7","impliedFormat":1},{"version":"4d15375ab13497104bc8fe56fdef2b5fd6853f29255737d23a33fa306ff7fd69","impliedFormat":1},{"version":"2cd3fc1d0d6a1e85baffd2d4f50f5efb192b5446eef567e97c94765402f0aad4","impliedFormat":1},{"version":"e4cbf2f1e89ecccaddd2c045e600ae41b732295953fb06247c7dcbc2d281ed30","impliedFormat":1},{"version":"27bbdb7509a5bb564020321fc5485764d0db3230a10d2336ae5ce2c1d401b0e7","impliedFormat":1},{"version":"8c1697d90c394a6fd955b98eae01238eff628e129b987a68aea10f898a48e7da","impliedFormat":1},{"version":"7580e62139cb2b44a0270c8d01abcbfcba2819a02514a527342447fa69b34ef1","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"f374cb24e93e7798c4d9e83ff872fa52d2cdb36306392b840a6ddf46cb925cb6","impliedFormat":1},{"version":"d10d63718e1646c2279e3b33831f82c60e31f622b2b7020f1196409ca4c09242","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"148679c6d0f449210a96e7d2e562d589e56fcde87f843a92808b3ff103f1a774","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"02436d7e9ead85e09a2f8e27d5f47d9464bced31738dec138ca735390815c9f0","impliedFormat":1},{"version":"f8d5ff8eafd37499f2b6a98659dd9b45a321de186b8db6b6142faed0fea3de77","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"a22dd55aa4d39906252000ab8e8a1b83b195eef7f4274eb51e457c1f11cf6580","impliedFormat":1},{"version":"540cc83ab772a2c6bc509fe1354f314825b5dba3669efdfbe4693ecd3048e34f","impliedFormat":1},{"version":"121b0696021ab885c570bbeb331be8ad82c6efe2f3b93a6e63874901bebc13e3","impliedFormat":1},{"version":"612d9da66bb046a9c1e2e8d026245ded881fc4b9f98cbfae714415d57ee0ae0b","impliedFormat":1},{"version":"32c2ad9494dad5d11b0564a619fee18f388db6c1e9e2cd3c360b3122549691eb","impliedFormat":1},{"version":"6c301d40aec56a74ec7bd7324e31a728dadf9bfba3e96def02938d3d973534ec","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"aa14cee20aa0db79f8df101fc027d929aec10feb5b8a8da3b9af3895d05b7ba2","impliedFormat":1},{"version":"493c700ac3bd317177b2eb913805c87fe60d4e8af4fb39c41f04ba81fae7e170","impliedFormat":1},{"version":"aeb554d876c6b8c818da2e118d8b11e1e559adbe6bf606cc9a611c1b6c09f670","impliedFormat":1},{"version":"acf5a2ac47b59ca07afa9abbd2b31d001bf7448b041927befae2ea5b1951d9f9","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"d71291eff1e19d8762a908ba947e891af44749f3a2cbc5bd2ec4b72f72ea795f","impliedFormat":1},{"version":"c0480e03db4b816dff2682b347c95f2177699525c54e7e6f6aa8ded890b76be7","impliedFormat":1},{"version":"25a5f6fd3a2243c859eddc99ab5fba11d970af2fe7a5df9c32b7668f76f97b01","impliedFormat":1},{"version":"8d207e1f9d2c30d6f77dfa693f3827c3fbf0d89240297e10bdfe1041d433df68","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"d682336018141807fb602709e2d95a192828fcb8d5ba06dda3833a8ea98f69e3","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"4fbd3116e00ed3a6410499924b6403cc9367fdca303e34838129b328058ede40","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"8c70ddc0c22d85e56011d49fddfaae3405eb53d47b59327b9dd589e82df672e7","impliedFormat":1},{"version":"2f9c89cbb29d362290531b48880a4024f258c6033aaeb7e59fbc62db26819650","impliedFormat":1},{"version":"a365c4d3bed3be4e4e20793c999c51f5cd7e6792322f14650949d827fbcd170f","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"273782b8454e78f6a8b30d2cfbf6860499c930595095fcc1689637115f0eddda","affectsGlobalScope":true,"impliedFormat":1},{"version":"3fbdd025f9d4d820414417eeb4107ffa0078d454a033b506e22d3a23bc3d9c41","affectsGlobalScope":true,"impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a8f8e6ab2fa07b45251f403548b78eaf2022f3c2254df3dc186cb2671fe4996d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"9f9bb6755a8ce32d656ffa4763a8144aa4f274d6b69b59d7c32811031467216e","impliedFormat":1},{"version":"5c32bdfbd2d65e8fffbb9fbda04d7165e9181b08dad61154961852366deb7540","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"0c05e9842ec4f8b7bfebfd3ca61604bb8c914ba8da9b5337c4f25da427a005f2","impliedFormat":1},{"version":"faed7a5153215dbd6ebe76dfdcc0af0cfe760f7362bed43284be544308b114cf","impliedFormat":1},{"version":"7029e566b8df176f703fb59fd437a38670c7a0e02c58b2d66dfb5b2e2b2defdb","impliedFormat":1},{"version":"7f2aa4d4989a82530aaac3f72b3dceca90e9c25bee0b1a327e8a08a1262435ad","impliedFormat":1},{"version":"d96b39301d0ded3f1a27b47759676a33a02f6f5049bfcbde81e533fd10f50dcb","impliedFormat":1},{"version":"e9f147ecca73d9346a4c073432843c159ccbe50bdcb678a78f6da10eae2cecf4","impliedFormat":1},{"version":"de061f7d72bd65c06fc1419f841dfdcb29a8e22fe6fa527d1e6eb20b897d4de0","impliedFormat":1},{"version":"663beafc2446079574570cba86e9b15f986f908ddb1b01274509970126fee945","impliedFormat":1},{"version":"a3102887d5058bf4cb5b37fa6964c09e9527c42053b3b5c642b89878620748de","impliedFormat":1},{"version":"0aaaa1727edd29673d85c9b26d7ca4d54e5407a48586903c51b48b7f7d196f61","impliedFormat":1},{"version":"d35bca0b261bff02635758c48e8ab99c61c420d0dfabbcf467e847171d876b7d","impliedFormat":1},{"version":"3bc12c40d90c342ff88a3d876996c555ed5cbee5fe8c3308a240b321f401ee46","impliedFormat":1},{"version":"ba130768aae855a5477e9e148e5c879548e6e7ccbcc56fd1934c8a18ea5b7569","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"d38530db0601215d6d767f280e3a3c54b2a83b709e8d9001acb6f61c67e965fc","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"b499af2054a037a162b3b72cd886f48bbf32a3502c865c6e29fac7d2ab3ce0b5","impliedFormat":1},{"version":"b83cb14474fa60c5f3ec660146b97d122f0735627f80d82dd03e8caa39b4388c","impliedFormat":1},{"version":"d87f90d2df7b638204d81d6c57e1f2a8cc9317c45ca331c691c375649aa9255c","impliedFormat":1},{"version":"7274fbffbd7c9589d8d0ffba68157237afd5cecff1e99881ea3399127e60572f","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"8b4327413e5af38cd8cb97c59f48c3c866015d5d642f28518e3a891c469f240e","impliedFormat":1},{"version":"4cceef18d7f088e797a463e90b7a9dad10c6bc667724b7686e3e740ae00122be","impliedFormat":1},{"version":"7ee86fbb3754388e004de0ef9e6505485ddfb3be7640783d6d015711c03d302d","impliedFormat":1},{"version":"cc1954b539604b1e562319119ac7e888172208b32ca873f9a357a92c826bd046","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"bb8f2dbc03533abca2066ce4655c119bff353dd4514375beb93c08590c03e023","impliedFormat":1},{"version":"706dd95827e7ebaabda91d5db2b755233e0952d98570e9c032b0f066a15c1177","affectsGlobalScope":true,"impliedFormat":1},{"version":"0b103e9abfe82d14c0ad06a55d9f91d6747154ef7cacc73cf27ecad2bfb3afcf","impliedFormat":1},{"version":"990b8fad2327b77e6920cc792af320e8867e68f02ce849b12c0a6ab9a1aebb09","impliedFormat":1},{"version":"5eb8cd1cb0c9143d74a8190b577c522720878c31aef67d866fcd29973f83e955","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"a6805fcafed712aea7759f8bc731014f9d22738c1d6ef9d43b8091d1d48346d5","impliedFormat":1},{"version":"c49469a5349b3cc1965710b5b0f98ed6c028686aa8450bcb3796728873eb923e","impliedFormat":1},{"version":"4a889f2c763edb4d55cb624257272ac10d04a1cad2ed2948b10ed4a7fda2a428","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"d88ea80a6447d7391f52352ec97e56b52ebec934a4a4af6e2464cfd8b39c3ba8","impliedFormat":1},{"version":"142617b3cdf902b69c6464c9fbd942b60ab3e733ca18c032b19e0f7e2adbefe8","impliedFormat":1},{"version":"0b603555f1881f87256ffd6344d3e3ed6d466c2e701eabf381f28be8c2125892","impliedFormat":1},{"version":"897e4f7662488e3ecc79e743bdd3b78f13bdb69a97851afa5b440c4211e32ea9","impliedFormat":1},{"version":"e2e1c6d3b2d93add5200bd7bc1a8cccb4e446836b2111ece45db8683a2c765de","impliedFormat":1},{"version":"251b03d5cd243854ce870d9a9a39f491faf69898c5d6b5eee28cc7649c57417b","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"2c4de79f406d137390608e8c0a44fba2ff8e00bacfcae7c9d1781fef10e9440d","impliedFormat":1},{"version":"07ba23a10465791be5d22deaf5ef7de7658774ddff53721e5ea17fedea1bc721","impliedFormat":1},{"version":"dca8c645c5afeb03b1ecedbf16323f33e7d0afaa6256c8e047e6e38087a97f53","impliedFormat":1},{"version":"775f181bd4a533d6f8b5e55ec1d9f1624559720ae8a70e9432258da26b38d27c","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"9109a1291dd4b9f1541bea81ee11c247a2ca9e1ea89f87f13aa1811c3c069616","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"622694a8522b46f6310c2a9b5d2530dde1e2854cb5829354e6d1ff8f371cf469","impliedFormat":1},{"version":"cd8ce8d68567f62dd580b3c3c37777ac3f5b81944c7417f5ea83030eab533385","impliedFormat":1},{"version":"e374d1eaa05b7dc38580062942ac8351ce79cbe11f6dbce4946a582a5680582d","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"a9e6c0ff3f8186fccd05752cf75fc94e147c02645087ac6de5cc16403323d870","impliedFormat":1},{"version":"49af4b52f0d4d2304c5f2c6fe5fab3e153e0acc38830d0202821b877c097dd02","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"e68b8e5a1df7c1be2bc105141456ecba70215806e1c28bfbc5c12bfce4be6e68","impliedFormat":1},{"version":"511c8f02329808d47d00b859c532ae9115590048b17325a946c74dac48428650","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"b5f9e66625783eefcbe3d2da074b2e7ba2066d61ce3fc6ef4f22805ad946cab4","impliedFormat":1},{"version":"e37115962d284b9f7a37c2bdd2add50f88365dde41f5e0ff591ffc48a8ec7575","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"f89488602bec98a142072fae7ea5ba99431a569ff580c64b7be39896474799d8","impliedFormat":1},{"version":"bbbc47961f39a57df103cf4ca3bb8f8732b4b6678a18225a0aa76d59c466956c","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"2ffb043dc5163458e473b7010859f86e01dc4edffcae0a93d885d028b426a546","impliedFormat":1},{"version":"c8f004e6036aa1c764ad4ec543cf89a5c1893a9535c80ef3f2b653e370de45e6","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"b064c36f35de7387d71c599bfcf28875849a1dbc733e82bd26cae3d1cd060521","impliedFormat":1},{"version":"05c7280d72f3ed26f346cbe7cbbbb002fb7f15739197cbbee6ab3fd1a6cb9347","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"04b7b2e0832dfd3c31e81df3975e8d8fda28e7ff999b0aa2932608a8f6661d5c","impliedFormat":1},{"version":"ca2d34c6ed5cbd3070b8b6f32f42ae54adcc6499c1e4b99f0a5798b3f27cc653","impliedFormat":1},{"version":"9ec68995e66dd6b9dac834bf5ae85fde802714ea2e82151a5d1d53ef01b463ef","impliedFormat":1},{"version":"5c4d626b4902f2ef8a1cc146d761d276cef988016dc674e3b98fbad70e64bc9f","impliedFormat":1},{"version":"fdfaa0aad899524962e2955287b5b991ffe3be50f64e02eb60c933ca44644a94","impliedFormat":1},{"version":"53c972a0f9bc3a4ec70fff7314123ea8cfcf75b3703046f767d2dc1eea87b2fb","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"7d73b24e7bf31dfb8a931ca6c4245f6bb0814dfae17e4b60c9e194a631fe5f7b","impliedFormat":1},{"version":"d130c5f73768de51402351d5dc7d1b36eaec980ca697846e53156e4ea9911476","impliedFormat":1},{"version":"413586add0cfe7369b64979d4ec2ed56c3f771c0667fbde1bf1f10063ede0b08","impliedFormat":1},{"version":"06472528e998d152375ad3bd8ebcb69ff4694fd8d2effaf60a9d9f25a37a097a","impliedFormat":1},{"version":"7303b45138d2511035056a5901a1490ebdcbf055cbb1276f8629c5121cbe733e","impliedFormat":1},{"version":"27f874cd5327507eeff699a74567f60c1215b94509f4308633a7b01922471ed2","impliedFormat":1},{"version":"a401617604fa1f6ce437b81689563dfdc377069e4c58465dbd8d16069aede0a5","impliedFormat":1},{"version":"2c6cf04bc525caf6546e859e8ef10bfb9573837ec0bc5ec7b53a7b1b8ca72781","impliedFormat":1},{"version":"8695dec09ad439b0ceef3776ea68a232e381135b516878f0901ed2ea114fd0fe","impliedFormat":1},{"version":"304b44b1e97dd4c94697c3313df89a578dca4930a104454c99863f1784a54357","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"87cc05fe13108f02e12da7e3efd8e360fef78d96a0c9e11408ea1b1b9fb3e03d","impliedFormat":1},{"version":"1abbf67c218d23c2ce76887caac2df6c7dab3d97ba2b65348432b876f510002a","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"c06ef3b2569b1c1ad99fcd7fe5fba8d466e2619da5375dfa940a94e0feea899b","impliedFormat":1},{"version":"f7d628893c9fa52ba3ab01bcb5e79191636c4331ee5667ecc6373cbccff8ae12","impliedFormat":1},{"version":"1d879125d1ec570bf04bc1f362fdbe0cb538315c7ac4bcfcdf0c1e9670846aa6","impliedFormat":1},{"version":"8bd496cf710d4873d15e4891a5dbf945673e3321ca74cf75187e347fd5ed295e","impliedFormat":1},{"version":"a6dba407fc287f1e25454e75028c91bbc00675f2d1c4e8b3edcc36c08611a486","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"e91f7b1344577a02f051b9b471f33044fef8334a76dc9e1de003d17595a5219b","impliedFormat":1},{"version":"c0723195c85e19656d6b5b9fdb81d3f3403c1ae4679e722c6ea058c516b38d12","impliedFormat":1},{"version":"186eea74805194f04e41038fc5eca653788b9dedbab7c2d7d17e10139622dd92","impliedFormat":1},{"version":"71d9eb4c4e99456b78ae182fb20a5dfc20eb1667f091dbb9335b3c017dd1c783","impliedFormat":1},{"version":"cfa846a7b7847a1d973605fbb8c91f47f3a0f0643c18ac05c47077ebc72e71c7","impliedFormat":1},{"version":"1594da19968752a22b2ac48c2d0e60575700e745c577a8a4a676b841238ad5bb","impliedFormat":1},{"version":"e0cee12109e0a10a4c3d6769fcc7644b7c1ea7f52365bea51728f5af29f8a137","impliedFormat":1},{"version":"7d4254b4c6c67a29d5e7f65e67d72540480ac2cfb041ca484847f5ae70480b62","impliedFormat":1},{"version":"3536968defef8a75514f547ead5e2e9c1e984820290ec9b00c5fdfb6ef786535","impliedFormat":1},{"version":"d83773870080c30a230e322ce13a9c6f3398e8dacea4ea8a83e26370f3bac23e","impliedFormat":1},{"version":"dcfeaf98d66314fec29a9076c4290e45d0b196a65827becc19138e9c7b855f37","impliedFormat":1},{"version":"6849fe9210fe4946d5f085bfed36758f33dc6ae15a751338d178dd4daa017c46","impliedFormat":1},{"version":"888cda0fa66d7f74e985a3f7b1af1f64b8ff03eb3d5e80d051c3cbdeb7f32ab7","impliedFormat":1},{"version":"60681e13f3545be5e9477acb752b741eae6eaf4cc01658a25ec05bff8b82a2ef","impliedFormat":1},{"version":"ffae4e1e06aa848a1e4bcef162cd1c48e5909b26223515981310af9c036bdfc7","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"34e16eb7c31768a11a08aebcfb3d70d7b8f0b016197e98d8419e566ceae6d6c8","impliedFormat":1},{"version":"f94ec1f7e4b709d26960306c9082a7a1b728a6e13089346aa48ba57c74cbf47e","impliedFormat":1},{"version":"9a11cb4033405e96c247cd5aa29790212aaffdd127869e8a5219103f0b389fd5","impliedFormat":1},{"version":"01479d9d5a5dda16d529b91811375187f61a06e74be294a35ecce77e0b9e8d6c","impliedFormat":1},{"version":"aff5213585cb72e94054dfe17250ff315f3569b3919d1ef1ad235f37c4ee894e","impliedFormat":1},{"version":"fb2ea35e1be6388d722d7725e2b49c697d34d9c890c3b96758faaeb86d35cef8","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"1a4dc28334a926d90ba6a2d811ba0ff6c22775fcc13679521f034c124269fd40","impliedFormat":1},{"version":"f05315ff85714f0b87cc0b54bcd3dde2716e5a6b99aedcc19cad02bf2403e08c","impliedFormat":1},{"version":"5fad3b31fc17a5bc58095118a8b160f5260964787c52e7eb51e3d4fcf5d4a6f0","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"456006a6975b26c0a1785feddae165f6d307e2d601ffde27e21fc4a790e448a4","impliedFormat":1},{"version":"c857e0aae3f5f444abd791ec81206020fbcc1223e187316677e026d1c1d6fe08","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"1fe0d18b111e1145a7e7601855bccd4ca20f24e3b9a5aba6bb1fa9d1a7059170","impliedFormat":1},{"version":"5632c3c26d420c063eebe64c45b1248b9492a67bf44f1d0c57e9dc8f6cf449bb","impliedFormat":1},{"version":"0df5aa619ab12993a39ea6dae062ee46eadbb4d738916460e636ada52bced75b","impliedFormat":1},{"version":"8fca3039857709484e5893c05c1f9126ab7451fa6c29e19bb8c2411a2e937345","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"10ab7be91f87ebe8916b62cf28af2e45b5601fc7b0e311adf838f912c6b31dd8","impliedFormat":1},{"version":"bc636fbc08e0979ceb7eb0731a33000283d77a33b62e1f71ee65be50394e40ba","impliedFormat":1},{"version":"7e0b7f91c5ab6e33f511efc640d36e6f933510b11be24f98836a20a2dc914c2d","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"75bbd3be047d539988a0ff0b56384ef7a6a25f3b676ad96bee547d44c31622a7","impliedFormat":1},{"version":"42960001a776b089ade681ab5cfddc936e0afb0615133ec1841f3dee89d3e1bf","impliedFormat":1},{"version":"0aedb02516baf3e66b2c1db9fef50666d6ed257edac0f866ea32f1aa05aa474f","impliedFormat":1},{"version":"da47712b394d944328245482603bc6f416d3949b67c9392279caab595076b510","affectsGlobalScope":true,"impliedFormat":1},{"version":"37d0071d8f0a06dc55c2c5e0ec3391affd4fd107c53410bf358196ec0bf3923f","impliedFormat":1},{"version":"b213dad76ca37fd552274c9499056e1c0d9c1bd38a55bb7f68b22ba6b84c3ad7","impliedFormat":1},{"version":"56ccb49443bfb72e5952f7012f0de1a8679f9f75fc93a5c1ac0bafb28725fc5f","impliedFormat":1},{"version":"20fa37b636fdcc1746ea0738f733d0aed17890d1cd7cb1b2f37010222c23f13e","impliedFormat":1},{"version":"d90b9f1520366d713a73bd30c5a9eb0040d0fb6076aff370796bc776fd705943","impliedFormat":1},{"version":"bc03c3c352f689e38c0ddd50c39b1e65d59273991bfc8858a9e3c0ebb79c023b","impliedFormat":1},{"version":"19df3488557c2fc9b4d8f0bac0fd20fb59aa19dec67c81f93813951a81a867f8","affectsGlobalScope":true,"impliedFormat":1},{"version":"b25350193e103ae90423c5418ddb0ad1168dc9c393c9295ef34980b990030617","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef86adb77316505c6b471da1d9b8c9e428867c2566270e8894d4d773a1c4dc2","impliedFormat":1},{"version":"5a49adaef698b7ad7e6127949fa1b0bbd3d46b7cbd11c54e392a4dcdd51f5190","impliedFormat":1},{"version":"96171c03c2e7f314d66d38acd581f9667439845865b7f85da8df598ff9617476","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"2489bf04d77dc025ba67f49f1a56eb24b9db477d5ff88123d887e163ed1776aa","impliedFormat":1},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"0b77b819b5417775fccb20c678293cf614c054a5b1a65421a5b933a9124ba998","impliedFormat":1},{"version":"e1f6076688a95bd82deaac740fccbe3cdea0d8a22057cccc9c5bce4398bdd33b","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"b1f1d57fde8247599731b24a733395c880a6561ec0c882efaaf20d7df968c5af","impliedFormat":1},{"version":"d7c1bbcddb06dcc8c9184013ace33c0dc71af715ab5987ccb42b903d2ec91193","impliedFormat":1},{"version":"35e6379c3f7cb27b111ad4c1aa69538fd8e788ab737b8ff7596a1b40e96f4f90","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"5a3ea721d03a361ccbdd7390ccd75f6e84cbca3a3f01f4b331ecc9af31890c49","impliedFormat":1},{"version":"e7dfaee4af38d45b1cab8a1ee0b3bc1f85ddcf64545ed391d675d78ae6526274","affectsGlobalScope":true,"impliedFormat":1},{"version":"98e2b197bf7fe7800f89c87825e2556d66474869845e97ad9c2b36f347c43539","impliedFormat":1},{"version":"af48e58339188d5737b608d41411a9c054685413d8ae88b8c1d0d9bfabdf6e7e","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"1de8c302fd35220d8f29dea378a4ae45199dc8ff83ca9923aca1400f2b28848a","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"332248ee37cca52903572e66c11bef755ccc6e235835e63d3c3e60ddda3e9b93","impliedFormat":1},{"version":"94e8cc88ae2ef3d920bb3bdc369f48436db123aa2dc07f683309ad8c9968a1e1","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"320f4091e33548b554d2214ce5fc31c96631b513dffa806e2e3a60766c8c49d9","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"d90d5f524de38889d1e1dbc2aeef00060d779f8688c02766ddb9ca195e4a713d","impliedFormat":1},{"version":"07ed3ddab975995eea41b22f3010506fb9f5fb301d04820b07d7a1aee5477d7c","impliedFormat":1},{"version":"969d8b0965849f4bae7cab0ba90bd1e1220e95999c2c6f01117fa7500901c017","impliedFormat":1},{"version":"6ec840ee5e2bc103f557fe38b1d585ee250540468713d7634ee066de372bf332","impliedFormat":1},{"version":"b0309e1eda99a9e76f87c18992d9c3689b0938266242835dd4611f2b69efe456","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"6ceb10ca57943be87ff9debe978f4ab73593c0c85ee802c051a93fc96aaf7a20","impliedFormat":1},{"version":"1de3ffe0cc28a9fe2ac761ece075826836b5a02f340b412510a59ba1d41a505a","impliedFormat":1},{"version":"e46d6cc08d243d8d0d83986f609d830991f00450fb234f5b2f861648c42dc0d8","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"ff863d17c6c659440f7c5c536e4db7762d8c2565547b2608f36b798a743606ca","impliedFormat":1},{"version":"5412ad0043cd60d1f1406fc12cb4fb987e9a734decbdd4db6f6acf71791e36fe","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"e297c0a524edee7677939122f90027bfbe5f2698939d9a85728e5044b39c7124","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"bc9ee0192f056b3d5527bcd78dc3f9e527a9ba2bdc0a2c296fbc9027147df4b2","impliedFormat":1},{"version":"b62381cae176db34f003cc6172ee8f3e0122014889d66391aa73698105cf4934","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"30d8da250766efa99490fc02801047c2c6d72dd0da1bba6581c7e80d1d8842a4","impliedFormat":1},{"version":"03566202f5553bd2d9de22dfab0c61aa163cabb64f0223c08431fb3fc8f70280","impliedFormat":1},{"version":"41eb514d9ce0a6e87957f08a4b7af70d93f87637f37dee706e2d92a6601c25a9","impliedFormat":1},{"version":"e7765aa8bcb74a38b3230d212b4547686eb9796621ffb4367a104451c3f9614f","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"7bd01f0f28cd3aeb2046274d85208e245965f6f2948edf4f7b2057bcf9f22ccc","impliedFormat":99},{"version":"d2f2cf2b8cc92bea913cda4a076e0f790b23a21e84f989d12f0116a7fe3906e0","impliedFormat":99},{"version":"6de125ea94866c736c6d58d68eb15272cf7d1020a5b459fea1c660027eca9a90","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5b20bc288ee49989c95b20847fc93b96bf61cc0845598897a6a53a967dd7d07","affectsGlobalScope":true,"impliedFormat":1},{"version":"064ac1c2ac4b2867c2ceaa74bbdce0cb6a4c16e7c31a6497097159c18f74aa7c","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"d3b315763d91265d6b0e7e7fa93cfdb8a80ce7cdd2d9f55ba0f37a22db00bdb8","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},{"version":"150653478b80c3bb651df96105e3a93c457147e12b16da350284edc36c8d7636","affectsGlobalScope":true},"7ad303e40d4fddf44f156129e397511953a71481c5cfd86b1862649aaaf240cc",{"version":"89121c1bf2990f5219bfd802a3e7fc557de447c62058d6af68d6b6348d64499a","impliedFormat":1},{"version":"d4a22007b481fe2a2e6bfd3a42c00cd62d41edb36d30fc4697df2692e9891fc8","impliedFormat":1},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"5d08a179b846f5ee674624b349ebebe2121c455e3a265dc93da4e8d9e89722b4","impliedFormat":1},{"version":"79b4369233a12c6fa4a07301ecb7085802c98f3a77cf9ab97eee27e1656f82e6","impliedFormat":1},{"version":"a5dbd4c9941b614526619bad31047ddd5f504ec4cdad88d6117b549faef34dd3","impliedFormat":99},{"version":"011423c04bfafb915ceb4faec12ea882d60acbe482780a667fa5095796c320f8","impliedFormat":99},{"version":"f8eb2909590ec619643841ead2fc4b4b183fbd859848ef051295d35fef9d8469","impliedFormat":99},{"version":"fe784567dd721417e2c4c7c1d7306f4b8611a4f232f5b7ce734382cf34b417d2","impliedFormat":99},{"version":"45d1e8fb4fd3e265b15f5a77866a8e21870eae4c69c473c33289a4b971e93704","impliedFormat":99},{"version":"cd40919f70c875ca07ecc5431cc740e366c008bcbe08ba14b8c78353fb4680df","impliedFormat":99},{"version":"ddfd9196f1f83997873bbe958ce99123f11b062f8309fc09d9c9667b2c284391","impliedFormat":99},{"version":"2999ba314a310f6a333199848166d008d088c6e36d090cbdcc69db67d8ae3154","impliedFormat":99},{"version":"62c1e573cd595d3204dfc02b96eba623020b181d2aa3ce6a33e030bc83bebb41","impliedFormat":99},{"version":"ca1616999d6ded0160fea978088a57df492b6c3f8c457a5879837a7e68d69033","impliedFormat":99},{"version":"835e3d95251bbc48918bb874768c13b8986b87ea60471ad8eceb6e38ddd8845e","impliedFormat":99},{"version":"de54e18f04dbcc892a4b4241b9e4c233cfce9be02ac5f43a631bbc25f479cd84","impliedFormat":99},{"version":"453fb9934e71eb8b52347e581b36c01d7751121a75a5cd1a96e3237e3fd9fc7e","impliedFormat":99},{"version":"bc1a1d0eba489e3eb5c2a4aa8cd986c700692b07a76a60b73a3c31e52c7ef983","impliedFormat":99},{"version":"4098e612efd242b5e203c5c0b9afbf7473209905ab2830598be5c7b3942643d0","impliedFormat":99},{"version":"28410cfb9a798bd7d0327fbf0afd4c4038799b1d6a3f86116dc972e31156b6d2","impliedFormat":99},{"version":"514ae9be6724e2164eb38f2a903ef56cf1d0e6ddb62d0d40f155f32d1317c116","impliedFormat":99},{"version":"970e5e94a9071fd5b5c41e2710c0ef7d73e7f7732911681592669e3f7bd06308","impliedFormat":99},{"version":"491fb8b0e0aef777cec1339cb8f5a1a599ed4973ee22a2f02812dd0f48bd78c1","impliedFormat":99},{"version":"6acf0b3018881977d2cfe4382ac3e3db7e103904c4b634be908f1ade06eb302d","impliedFormat":99},{"version":"2dbb2e03b4b7f6524ad5683e7b5aa2e6aef9c83cab1678afd8467fde6d5a3a92","impliedFormat":99},{"version":"135b12824cd5e495ea0a8f7e29aba52e1adb4581bb1e279fb179304ba60c0a44","impliedFormat":99},{"version":"e4c784392051f4bbb80304d3a909da18c98bc58b093456a09b3e3a1b7b10937f","impliedFormat":99},{"version":"2e87c3480512f057f2e7f44f6498b7e3677196e84e0884618fc9e8b6d6228bed","impliedFormat":99},{"version":"66984309d771b6b085e3369227077da237b40e798570f0a2ddbfea383db39812","impliedFormat":99},{"version":"e41be8943835ad083a4f8a558bd2a89b7fe39619ed99f1880187c75e231d033e","impliedFormat":99},{"version":"260558fff7344e4985cfc78472ae58cbc2487e406d23c1ddaf4d484618ce4cfd","impliedFormat":99},{"version":"a3d5be0365b28b3281541d39d9db08d30b88de49576ddfbbb5d086155017b283","impliedFormat":99},{"version":"985d310b29f50ce5d4b4666cf2e5a06e841f3e37d1d507bd14186c78649aa3dd","impliedFormat":99},{"version":"5c6b3840cbc84f6f60abfc5c58c3b67b7296b5ebe26fd370710cfc89bbe3a5f1","impliedFormat":99},{"version":"91ef552cc29ec57d616e95d73ee09765198c710fa34e20b25cb9f9cf502821f1","impliedFormat":99},{"version":"1ab840e4672a64e3c705a9163142e2b79b898db88b3c18400e37dbe88a58fa60","impliedFormat":99},{"version":"48516730c1cf1b72cac2da04481983cfe61359101d8563314457ecb059b102a9","impliedFormat":99},{"version":"03f346d97547a4fe35c939c3d34af22827b845e4e23f05913706f21144cec349","impliedFormat":99},{"version":"7e864f3e2d8573eac961e3fc9b29be100feec58b48d0e7ca5c5ba58514f74e04","impliedFormat":99},{"version":"2b37ba54ec067598bf912d56fcb81f6d8ad86a045c757e79440bdef97b52fe1b","impliedFormat":99},{"version":"1bc9dd465634109668661f998485a32da369755d9f32b5a55ed64a525566c94b","impliedFormat":99},{"version":"5702b3c2f5d248290ed99419d77ca1cc3e6c29db5847172377659c50e6303768","impliedFormat":99},{"version":"9764b2eb5b4fc0b8951468fb3dbd6cd922d7752343ef5fbf1a7cd3dfcd54a75e","impliedFormat":99},{"version":"1fc2d3fe8f31c52c802c4dee6c0157c5a1d1f6be44ece83c49174e316cf931ad","impliedFormat":99},{"version":"dc4aae103a0c812121d9db1f7a5ea98231801ed405bf577d1c9c46a893177e36","impliedFormat":99},{"version":"106d3f40907ba68d2ad8ce143a68358bad476e1cc4a5c710c11c7dbaac878308","impliedFormat":99},{"version":"42ad582d92b058b88570d5be95393cf0a6c09a29ba9aa44609465b41d39d2534","impliedFormat":99},{"version":"36e051a1e0d2f2a808dbb164d846be09b5d98e8b782b37922a3b75f57ee66698","impliedFormat":99},{"version":"7a5a30ab6ebbde52db8fd9277a5087dbd09ae768b03d75bc5cdf91262e58c95c","impliedFormat":99},{"version":"77ad95ceb744b108c227e59e4223b41275231f4816471095a4451dcfa4d89af9","impliedFormat":99},{"version":"bd3197995140508b18d6c6443e10bf7a2e528d853b9a8f529f8858211d59f22d","impliedFormat":99},{"version":"dff4557148216bfff603f140458b0f4e0b6709933c0aacd9f0620a8db24c5ef0","impliedFormat":99},{"version":"464c71ab63721c4b53746f724c9ab1708b487b7b92025cf22e68ab13030e72ef","impliedFormat":99},{"version":"f8a6bb79327f4a6afc63d28624654522fc80f7536efa7a617ef48200b7a5f673","impliedFormat":1},{"version":"e4b3d925bbf20debf44259a9bed6024ee08c258df89588299925be4003c1e76a","impliedFormat":99},{"version":"c1a2e05eb6d7ca8d7e4a7f4c93ccf0c2857e842a64c98eaee4d85841ee9855e6","impliedFormat":1},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":1},{"version":"6e5857f38aa297a859cab4ec891408659218a5a2610cd317b6dcbef9979459cc","impliedFormat":1},{"version":"ead8e39c2e11891f286b06ae2aa71f208b1802661fcdb2425cffa4f494a68854","impliedFormat":1},{"version":"82919acbb38870fcf5786ec1292f0f5afe490f9b3060123e48675831bd947192","impliedFormat":1},{"version":"e222701788ec77bd57c28facbbd142eadf5c749a74d586bc2f317db7e33544b1","impliedFormat":1},{"version":"09154713fae0ed7befacdad783e5bd1970c06fc41a5f866f7f933b96312ce764","impliedFormat":1},{"version":"8d67b13da77316a8a2fabc21d340866ddf8a4b99e76a6c951cc45189142df652","impliedFormat":1},{"version":"a91c8d28d10fee7fe717ddf3743f287b68770c813c98f796b6e38d5d164bd459","impliedFormat":1},{"version":"68add36d9632bc096d7245d24d6b0b8ad5f125183016102a3dad4c9c2438ccb0","impliedFormat":1},{"version":"3a819c2928ee06bbcc84e2797fd3558ae2ebb7e0ed8d87f71732fb2e2acc87b4","impliedFormat":1},{"version":"f6f827cd43e92685f194002d6b52a9408309cda1cec46fb7ca8489a95cbd2fd4","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"a270a1a893d1aee5a3c1c8c276cd2778aa970a2741ee2ccf29cc3210d7da80f5","impliedFormat":1},{"version":"add0ce7b77ba5b308492fa68f77f24d1ed1d9148534bdf05ac17c30763fc1a79","impliedFormat":1},{"version":"8926594ee895917e90701d8cbb5fdf77fc238b266ac540f929c7253f8ad6233d","impliedFormat":1},{"version":"2f67911e4bf4e0717dc2ded248ce2d5e4398d945ee13889a6852c1233ea41508","impliedFormat":1},{"version":"d8430c275b0f59417ea8e173cfb888a4477b430ec35b595bf734f3ec7a7d729f","impliedFormat":1},{"version":"69364df1c776372d7df1fb46a6cb3a6bf7f55e700f533a104e3f9d70a32bec18","impliedFormat":1},{"version":"6042774c61ece4ba77b3bf375f15942eb054675b7957882a00c22c0e4fe5865c","impliedFormat":1},{"version":"5a3bd57ed7a9d9afef74c75f77fce79ba3c786401af9810cdf45907c4e93f30e","impliedFormat":1},{"version":"ed8763205f02fb65e84eff7432155258df7f93b7d938f01785cb447d043d53f3","impliedFormat":1},{"version":"30db853bb2e60170ba11e39ab48bacecb32d06d4def89eedf17e58ebab762a65","impliedFormat":1},{"version":"e27451b24234dfed45f6cf22112a04955183a99c42a2691fb4936d63cfe42761","impliedFormat":1},{"version":"2316301dd223d31962d917999acf8e543e0119c5d24ec984c9f22cb23247160c","impliedFormat":1},{"version":"58d65a2803c3b6629b0e18c8bf1bc883a686fcf0333230dd0151ab6e85b74307","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"d4a5b1d2ff02c37643e18db302488cd64c342b00e2786e65caac4e12bda9219b","impliedFormat":1},{"version":"29f823cbe0166e10e7176a94afe609a24b9e5af3858628c541ff8ce1727023cd","impliedFormat":1},{"version":"e87873f06fa094e76ac439c7756b264f3c76a41deb8bc7d39c1d30e0f03ef547","impliedFormat":99},{"version":"488861dc4f870c77c2f2f72c1f27a63fa2e81106f308e3fc345581938928f925","impliedFormat":99},{"version":"eff73acfacda1d3e62bb3cb5bc7200bb0257ea0c8857ce45b3fee5bfec38ad12","impliedFormat":99},{"version":"aff4ac6e11917a051b91edbb9a18735fe56bcfd8b1802ea9dbfb394ad8f6ce8e","impliedFormat":99},{"version":"1f68aed2648740ac69c6634c112fcaae4252fbae11379d6eabee09c0fbf00286","impliedFormat":99},{"version":"5e7c2eff249b4a86fb31e6b15e4353c3ddd5c8aefc253f4c3e4d9caeb4a739d4","impliedFormat":99},{"version":"14c8d1819e24a0ccb0aa64f85c61a6436c403eaf44c0e733cdaf1780fed5ec9f","impliedFormat":99},{"version":"413d50bc66826f899c842524e5f50f42d45c8cb3b26fd478a62f26ac8da3d90e","impliedFormat":99},{"version":"d9083e10a491b6f8291c7265555ba0e9d599d1f76282812c399ab7639019f365","impliedFormat":99},{"version":"09de774ebab62974edad71cb3c7c6fa786a3fda2644e6473392bd4b600a9c79c","impliedFormat":99},{"version":"e8bcc823792be321f581fcdd8d0f2639d417894e67604d884c38b699284a1a2a","impliedFormat":99},{"version":"7c99839c518dcf5ab8a741a97c190f0703c0a71e30c6d44f0b7921b0deec9f67","impliedFormat":99},{"version":"44c14e4da99cd71f9fe4e415756585cec74b9e7dc47478a837d5bedfb7db1e04","impliedFormat":99},{"version":"1f46ee2b76d9ae1159deb43d14279d04bcebcb9b75de4012b14b1f7486e36f82","impliedFormat":99},{"version":"2838028b54b421306639f4419606306b940a5c5fcc5bc485954cbb0ab84d90f4","impliedFormat":99},{"version":"7116e0399952e03afe9749a77ceaca29b0e1950989375066a9ddc9cb0b7dd252","impliedFormat":99},{"version":"29c34a50fc73b63f8e49dfda35f64bfe5c157607d9747ada9f222be6ba41be46","impliedFormat":99},{"version":"65661f02680b57b4d68ecc315672b570f3a00a04e50e12d5b2a909933725768e","impliedFormat":99},{"version":"6c3741e44c9b0ebd563c8c74dcfb2f593190dfd939266c07874dc093ecb4aa0e","impliedFormat":99},{"version":"cfc05355e3be906cef951e2fef8282618237fc4a437b358aed11b3e5d147a841","impliedFormat":99},{"version":"9e31632f42f4b65cd48216e7183ad10c57363c464ad536149b86b341578fe832","impliedFormat":99},{"version":"8bcc940343b6801dfa541c430195c16176ef8d74a09f9d1ab9b7b0b66df88d38","impliedFormat":99},{"version":"a65735a086ae8b401c1c41b51b41546532670c919fd2cedc1606fd186fcee2d7","impliedFormat":99},{"version":"fe021dbde66bd0d6195d4116dcb4c257966ebc8cfba0f34441839415e9e913e1","impliedFormat":99},{"version":"d52a4b1cabee2c94ed18c741c480a45dd9fed32477dd94a9cc8630a8bc263426","impliedFormat":99},{"version":"d059a52684789e6ef30f8052244cb7c52fb786e4066ac415c50642174cc76d14","impliedFormat":99},{"version":"2ccdfd33a753c18e8e5fe8a1eadefff968531d920bc9cdc7e4c97b0c6d3dcaf8","impliedFormat":99},{"version":"d64a434d7fb5040dbe7d5f4911145deda53e281b3f1887b9a610defd51b3c1a2","impliedFormat":99},{"version":"927f406568919fd7cd238ef7fe5e9c5e9ec826f1fff89830e480aff8cfd197da","impliedFormat":99},{"version":"a77d742410fe78bb054d325b690fda75459531db005b62ba0e9371c00163353c","impliedFormat":99},{"version":"f8de61dd3e3c4dc193bb341891d67d3979cb5523a57fcacaf46bf1e6284e6c35","impliedFormat":99},{"version":"addca1bb7478ebc3f1c67b710755acc945329875207a3c9befd6b5cbcab12574","impliedFormat":99},{"version":"50b565f4771b6b150cbf3ae31eb815c31f15e2e0f45518958a5f4348a1a01660","impliedFormat":99},{"version":"1529a8d83d945d89dde6f147041286ac729e33ec61b903ef417d6010ec42746e","impliedFormat":99},{"version":"0ed0a01cfb08b2a7f17c061eb0f02ca0a0545b11eb314d6998a41876f21a9e71","impliedFormat":99},{"version":"e37c13ecab314091b479dd40d650abf722965c90dac26f80aadd00797b012897","impliedFormat":99},{"version":"d8f9412761e9e49bc60dc5c862ad446a7a860848848e355a5ae70e4a7c8d065e","impliedFormat":99},{"version":"9bb1316d544e1fc153d572297f15f69f7c5a3f46ffbcfd635ae75b0ec5526421","impliedFormat":99},{"version":"cb9d2c9f566f208e2d1ae7972306871963f8d511b32f28cfba2cdf688877b56a","impliedFormat":99},{"version":"bd09a0e906dae9a9351c658e7d8d6caa9f4df2ba104df650ebca96d1c4f81c23","impliedFormat":99},{"version":"82ca1472f821b70ae67829aae9d5cf9b33c3473b7f4c4522703b5bbb4f24978c","impliedFormat":99},{"version":"b0aa778c53f491350d81ec58eb3e435d34bef2ec93b496c51d9b50aa5a8a61e5","impliedFormat":99},{"version":"fa454230c32f38213198cf47db147caf4c03920b3f8904566b29a1a033341602","impliedFormat":99},{"version":"927923c0cf087f4b9bb480acf913052cc27d2ce3e788290e6f4d3fbe509c761e","impliedFormat":99},{"version":"b9e8a17161bd798b468001951568040f3c8f7b3a605b9991969aa83876817c13","impliedFormat":99},{"version":"20f3098d77ea8cda7c1e255e0553b9972e6e07ec4d0754163f40994b4550960c","impliedFormat":99},{"version":"d6bddb2c166c14a52589a3fe142c26c18431eb082ac03e0212c10715b14dc90d","impliedFormat":99},{"version":"3e628e77503ce26626bfc0dc6ccb543b07a8c60501ce94e78d770eaa5581abb8","impliedFormat":99},{"version":"60f755e0695a41551cfc518877be32fc599a6050a3f59e709025724607911086","impliedFormat":99},{"version":"cd69ea034e7d22157786d474d6479ca4655b6806829062f5425940c01b3a9992","impliedFormat":99},{"version":"2b97e8ae6c304e335c4159ee78c1c1b7de282d1ea4d4cc088edcd7a9cc48b6da","impliedFormat":99},{"version":"0380e39204e3ca59b0793b2a3989782e343e0832f2f2a56461d6241ec43cf1c6","impliedFormat":99},{"version":"40c083fe489e7e64ce708d18d167a3e0b69916bebda536d1ae5deb641c452716","impliedFormat":99},{"version":"5d48cdf280a535f8e2ecbbb3e4dd569f602bdfaf89186d0ebfdba73304717e7e","impliedFormat":99},{"version":"764f8d30dbabc76738a3dfd5b6260abdf40009de4a15311004570df583c29d8e","impliedFormat":99},{"version":"e6da96f97992ed6d5a112f66836185d9ee79bb1d85f756b7db208bd79786e90f","impliedFormat":99},{"version":"7bebedde75534b450e10f90167821a88e8434c2d778b9bf6b810db13b0a8f9b6","impliedFormat":99},{"version":"c2d7b6277102b72ed33d6bdfd672f6e78b51b00bedc708751e6c7f95178f6766","impliedFormat":99},{"version":"0e083ca00d72528fdcbb60a91cb9312f7ca54e9fa2df27d2e0f9701a09d2eaff","impliedFormat":99},{"version":"353101870de182f06de25aa1e2aeb3e26b1215c88223939919e45540967dd486","impliedFormat":99},{"version":"5cca5c1f12c16ed7dd00e360f443800f5230df04dd143f3fe797403c66353971","impliedFormat":99},{"version":"dfcebfe0320a025d873a7bfbc6914c55a9542b6a4565d640d625b66eaf9f7847","impliedFormat":99},{"version":"cc19f218e42c06bec821a9369c9269c0d44d4c8745b1273f55ece88081e17289","impliedFormat":99},{"version":"25833091917b0f02eeba6724e7d287943e4add968d3e124e577c4421fbf5e505","impliedFormat":99},{"version":"df4242d09e2a83e629ba7bf6b1907be08bafa27f15d60b5a24220b74f48a2082","impliedFormat":99},{"version":"f3a7e33709e745276b57c012aa27daabbb84d488c0a894619876c536caf79557","impliedFormat":99},{"version":"89dcbbf69b16cd94043e16c7fbcfa04256577ec98bb8ae894833d2a922394db4","impliedFormat":1},{"version":"d9a75d09e41d52d7e1c8315cc637f995820a4a18a7356a0d30b1bed6d798aa70","impliedFormat":99},{"version":"a76819b2b56ccfc03484098828bdfe457bc16adb842f4308064a424cb8dba3e4","impliedFormat":99},{"version":"af1120ba3de51e52385019b7800e66e4694ebc9e6a4a68e9f4afc711f6ae88be","impliedFormat":99},{"version":"25b6edf357caf505aa8e21a944bb0f7a166c8dac6a61a49ad1a0366f1bde5160","impliedFormat":99},{"version":"d391200bb56f44a4be56e6571b2aeedfe602c0fd3c686b87b1306ae62e80b1e9","impliedFormat":99},{"version":"3b3e4b39cbb8adb1f210af60388e4ad66f6dfdeb45b3c8dde961f557776d88fe","impliedFormat":99},{"version":"431f31d10ad58b5767c57ffbf44198303b754193ba8fbf034b7cf8a3ab68abc1","impliedFormat":99},{"version":"a52180aca81ba4ef18ac145083d5d272c3a19f26db54441d5a7d8ef4bd601765","impliedFormat":99},{"version":"9de8aba529388309bc46248fb9c6cca493111a6c9fc1c1f087a3b281fb145d77","impliedFormat":99},{"version":"f07c5fb951dfaf5eb0c6053f6a77c67e02d21c9586c58ed0836d892e438c5bb2","impliedFormat":99},{"version":"c97b20bb0ad5d42e1475255cb13ede29fe1b8c398db5cba2a5842f1cb973b658","impliedFormat":99},{"version":"5559999a83ecfa2da6009cdab20b402c63cd6bb0f7a13fc033a5b567b3eb404b","impliedFormat":99},{"version":"aec26ed2e2ef8f2dbc6ffce8e93503f0c1a6b6cf50b6a13141a8462e7a6b8c79","impliedFormat":99},{"version":"9d62e577adb05f5aafed137e747b3a1b26f8dce7b20f350d22f6fb3255a3c0ed","impliedFormat":99},{"version":"7ed92bcef308af6e3925b3b61c83ad6157a03ff15c7412cf325f24042fe5d363","impliedFormat":99},{"version":"3da9062d0c762c002b7ab88187d72e1978c0224db61832221edc8f4eb0b54414","impliedFormat":99},{"version":"84dbf6af43b0b5ad42c01e332fddf4c690038248140d7c4ccb74a424e9226d4d","impliedFormat":99},{"version":"00884fc0ea3731a9ffecffcde8b32e181b20e1039977a8ae93ae5bce3ab3d245","impliedFormat":99},{"version":"0bd8b6493d9bf244afe133ccb52d32d293de8d08d15437cca2089beed5f5a6b5","impliedFormat":99},{"version":"7fc3099c95752c6e7b0ea215915464c7203e835fcd6878210f2ce4f0dcbbfe67","impliedFormat":99},{"version":"83b5499dbc74ee1add93aef162f7d44b769dcef3a74afb5f80c70f9a5ce77cc0","impliedFormat":99},{"version":"8bf8b772b38fc4da471248320f49a2219c363a9669938c720e0e0a5a2531eabf","impliedFormat":99},{"version":"7da6e8c98eacf084c961e039255f7ebb9d97a43377e7eee2695cb77fec640c66","impliedFormat":99},{"version":"0b5b064c5145a48cd3e2a5d9528c63f49bac55aa4bc5f5b4e68a160066401375","impliedFormat":99},{"version":"702ff40d28906c05d9d60b23e646c2577ad1cc7cd177d5c0791255a2eab13c07","impliedFormat":99},{"version":"49ff0f30d6e757d865ae0b422103f42737234e624815eee2b7f523240aa0c8f8","impliedFormat":99},{"version":"0389aacf0ffd49a877a46814a21a4770f33fc33e99951a1584de866c8e971993","impliedFormat":99},{"version":"5cb7a51cf151c1056b61f078cf80b811e19787d1f29a33a2a6e4bf00334bbc10","impliedFormat":99},{"version":"215aa8915d707f97ad511b7abbf7eda51d3a7048e9a656955cf0dda767ae7db0","impliedFormat":99},{"version":"0d689a717fbef83da07ab4de33f83db5cbcec9bc4e3b04edb106c538a50a0210","impliedFormat":99},{"version":"d00bc73e8d1f4137f2f6238bb3aa2bbdad8573658cc95920e2cdfa7ad491a8d8","impliedFormat":99},{"version":"e3667aa9f5245d1a99fb4a2a1ac48daf1429040c29cc0d262e3843f9ae3b9d65","impliedFormat":99},{"version":"08c0f3222b50ec2b534be1a59392660102549129246425d33ec43f35aa051dc6","impliedFormat":99},{"version":"612fb780f312e6bb3c40f3cb2b827ea7455b922198f651c799d844fdd44cf2e9","impliedFormat":99},{"version":"bcd98e8f44bc76e4fcb41e4b1a8bab648161a942653a3d1f261775a891d258de","impliedFormat":99},{"version":"5abaa19aa91bb4f63ea58154ada5d021e33b1f39aa026ca56eb95f13b12c497a","impliedFormat":99},{"version":"356a18b0c50f297fee148f4a2c64b0affd352cbd6f21c7b6bfa569d30622c693","impliedFormat":99},{"version":"5876027679fd5257b92eb55d62efee634358012b9f25c5711ad02b918e52c837","impliedFormat":99},{"version":"f5622423ee5642dcf2b92d71b37967b458e8df3cf90b468675ff9fddaa532a0f","impliedFormat":99},{"version":"70265bc75baf24ec0d61f12517b91ea711732b9c349fceef71a446c4ff4a247a","impliedFormat":99},{"version":"41a4b2454b2d3a13b4fc4ec57d6a0a639127369f87da8f28037943019705d619","impliedFormat":99},{"version":"e9b82ac7186490d18dffaafda695f5d975dfee549096c0bf883387a8b6c3ab5a","impliedFormat":99},{"version":"eed9b5f5a6998abe0b408db4b8847a46eb401c9924ddc5b24b1cede3ebf4ee8c","impliedFormat":99},{"version":"f99db2cd80274f32467ae5231d74632c35596ab75fa65baac3fec9dd551cf9d7","impliedFormat":99},{"version":"c799ceedd4821387e6f3518cf5725f9430e2fb7cae1d4606119a243dea28ee40","impliedFormat":99},{"version":"dcf54538d0bfa5006f03bf111730788a7dd409a49036212a36b678afa0a5d8c6","impliedFormat":99},{"version":"1ed428700390f2f81996f60341acef406b26ad72f74fc05afaf3ca101ae18e61","impliedFormat":99},{"version":"417048bbdce52a57110e6c221d6fa4e883bde6464450894f3af378a8b9a82a47","impliedFormat":99},{"version":"ab0048d2b673c0d60afc882a4154abcb2edb9a10873375366f090ae7ae336fe8","impliedFormat":99},{"version":"3e61b9db82b5e4a8ffcdd54812fda9d980cd4772b1d9f56b323524368eed9e5a","impliedFormat":99},{"version":"dcbc70889e6105d3e0a369dcea59a2bd3094800be802cd206b617540ff422708","impliedFormat":99},{"version":"f0d325b9e8d30a91593dc922c602720cec5f41011e703655d1c3e4e183a22268","impliedFormat":99},{"version":"afbd42eb9f22aa6a53aa4d5f8e09bb289dd110836908064d2a18ea3ab86a1984","impliedFormat":99},{"version":"4b19a27eaccfc0549e5a68d2012546bfc62064842d923c30699381460d32b472","impliedFormat":99},{"version":"97a50b01aa5289ea4e360a4d86f2dd8fed81a03ad657439b369dcace978feca2","impliedFormat":99},{"version":"7c06703b5cba08462692c2df83b4ec2faedac46ecd3a6a83240db8729f0efa71","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"8518c0e9b94e14052c39cc87cb9621324ffb94f240c65b494b8b61b1cbc2c824","impliedFormat":99},{"version":"64ebf98bb8a7a8a864c2342d89707fffd55742878775bd034ae71c2c33e52ae5","impliedFormat":99},{"version":"a2df82aea8fc96fb157dd2926a39f60c3aa47fe7921f19580a7fbb69d1155a44","impliedFormat":99},{"version":"9801142fb84cba8344715572bcc481b647894b2a06a0e11cf11dac017b2d8b91","impliedFormat":99},{"version":"8f1b4277b405f95874e8e572936927309a11476813ce380e8a0f94139379b74f","impliedFormat":99},{"version":"06620f9f9eea91a1729e3b15ada2355c8b307a02dc6c94ed23cb3f95c4e9a30f","impliedFormat":99},{"version":"fa3f11bde7ecde935f3aeb7a2c5195e7f12a06e4c8fd410575c563d37adcb018","impliedFormat":99},"1a31918039f605e863da70982270a192037b9db1ff47d5ad645008fffe7ea954",{"version":"62cb04a4dabe59c2ec8065d6fca3f1c2259ebe8240bcc673f0af50f215f5f80f","impliedFormat":99},{"version":"2caec5026b5ca5f720d204ecbb62f86fb15ae6a96c25d712401a89f79b07c98a","impliedFormat":99},{"version":"5ee037fc16675d2b02970d18fdcf269da9e335a04d28f5931b20d8518a8f64bd","signature":"8b66059baf63efe371e6af4d399157c1fe3ebdb31776b84b247e511e0df47f13"},{"version":"8de5bf5a26e967191d40de27df9be18ba82841f88664a73654d20d9cd05fc041","impliedFormat":99},{"version":"bbe0f8b7ea086b27f00960f897eec4b8077d9feb313e79aaae0e53e6ee7c7f73","impliedFormat":99},{"version":"87811c6c4813091299c878366b6b27452529f63ab70351c8a4a9e703590417f8","impliedFormat":99},{"version":"fe4247a5161969bc7ee06e7655d96f118f6b10b2cb681ebdf332c7d4da84f5af","impliedFormat":99},{"version":"0b0604c013afc1fcf9e5b5185d913402863b9be554b155ccd13b343fb6d5af6c","impliedFormat":99},{"version":"d26ae4cbfa2957baee8a6f2987a82e597f18ad98e8d08f5b6c627e2abc5e2630","impliedFormat":99},{"version":"b640c9b2090fd42bd34d877b4da63b984f77151e8492d804a26353258e28b7ca","impliedFormat":99},{"version":"c36739f58aadbf09f620712c3531a0bdf944b16b9edcd16a6c1f3d7d935f3f5c","impliedFormat":99},{"version":"94617f973c27753e95a3f78cfa2298ec16fa4c1873360124b3fb135f94d203e9","impliedFormat":99},{"version":"949107fa4b339e34b9fca78611d63776cf5952927e500d72bf108fdef03d3a42","impliedFormat":99},{"version":"5a76d9a6dd9260f407a8dd8657179d972640b1f6736744ff397921ba891d8d99","impliedFormat":99},{"version":"309757128a194184868dbda49a9583104e10d5f8ec0b144b5adea8faac559446","impliedFormat":99},{"version":"d22ca62641feb3cc9320464b4f1599d02791ae81c96b1d62d52cb58589a8aeb1","impliedFormat":99},{"version":"5edb6bbdd99524132182d829a58beca3da66609cf5d4ffcc79798a293b77b41b","impliedFormat":99},{"version":"3dea506a24fccaa833a3b7b154751904398ab945f7507de0e99563509a0bd886","impliedFormat":99},{"version":"44674355dd062213ce67cfd3ef343c59835748de456b5cd2b13ad92531ab5dac","impliedFormat":99},{"version":"a851a0369c9c4ee56abe7ecbf3b73841f4731da108192887bc74e3a9eb3289f2","impliedFormat":99},{"version":"9760ba531aa6e82a8e9a60d101b65ddae9d398e2f91770595d9f1c5f77931e9c","impliedFormat":99},{"version":"e2b27cc682a1368bfe175961bac462ac908de1dfb49590eea5550fd5bf9a6f29","impliedFormat":99},{"version":"49b088cac71a031ab09cc6d68aaba7b57e373693c72ea56dbcca094493a543e3","impliedFormat":99},{"version":"e546f06410e5259e513d9c85f3ffad6074634c223dfedbcc6fcce144176303bd","impliedFormat":99},{"version":"c049f4fe2068d1f7954e4fed057623cb870969eaf5a71bc77b5a677bf5af3f24","impliedFormat":99},{"version":"76cf68162903a40eddf28199107e921b9c08c033eac1c07efd4dc979fd4884e2","impliedFormat":99},{"version":"41c111ea5e8946726775a71c224438a7a7009329d1b6119b1c8dae82e433f6dd","impliedFormat":99},{"version":"305af9c2a8c308a4f430c3f2b81d68119f4920ae5942c3109d1178d879a028ac","impliedFormat":99},{"version":"d0f0427311a816aed0f31eca5bed01d2c720ef0c104f8b6b62c86101556f716b","impliedFormat":99},{"version":"a5bd6ae20ff492c2ec9a0eed17d73cc2a722c09d12a8cdf321d74c40de279686","impliedFormat":99},{"version":"ac25bb824a57d31b55be466a44ebe002841d19291da64cd6ae7ce6525b7a7b04","impliedFormat":99},{"version":"1c51722bdcb3e6bdbfd301fbaf546638dcebae9e6a02716e21373343096851b9","impliedFormat":99},{"version":"49cb95b8f3503bf3bc2481d747603e5ef2d541466b96c4b349c2296def086882","impliedFormat":99},{"version":"cefd620019820a3980cd72b3e41af68a97e42e095026f50cf95e6304307cfe48","impliedFormat":99},{"version":"ed2be494a8b56779518444accb7e616f85dbcbb19043a5adfe2b2ce9d92b52dd","impliedFormat":99},{"version":"79fbdd164ed7ef0ee1d9bdb3f65ae228d6c66aa915a46154912164717a97b96c","impliedFormat":99},{"version":"c542323dc44c3c36a1576c347164ad2167b4fc8c13da9610c7cb866786637c18","impliedFormat":99},{"version":"55b13f885a41b6ae500f75d5c588d05f78bfdb471d9b04cce391aaa9a3f9f05b","impliedFormat":99},{"version":"4c62dee16a75c9b0ee2b7ad080a8778c8ed8392e5297c9e07f001148b59f5ca0","impliedFormat":99},{"version":"48eb9347a1f264669519d10aaa422de9a6053b41b3fe84911ea8898a942b922b","impliedFormat":99},{"version":"afc496c3b64cd9d58d63640845132e08e48e05a29ddf2f1aa23cd46bfac3a584","impliedFormat":99},{"version":"05c8ef07d5dfb84772cedee2faca46c8ca54d151c16871a6643ede5e208daf05","impliedFormat":99},{"version":"24ffd8a17ccccb3e56570ca53ddd02e772f0b6a4ad3c5616ae642a7e508b7cbb","impliedFormat":99},{"version":"fbd75ba9061b448f015dc71a9ea3891ea17d361e6106ac1356aee358420a8562","impliedFormat":99},{"version":"4ee579b9d37f6198fbc1d3eae2aacda3b7ec6ede001f455e6ce12aef07d5ec59","impliedFormat":99},{"version":"3d4a999e994018e2440e6dd37afecaa4154d616052df8a28e2f1a2259f19b81c","impliedFormat":99},{"version":"db1521b3a21573cf7a452af203effa3482cf0a5b8454e2af4a1f484306a64974","impliedFormat":99},{"version":"45c4a138c13303d5fcfbab589251b7869887f7eff622a2efb7925e2ed7797420","impliedFormat":99},{"version":"1b14568ba50dc5776828b4c6e1be1e7b3aaba855680eb3ac9b07c8a00414373d","impliedFormat":99},{"version":"34091b6bb864d275a0c9080bb1e72065a616d176d3eb00d0402326bf83e206ff","impliedFormat":99},{"version":"11b55793b4507c2cc763198427e11a0bd5e931aee36e265bbbf21632dbd4872a","impliedFormat":99},{"version":"e831709cb7f77e60ebdad8982c5751ba745a77573cda6f44f4ab8d4027ae88be","impliedFormat":99},{"version":"c0defffbf81eedd1d7e9f54b434866942c2ab6c5381f8cc6d5022dca5888d07f","impliedFormat":99},{"version":"e8b639c2a91e047787f660a9dd3895a2ce1200fcda7560841d5637f9583c8db2","impliedFormat":99},{"version":"767e0b38fc031fdf1ba7fa253f264912774e67d1d52ce4e852eae8c3985ba6f1","impliedFormat":99},{"version":"8d37af9291a8ac73663132c067ffb65e649254333a9dc86a1b22361fb88cf7fb","impliedFormat":99},{"version":"8cbcd14fda4ed21695f26c27018361772c9d5f754c702620f42436f0f542c1dc","impliedFormat":99},{"version":"90d4a3535371f8c168b88ab0da2562f754810e26455c95698e98b15f11da22b0","impliedFormat":99},{"version":"12df154d3dd79dc0a4b71fc1e3c5d5fbdbb6a5dfd99c68783020e56743fd4776","impliedFormat":99},{"version":"4f99756bda4a08559be8508b7e83a5be0a93dcaab68790310abc4fa09b93a849","impliedFormat":99},{"version":"2349b5b7d1a01add8c3f99286cefad821143f1df1aae0867cf813f02237a1728","impliedFormat":99},{"version":"09e449359562a9ccee76260902e48260ea75ece6e8d17abe9e7593d900a320eb","impliedFormat":99},{"version":"a6a33dc9ae9532283825868ee5af9118ebc26978ced995fcbfdf90ff7c5e2f0f","impliedFormat":99},{"version":"dd4de144ff9e9db9fd3dbca7e1fb7f639b39ef953f2b3c49240c7f2a9736fdc8","impliedFormat":99},{"version":"a0f9aae8392286dc83d0712328a0c066ff43594158ff67606b8f9c1aeb69a823","impliedFormat":99},{"version":"4c9dca53c43c6cde0627e9851dd56d958d6ae68562e2913cc3abd1ef73f9a00b","impliedFormat":99},{"version":"6914eb2bebd7fda4f9fe8db98ee09f5554a1ced9684847b3e27dbefbdb87b1dc","impliedFormat":99},{"version":"dd21f9d9947f9e4a773809cc37b0b094607afdc83cd0e79f2d97250c4501b6c7","impliedFormat":99},{"version":"2066f78e539fc3e9b9a756cddea61af372d9f35be7ffa9497c6704da56c9e61c","impliedFormat":99},{"version":"3d3fa0b2c95631d01e1291b98adf0423f2a74a87db32df8fe0c062b6db6eabff","impliedFormat":99},{"version":"bab5341c5fbc115a91a38bc1dd18a3b8f9e9f6601f898eed8bf6b56d2469b823","impliedFormat":99},{"version":"0d1fd7906b3767ccf874e84b76817b71c81836b02f9c5fae2daade6fc26f1b2b","impliedFormat":99},{"version":"828d97f23443ff54c85832f7fe1bd2aa204cc761193f2fdb95391c3af2eddb4d","impliedFormat":99},{"version":"47aa55962c8eb1bf8f46e84aacd04d89296bb9ca3a2978382cb81bc4f1c726c9","impliedFormat":99},{"version":"6df68fc4f79f293f1ab2d25951f191161ba0b13ab2c06b9f0dbba5513c5a0e1e","impliedFormat":99},{"version":"cb6bfbb5f55783ddb18adfce848dd24bcf85bb777bf77a0d71dd773cc7b62c22","impliedFormat":99},{"version":"286f368d27af276cb55d1130452b98b9956bd59d142a62b514ee6f1f98395271","impliedFormat":99},{"version":"b046da4f4d04ae2789837a9c1c32896d2d99256655b4377e1e47c0945288f9c1","impliedFormat":99},{"version":"0397b33949bba44c603a8919b1cec9428bbf84ee3c90819b080dfbc764e01724","impliedFormat":99},{"version":"7f971c8a26679b60b7765f1a9da29b987a93795909acea2896b2da567b2429bd","impliedFormat":99},{"version":"55db20deb5ce62732518b1fab73d5b3609113c027f58658c1f3104f8c9073e8d","impliedFormat":99},{"version":"279792abef7ddfc2566cf92e89447f0f2fbf3c5e1ce8e01a2325d6b56e513317","impliedFormat":99},{"version":"4d1276fa5da13705e9e23f77bf97aa8686c37670d3dbd6e85d5975387da6e9c4","impliedFormat":99},{"version":"4d43f7782e2ae2d5be0449e202e894dfdf0b5e8fcd8fea275b1b1795045ea3a7","impliedFormat":99},{"version":"4c7e9a01365d12cc787fa964cb28503b4bad69cc773e25f883ff53fcd049237e","impliedFormat":99},{"version":"791d109b55c4589a49c89729aa8df87d88ad546a322a4364ee3770a845f93a0c","impliedFormat":99},{"version":"ad2aadb0857cbc6b0a1177ec6b8944b734f5c6c9481baf77d7f3b2c0fcc5b2f0","impliedFormat":99},{"version":"319d7c7aabd2bacb683985cadfe402cf1ce13185241638ae98ef2467c111717f","impliedFormat":99},{"version":"1d8c74c5747df6706dece004ea4c537ed5080e95b90d1a1620bdaf46bb8d180b","impliedFormat":99},{"version":"2e0fbb883f847aa1bcb15d265757653f79d19fe679c2b84adc5b12a1b83371f6","impliedFormat":99},{"version":"e62d894e8ccc27d596235f8cc055ce0d27772eb13b310577ad7c2bc4e2f2b23c","impliedFormat":99},{"version":"bbb48c9b123ebf986aecbdc2085387b279b9db9822a99075135c164d4bc12c99","impliedFormat":99},{"version":"f46ed0abe3275015dac23dfc2f7c0d7aba8d7378f9dec9a0dea5f9565bd6f582","impliedFormat":99},{"version":"0dd3e4ae8e0f3470f2e5dc1fd95600e2f0753112d3a7e0f61225a81e8389a6c8","impliedFormat":99},{"version":"8fb8660a87df46177ea76db4338c4475b8b8e7e42e36bf124195489cb6fc9ae2","impliedFormat":99},{"version":"55f0aea7d8e5668fd2b74231106be4dd6c75563e8e794a76b8539c5320a3b41d","impliedFormat":99},{"version":"f42f470b9934ca024afac9c220e5c57b5b70de2383a821e3e7d58b5de29f6646","impliedFormat":99},{"version":"22f011193d780d619a9a4dc9fd0307bfc0c7869d496e371245d50b72a0e1d0fa","impliedFormat":99},{"version":"acbdb315c0433c6832fb64027dd421c1d193551e20b5da391cdb20c6b8f95dc8","impliedFormat":99},{"version":"d7ac7a29f5fc29b2b157fd2ab7738946597a29b5670c2290d4b81aada8fbf6bd","impliedFormat":99},{"version":"7f85382c44340cc8c380bfc3dd2e5c69c280308538044601788e2f1ae3e55a9a","impliedFormat":99},{"version":"7d7e9f13134199ed1b0cf6c8775a614c1bf4341ae74edade456500546ab2776d","impliedFormat":99},{"version":"f57d7c9fb29a389e44af09d54c57d9be1a130b80537af035334f9a8f2e55fa76","impliedFormat":99},{"version":"0f76779994fafbeee4d135c21fb87d35b81c6fa913ef85e5b6bccb3ffcbd57db","impliedFormat":99},{"version":"64c4f811d9ace920e8df2522f72905cdeafa7812166416efdd51a9350d361628","impliedFormat":99},{"version":"cdd9d75a5e91d288b1f009ed8d885e29dd61d04cd12fe66645a69e9364fc2fd8","impliedFormat":99},{"version":"c81859d7edd8ea8016c3ef669bde80a68a0a8e85440307d6c07c0e47e6e70a7d","impliedFormat":99},{"version":"a1d72f11de9816b4776ecadea48d9c2a8c486cd8b465b674647557653a151a0f","impliedFormat":99},{"version":"33b8b6eca734548828961d9ecc9c21f729f08bab8c2c9d95c9ce772db6431f94","impliedFormat":99},{"version":"1861836a3156845df6c43d92e7634515050e349eb569d5ee523e25a2ce6dbc55","impliedFormat":99},{"version":"4dade3290736a203b3e191085b91bc93c9a51dfe608211d314ce688a7d1915c7","impliedFormat":99},{"version":"558b4eb69385b53bb4f76dfb9944da2b324c97f4cc915f8a57ffe12adc08bfc2","impliedFormat":99},{"version":"57b6feaf806b2ba926d3000f59436460462d6b551397322dd983bca10cb82083","impliedFormat":99},{"version":"9a599074752221456c6ff3fab0a9e5e42318e610bb21a23c1364d12b0669d01d","impliedFormat":99},{"version":"17854000164e5e5ef1318f5f542b770d80b0374fa95ac023e01a91440d93de35","impliedFormat":99},{"version":"fcaf10eebf077d5a0e4be81424fcc296905533359c46ba1198c41e98e957d70a","impliedFormat":99},{"version":"71d9bf57ae0b6685fa69e800f44a0e040d1418f74737bb590d71374f02a14f5d","impliedFormat":99},{"version":"a3e8df36aeafd3a63629f85cf72f46be39de7f350af3cbc40bc7917f41b36861","impliedFormat":99},{"version":"86fbc8e6592abc2775bafd3c2b2eb5cfd0e37c4d57e0893523dea6d67c19f05f","impliedFormat":99},{"version":"14a976b28005d68fda91658eec498db736406afdd383a6a4e674ba2528815348","impliedFormat":99},{"version":"e6acc224ffaf9707ce0d3914ce46ed083c7b6469a0f195543fa7cf64b648d4fa","impliedFormat":99},{"version":"8ec434028fb805c17f244b68ccc992391ff8ec6ba0c4bed1d04e51741f344e90","impliedFormat":99},{"version":"e2a0a09c2e1fab39ea345ec351a93f93ce425d2c6234606e705b3a6c84b8b46c","impliedFormat":99},{"version":"e036c82ae9c6a656dc591ddd4f68f70377e6900e7e4042769d4cea7d215bca3f","impliedFormat":99},{"version":"248ec4811949f314a09307eae53df10b95aa35546a9e3dc6363a2916b02195c7","impliedFormat":99},{"version":"e435436a95eb581e852b35d6f5eaaa62be7a8bc6da288254ac3394c60511e5d0","impliedFormat":99},{"version":"5cc46f1e20556b157f798a8043e6447c88f118859810d55d9b845e245b17f2b9","impliedFormat":99},{"version":"20fa43bd305c2a0f27590ffb829d5897db5160044136e1c216239a8fb91cfc60","impliedFormat":99},{"version":"3c9a0663746013d1a7f2085ca9fd23d4a1d81831d105f55d83499643db339a6f","impliedFormat":99},{"version":"cbf9bfc5883bd84a49685342affdab376d54622d075c360c1e9ee0cb1a52176e","impliedFormat":99},{"version":"b67eedd415da9341688bfc4897f67951e6f6544130d339d631c578d9ce5179d4","impliedFormat":99},{"version":"74dfe23f35f7f780bc43517662cf6bf54e2340fba8c96499a9345f5c54310765","impliedFormat":99},{"version":"216e481f8b352b3381c3ca193245c9e70eb988f13b11c90560fce7f58046d9e4","impliedFormat":99},{"version":"442f979c95b63bc7545f20a9dbfacf969a8bf8d50f38ff5e8647883af962ea90","impliedFormat":99},{"version":"7ccf30591af329e39435375251583e00f458be6e9412ca78457fd618c3ed3219","impliedFormat":99},{"version":"0e4023053e2d67cb1875fd3fccb3d9719dd35aa7ed8dac2f8f0a6b83eac1c619","impliedFormat":99},{"version":"121d72cd64ce108c997080ae85d77526c0bf92cbabd595e85b22cc5b2eb5356d","impliedFormat":99},{"version":"2e58e0a3705c2c5e2f8990d9fe54a987140eb6b07d39969667fb5a61e0012164","impliedFormat":99},{"version":"0ac60f7dd28ae5fd26af8a26038bff0a0860f2a934bc3eff3508b9ea606100ee","impliedFormat":99},{"version":"23e86961cc70c48457a30b54d2a7cb321db04ec37a2ce709a558935874ef5980","impliedFormat":99},{"version":"813924ffc37d482abc63f7809402b6d04bf10ced84ceb0ce5b593f93000b0738","impliedFormat":99},{"version":"d39f8d34fef31f645d4a5036d21b6d76f1de2fcb002cd976c7efae530249d79d","impliedFormat":99},{"version":"3dde9915cefa4b06bdd06d0afacb2008400eee07fedb70cd4fe895be5d7c37a1","impliedFormat":99},{"version":"6fa7984697bbae60ab189ff7930691b20d214c1fb68ed6098cef5f616ac8d3d5","impliedFormat":99},{"version":"49cb62a7fa903a66787723065158a23455550d938649544b9afa0fddf13da5a4","impliedFormat":99},{"version":"8093af89ce523d6325f01b1502b431dea33dee468ea0b207f3ecb6b8e8c869fb","impliedFormat":99},{"version":"1fd777885f679efef7156857398a8e40a65f48f9d4b53b22188593e5d9a67f7d","impliedFormat":99},{"version":"381f82f83cfc6930ccbc008af3091f09a75242ae15d2e04632db7fc4e44c0080","impliedFormat":99},{"version":"fbca59f60b22cd0db89922e7f057c713b027aaa1e2c2818632fea718f8200632","impliedFormat":99},{"version":"1bd895f3a629b01cf0ce259e989e8091ef945cb39e29bc4f5dbb35ab66718b91","impliedFormat":99},{"version":"ba8d03eb92363477a46ccd739f48b8a87432bc39a4e0ccadb1bf336c290b103a","impliedFormat":99},{"version":"6ec0dc970053f2d727d22210393569ebedb4ec3a53c0a5e4ac0cbfa179814ef3","impliedFormat":99},{"version":"4220374c20d714335b11a60a1b0392dcf3c0ac82f0773466b731c3ff758a1fa0","impliedFormat":99},{"version":"ddbd6bec7bf000be900df2f6ad03461580cb94c02a2b237d601f091c7f749ee7","impliedFormat":99},{"version":"a4e2ebf51e2314ddbd330380d53527e3acbae6d1fb4919c35f6e6ec534eeca88","impliedFormat":99},{"version":"8aadedc389b7ce3087eb1ef11b85a79ab2b15a418ca0d469159091f33359096a","impliedFormat":99},{"version":"351223dcd7cc1c134826feb03e21e9d40d39497e4641f6640b5b5f118c731cc4","impliedFormat":99},{"version":"3b112575da3840ac278f8edb8898acb94b9c79df94b041b4eb17e3fe1263a643","impliedFormat":99},{"version":"902151bc4db55dd270dc9f38cba4f06c2d5fae54c519677cc3e1197335c58581","impliedFormat":99},{"version":"0b82763f479497a1517786b27d8826d3640d15af245804d322eff12710c5a652","impliedFormat":99},{"version":"c1109b59c4c5033695016057e596c8d34abf048a3e2c1b28169c21b22cba56b3","impliedFormat":99},{"version":"8a35fefe86f688f467c8bef9bf7b465e9be8e5b3476504096076e535127ae3c9","impliedFormat":99},{"version":"00040c315b5da891b7368217aa2d92cc5333f92ea0272ef70806aa5a16d642f1","impliedFormat":99},{"version":"d32d0644afaa7ab25fab4ba7f0c30e194cf211463a9100bf406c24f67573babe","impliedFormat":99},{"version":"9fac0a75a13381632fc5bd29ba84c5f6706c11c447adf8f6628a9ca519c2914c","impliedFormat":99},{"version":"986204e16e3297bca5e64b327e52f9fc4c65bc34497227f63b1a48bfcb333dec","impliedFormat":99},{"version":"824b4ef28c193ed47adf80450a0b8738313561707fe300eebac14ea2405eeb04","impliedFormat":99},{"version":"b634c2e561e3bf8efbf1b79060a9bd5b26022a71574900c3e124a14bb4c4c71a","impliedFormat":99},{"version":"76b8eeff48c0605828e2037ed6bf67d25675860a2f6a8fa1848d3930c2ae871f","impliedFormat":99},{"version":"d7ffc3f3fb6c878d49a92e0e90b737cb09642f7044d6ce3bf53f5698580d8066","impliedFormat":99},{"version":"5ffc2c7cc0be96e71688f7a0062f2e85c6e68b21e605d073a5a6c6a1f3a2a5fe","impliedFormat":99},{"version":"8eae28b0fd19d123cc1b99840528bb1cdf76ba7f0b15034da949d73aacb9702f","impliedFormat":99},{"version":"9b4cc49d65c87c5c38ac19676dc14aa914b13b66ab2f8c93c53a65272e0486a6","impliedFormat":99},{"version":"21c521798aeb2a82dac296829f7450a791b6f154b8a8208f77c0f7370d5f34a4","impliedFormat":99},{"version":"a383c2d014291b0810ebc39adec74792c53ac0fdbd1ec39c87779a8d07e7ec9c","impliedFormat":99},{"version":"635d4c2bbb40d953dac16ebcf52b123b4bc86e58bf6564d0dec062a4c489ee21","impliedFormat":99},{"version":"eb420edce35fac3ff390bbb9d2d98325db9c11a2cd60e0cbdfc1b118b62cf1df","impliedFormat":99},{"version":"f4d93034f0661b40ba9cf90d7d3474d5c0be65fb7f4f3a1d5e9c34210aadb6aa","impliedFormat":99},{"version":"1ebee1d844f9cfa95e5315d90140f87eb1519c8739e7a51111142d3838ce31c4","impliedFormat":99},{"version":"4f8e5dda56f05c96d1f567bb96f32e81039d21ac9af5b4d5fec500f090f21388","impliedFormat":99},{"version":"6f74bde7fdc1ca03aafef483d4f1b0ca50489094b0ce52ce5741c71635494d8c","impliedFormat":99},{"version":"189a993d40cf154596f0becfce65d93b0e3d8c62a1852b9506d86d68b3702d88","impliedFormat":99},{"version":"c1d358eb44c209b7b5ad87abd6d889e28513718dabb6802de8844e517a89730f","impliedFormat":99},{"version":"dfce10bb8fb7c37e9625e889e27519567aa36329ef0b42094dfeac141dd45793","impliedFormat":99},{"version":"dae38b8a375a2e3f8820861c302e675fdeb6981276fab61ad1668c7596e7deab","impliedFormat":99},{"version":"4967ae99536846dd5ac9f3af41ad296b9c36d611a52d7b00ad686304bd23d71d","impliedFormat":99},{"version":"e053193d1742f8702be0bdfa544ad435927557f7ffe67a23ef92b176e60a7657","impliedFormat":99},{"version":"4f1c51f422685db98ead37cf10e723379d21cf990db0d6fb8000b48ea6c254ec","impliedFormat":99},{"version":"951543da1d33a4ed5c53714a1f4ec35b51f8a1c61107649c5ba6d9a1d2146103","impliedFormat":99},{"version":"d763c971f8591a21a1f01c101ee8a769e52e412fe7b1a455a9ea71dc88a758c4","impliedFormat":99},{"version":"b4d7cd9d4092923da05ed18680138dfc72f236181f5618553d75e0ab06f67964","impliedFormat":99},{"version":"2784337e4229d64fcfceb6c4ffebec4d7fee7ac312a316f8eac1a4f760ae20b3","impliedFormat":99},{"version":"63db0b6285f515f8ae6397e307d3b26a56a5249fd80bdf7569ab4b87aa87ca55","impliedFormat":99},{"version":"7ba72b7633d2d2e507f07a59027e24505577797753965b575416ce8e89a04270","impliedFormat":99},{"version":"1762e04cb04fc69364160a0e3d121c63b85945f842575f5e3abc0392acbdaf81","impliedFormat":99},{"version":"b96cdad7e7b0fd253213617c6f76f79444e6c151415584d9195266a9203b4206","impliedFormat":99},{"version":"71663fef9e157f5257a31408ef5aa7fb4774f245de27cd9c618d4f45db01f19a","impliedFormat":99},{"version":"ff8a53256414952562c88aba30a7a2492b86575686c997815b307b1f10bdf784","impliedFormat":99},{"version":"279b59c01e2739e252d3609d4408cd1e67ab977df56f9547923496681f600450","impliedFormat":99},{"version":"d50c4d520094e034e1cf9f4ca262a856b53c67dda3a1959057278317206e5f86","impliedFormat":99},{"version":"39637b145df70dc01000bcffbce1990f85327b9e429d5d0be455e7e1efb235a4","impliedFormat":99},{"version":"afe91c616fc7ef7f8e212efb62c66d8ddefd1b8de2582997f98ac57a4d0a7655","impliedFormat":99},{"version":"7326c8eb2ed751b126ca6b25e9d8e9db67556cb40d5ea485afd7518db9657ba3","impliedFormat":99},{"version":"6ff130b441bf073004bb7fdbf5513fa7bededeb5ee9add10e0e395738eb5f4f7","impliedFormat":99},{"version":"272e5e5ad2298abd3852b0f5bf896ee1284a60bbd9e4e45c961d66fdaf309655","impliedFormat":99},{"version":"02ba9c57dfa46d5673abe376a9c35d03f3f4100b4d9c5f9355e0a62bfe8c16cd","impliedFormat":99},{"version":"35a603a9a038aa340a6cfe26b59a9ec2d984da2c5e63a3c46c40113db6635ed5","impliedFormat":99},{"version":"670c15b05a76b37a6ccc1d2e381c7a1780d1288b50751a037c3809c7d4d433f0","impliedFormat":99},{"version":"fdf5d761568f1bb4a318f8ec9f33e99d1f6fe3d7d1a1f573605f29a03b4bc002","impliedFormat":99},{"version":"ae8bef1c3edc4eceaae9df6bf6a046c52bc4838d2344e2bcfd55b84a5c4714c7","impliedFormat":99},{"version":"358b82dd6de9956c92e1f6aaedc4432bde6a8cf4a9b2cc18e6cff0ba2787cb20","impliedFormat":99},{"version":"15811478ecd27a7b1369df74b7135e8ad8a8352c35dbb13035cd5ab2352fa0a7","impliedFormat":99},{"version":"c0ce7088c30ff91f0ff8d514b7f8dcace90b65c554b8a3027cf864f558c4cd80","impliedFormat":99},{"version":"4b49f46b98cf981e1d9f0e75056c458f0c242ee7661f1da3e6cd8eba7b599bcc","impliedFormat":99},{"version":"d7033cf81e6636c20362085d87152a2478c2529db0fcc1eb3b1c0ef152caa132","impliedFormat":99},{"version":"95417c140dd26defda52f7a4bc43500a0915d931308c85866136f829d7d8c88a","impliedFormat":99},{"version":"c2668c0b8cb8bde3d7e6901aa6eba98963785e887668eebe974e8df49a44cdc5","impliedFormat":99},{"version":"bf028ae34f02d5604c337ad4fcc8541ce14e977c3bcb3e7f43bc396af5721494","impliedFormat":99},{"version":"fa1ef591870037b5eb045e5d01902f9015ac8553622a8c18dce178cda3f981a3","impliedFormat":99},{"version":"8a14c1e4557c91f4ca586e0762f423f699e6b97ee5916f548c68966624ed0a0d","impliedFormat":99},{"version":"ebf000b79f4b3d3f8fe4c1d1dee01b12bb82262750598e382444c2f4c6cb6e13","impliedFormat":99},{"version":"df0211c66f5be54737f0d9f4f9b60261365caa812962343f8d4af61c62ffce56","impliedFormat":99},{"version":"2e6b186c28e363aa0f3144864f74bb0bbcad508f91e4f2dd7b652aa32abed04d","impliedFormat":99},{"version":"0593c7918b3a884cbaae040e5c53c8534d0b7cb2dfbeb923a3b6af15cf597690","impliedFormat":99},{"version":"103d7201704781b7503ddeaf536ae71aaf9005d861e9f9f00ba2d5b7c9131946","impliedFormat":99},{"version":"6d39e1758a21b9b1f7a33aa4068bd2fcf9d83f202be716bf03891f04cd11e607","impliedFormat":99},{"version":"4661c14dd9c9cea4b7ec3577adba9b05c554732cd21e51a9a8d67d55cd923035","impliedFormat":99},{"version":"cb7d54e8e4fb0979f0a528e1a40ee833ecf3f2b40cf9db8df5987576359c9a89","impliedFormat":99},{"version":"b3803db7a72c9399c5285a136497140d15618995742f06081d9c2eb52c607b68","impliedFormat":99},{"version":"2a01ae25af7e6725d8d5489fa2aac7e2c521ad4c072fb58f1c6e3bbddf4d913f","impliedFormat":99},{"version":"720a99ab213d49587ddb67dd0cfe541b0cc1b5f6e53ee916b75319bfec2f3ebd","impliedFormat":99},{"version":"81e828965aa9c2efa7403a7a4a533c51e22b618912a03d035a042eb82d1313f8","impliedFormat":99},{"version":"168489d5da3a1925ee759c4ad8d031581cf887a2d305602b4a658cc23ae44106","impliedFormat":99},{"version":"6be5fca571daf55f2f6f44aa9b1497ebacbf3e03e6708e51733a6d84575754e9","impliedFormat":99},{"version":"a623e124420520aaf6077ebf64567f3bee65a6dc3773a1332f0ed513f7f49ce0","impliedFormat":99},{"version":"4d75c7f4eb68d365da1731fa1fa0170895aac801cf21a292dd0503763994f3b7","impliedFormat":99},{"version":"06fe4e250c71760eae5020c88f85742299af8f53530ad9ab937469d5cccd7255","impliedFormat":99},{"version":"0adabfced9aa978628b37783f1e59aa42249a0b7b3fa7b09312c7b866fa3328a","impliedFormat":99},{"version":"7f2cdc3709d66f6004bd77cf9359b467811dd37cb22c00c8af408ab74375769d","impliedFormat":99},{"version":"adbfbc12814060b647c81470c08f3420d89fc93dd1c45c422a10561e3bb6f13c","impliedFormat":99},{"version":"5f1614f54f178ef74d4b33e5871b7a7dec7d330e5d94b28321dd9d32546dae05","impliedFormat":99},{"version":"9f741273f89ff4d1ca8586715c40eeaeb7784d30a9c8324a1dfd39a06371be97","impliedFormat":99},{"version":"9885942ed0ad172b90b45bb4af647ff107a867529534e991f0a112d900d0f215","impliedFormat":99},{"version":"0c262fca6a1199175283b61213d6c6b0646de8584ba0602722eb95b9ad9dfd1b","impliedFormat":99},{"version":"39c1c529a708d276e7f9009b6620063290c18e4c916fc3093d308f32317208d3","impliedFormat":99},{"version":"f2b5e36f2402b53b809e2f64b8e020c6a7637ebce13b95fdac9c26e1a14d7b74","impliedFormat":99},{"version":"8680096ad4dea123f9c49cb0a5deb1fe7a625c93f1a50c4516610925749c03c7","impliedFormat":99},{"version":"0f619545bd7f204050fbe17ab02fd6133ff926467eceda17b430eff646d72ffe","impliedFormat":99},{"version":"923fa583f765c8c013082e64f937b199f980b1b15c40bc3f583b95530e7d2d33","impliedFormat":99},{"version":"505e2b29b7aff97d5cfa06b644a73af595cbb2ad62514705354111eb4a9d25d4","impliedFormat":99},{"version":"0626be91af9b502263c02d30f54462d3b3d441a317caa2fe6c690ab8bae67897","impliedFormat":99},{"version":"0282f7c04ffba8d50df82954017daa8a653cc71c2fd37ccbb87c64105176ce32","impliedFormat":99},{"version":"32d95fa1add10335cf6d98c640a47ebdae0e542b4b2d8c0a302455933810ff54","impliedFormat":99},{"version":"18da6199a28384e4287fbd4035fb4e3602a144975228b436c40371b3a31fd0a3","impliedFormat":99},{"version":"22ff98edd221e6aed5bc9ef9f6a1f090ecb34a1cc465e5b2f7c6b71789238447","impliedFormat":99},{"version":"bcf15f71abb2654e027aa15e6cc2c6b1a56e2a015a4bda6b1215b780b9cce61e","impliedFormat":99},{"version":"b390b5480af1df51fbb3b9bf2140ca51fb96cd6b577ab53de8afe932c1029951","impliedFormat":99},{"version":"449439716e67042af43d9c9290ed4c49bc46854b77273874f7e7f4860b52bdd2","impliedFormat":99},{"version":"a3eeb212864f2cbe7a7e7946425365b0b1bfa25f93e975f622d760738f72cf5d","impliedFormat":99},{"version":"326be804404a38c9b39ef236e45fc4822c4287a337b5d4d2162b7e5577489ee3","impliedFormat":99},{"version":"46d6be7f5084b138f83944e3550fa72591935fed3679ffff9447e2c4c64c5d82","impliedFormat":99},{"version":"b1a7f35702d8f723354d2d69d8f778ef8ceab7f690ddc7fc7ddeea2524c5075b","impliedFormat":99},{"version":"770e7572cfd883ddb4d296572cc49e4183a9299fbdddadaa3f15d9e1643154a5","impliedFormat":99},{"version":"0a0f7ac30acd0ee10988b7fca9c3829610f98b53f83b7e5c7ade6572fd4e4849","impliedFormat":99},{"version":"153bfb6e514f8d63ad14385ebea4a499d36c0385ee58dae66f9dac2337291d2f","impliedFormat":99},{"version":"5d7fb9ed27493c65389f521ca859506d10a6b7e6bbcbdfffb84ae993918037da","impliedFormat":99},{"version":"5bb4df6cdd57a24e32fbd69fb6adf79c3dcbbc4b802a8f3d893a5aca37d3004e","impliedFormat":99},{"version":"21fa1d6be54130de54330ff4c1437ee0baa70d8dcfff4d1c4a85e558875a8c89","impliedFormat":99},{"version":"46439b4d9e8742c3de2c32b1ef4962af91612d83db8a2d77733a21883cb6d976","impliedFormat":99},{"version":"2813b5d86a9e0a9986104fdf4a2a37cbe868a6e17c5e6e3a1abcdcbe1cfc93b8","impliedFormat":99},{"version":"d9450d1102d1179b0faf3baab898c89596c1078885278f94a1aadc4f70049312","impliedFormat":99},{"version":"0f76b54c6dce2b5f193c87e6caa0cd7289ac10781ae342ee6bb7af0ce36cc32a","impliedFormat":99},{"version":"2867f05c20a2225b4c1c8ffb1b5240d30064527fb7a5b1750bd41a89d6c4c8d7","impliedFormat":99},{"version":"1fc856f4fc93907ee59c01efd8edc6b5d49043e3766cec4bea5fa2c2f2f4edaa","impliedFormat":99},{"version":"b864f5118eb5e45345fbd0656be02c88fdb5358c589ad6b5c66ec19f4f0e71a8","impliedFormat":99},{"version":"aac54061ec7558135b4f676212c89b82c969b069342a42f1d5ba6d30f743cb6e","impliedFormat":99},{"version":"d84163e6c4bab8b697bf1c8b0c0908f404df2384b47da63914679f90c0a56bb9","impliedFormat":99},{"version":"ef519ebcf62f2c37cd54485272e42e21bb966b2c203b3565fd265ffee1054a7a","impliedFormat":99},{"version":"fcd2b3eb9f11f2bae710fa4d7a596dadbf1a465b8585f05db63a28d93079b838","impliedFormat":99},{"version":"1d8ab77f40c29a333fbba3585751b384005ced7d895d2c947f146a95413e9951","impliedFormat":99},{"version":"809337f529d322c198398ab84513d7b4dafedeab24179221b6b2f1dbf0f05065","impliedFormat":99},{"version":"60548d44220d4767d7ab2ad9461d664d9aa71afc9e44e03056ad380ddeaa1d66","impliedFormat":99},{"version":"2187909b7da18743b0dacba6e2390825cd20362ff44068e0d47427a607eb22ab","impliedFormat":99},{"version":"962cfde9b7e05989a758aec98703ed75159c40e212df5cec74381eaff82aba98","impliedFormat":99},{"version":"72ffd8535e8e7025b7969c8461c2cc456d23ff175103df046b4fc55a61a38211","impliedFormat":99},{"version":"c890c291338418ad77c86cdce322a8f750bd9b4cd9a456b6363da72549a7a94c","impliedFormat":99},{"version":"df2768213ab37da8fc9ee6cc9f24f4a32c62e874d28625277b6b12f944d4382a","impliedFormat":99},{"version":"effcca1c88335a4448160c1dc7bfe98663f38a67796ee11a04d7bf9c568c8373","impliedFormat":99},{"version":"cde28c7dfb93f6c7f8afb49e6d3a8f8bb577803c7eaa2ab4f02a722d1453086f","impliedFormat":99},{"version":"b05aee7d890773c66fe01f130ee432755c5e0b0e9857e87deac88beea7a57577","impliedFormat":99},{"version":"071091ebb5656ceaca89be5827e45d3367dd7e5f2b51589f5cee241116f33028","impliedFormat":99},{"version":"6324f22adfec34e153717b6d0f416464b8bed2086a7f301fb187cce198e582bc","impliedFormat":99},{"version":"882daca29017e753f72dc0ed60ced6c7ef8c9455bb865f95ce6728e8a7b405d9","impliedFormat":99},{"version":"85cc6f0801e8718042f3116924db65682fc16ef48165136fe136f39d42cf097e","impliedFormat":99},{"version":"61a82094c65a52eff9ff1986c6082e361b0485bfc9eef3c44341f26fe53d1453","impliedFormat":99},{"version":"3b03dfcac351c5c9e0362f081676d3adde495b3d07a8a29f4642bc261091898f","impliedFormat":99},{"version":"909ac7b41961cd1fef0ac5d31d2765c72cc82b4de3a184154b48bc0247edcdc9","impliedFormat":99},{"version":"20fec64d6a21097265323e83c04be9b68e07c26624ba30f05ce53de89fb01571","impliedFormat":99},{"version":"a6d376714411e99706294f4288fa3310d14aa3e211040580102c99fe8c87262d","impliedFormat":99},{"version":"2646752b05b717bd2900551d822735156de24591a2ac17d2ecbf713b5b13f90f","impliedFormat":99},{"version":"ac0ebd6f1cd412a64edaefafc74d7dfb6ce12d03ed88850025622c3e2fa2821d","impliedFormat":99},{"version":"fab8951025c1480a7956466501e4547fa69afacd8a86b6079e1d1a7bfe7c43d5","impliedFormat":99},{"version":"ddd2c8020e0d3e479bd12e739744dd4eddfd512706ee6fc8b0d65b58e234af38","impliedFormat":99},{"version":"027d9c5e810e507c637cb8022c9d44445bd5e3a3c84c5cf55bb2fe67d3bcfa23","impliedFormat":99},{"version":"3679e5a37ccea0b56e73b6d42a36ddbca0a6fcc4119dcb34e652b0e2cb7189c0","impliedFormat":99},{"version":"9e4b60c3965c575a33156eeed8b5f8d5de378793aa1b976189aab25e6ab48ec5","impliedFormat":99},{"version":"73ed3cefe0c0aeb059cf27bafb51e843918087c843ff457a871ed3a074c9aa5a","impliedFormat":99},{"version":"d35344448dcd8a37ee1e2a467f8e9d26c388b712ee3d24a3aa4900585b5545dd","impliedFormat":99},{"version":"3ef76317f676e81cb18eab1fb7e6a1ac60015a57fd679430ed8d1257a911ff89","impliedFormat":99},{"version":"45eee0eb80e0b7604390ddc34fe455d0fc04603d9e4829549272f48c8942c7b9","impliedFormat":99},{"version":"fabe29b682338fa7914d62f21609e4dcbaee9f93ef91adf6db64de244e8140f9","impliedFormat":99},{"version":"bdf0099507aaae26031e61ece1c443799bd090bc99c6e42baad4cf7e9611afea","impliedFormat":99},{"version":"f24ce130c62b7f407a3cea55bd46eb78282ae1e03a5a980a7bf73628e3a25dea","impliedFormat":99},{"version":"cd6e7a1b92dbfdeb233611cb452d8eed24b949cb029f97bef483a5fa3fc7298f","impliedFormat":99},{"version":"09edc5752759a8d4d1fafcbaaa9fad6bc6f1b3ce1a422262ffbd0644f8cf5f30","impliedFormat":99},{"version":"3cd21ebde7071845559b76ee05ad428c5325f788c1fe22e9de4d81e77631fac1","impliedFormat":99},{"version":"542ae7a098c4b7d9ee35b2ded57786de1210bb12845b23605221030b17e04f55","impliedFormat":99},{"version":"8401ff0d8b9f30a8a341ec7269f234d7152177d6a4131e0600e7bdf489233917","impliedFormat":99},{"version":"66a26a0d221fe38994b72cbae8515a91afd5928e40d2f64107ce6bb8fc81375b","impliedFormat":99},{"version":"50594b654ca71c8e7c09bbf175cd34d5efdab5ac423fcb19fd8288456c0eaddc","impliedFormat":99},{"version":"c18cb17ebe4e2417dd2a31a2a4979cfcc72338fde7c9f32768023139c41ee521","impliedFormat":99},{"version":"28d23dbc2c8d8dd21e9b397ae08f5109cbab3a0599df0248190b0cb396c5990e","impliedFormat":99},{"version":"fedffc2d00ff968dc10e3e076f5cdbc5d7ba1e8bda6158bf0ab4286711e1ac27","impliedFormat":99},{"version":"93212dfc3cd9457c7ebde8ed4500164a895d15b55c9f10aa4ac46ffb455c2499","impliedFormat":99},{"version":"f0ab96c940eb748ddfb6f3b1442393759257e624d92034ca6e9698b41a06b2b7","impliedFormat":99},{"version":"823f37edf2f78ed8fd17a59f0837ffefc4b53cc7a0560bcee95bc58626fb9b6b","impliedFormat":99},{"version":"f6439fe717e31e5d222d7e91c5c1082d280f9bf9209474fd7542c4da31b69d7f","impliedFormat":99},{"version":"49126f1c0cd14332be63c13677cbd1543c09362488732b7cd926c8be1fa5a5b3","impliedFormat":99},{"version":"17fd67f8069c954e24fb5ef02e4c97fb2c7b8e5368982ef018232ed2b16b9e9f","impliedFormat":99},{"version":"45be37950ed5f5d4e489a93c3a278811e6685f423d552c33f1518df9c339b5b6","impliedFormat":99},{"version":"0b8f5565dbf8ac71e1f0e36cdf6af9d5e595bac817b1cb49029a387f77a97f35","impliedFormat":99},{"version":"1abd78da038403165528314141ebc823a6adf229febe585828be06e2888b8b52","impliedFormat":99},{"version":"4b068760205754ede438631a83b74443ca9221c47fb5c1026e488a6ff7154dc3","impliedFormat":99},{"version":"10e52fe23fd22927554a0f6637c2f3814aa50f4b449837e84d7c3849c5ace8bc","impliedFormat":99},{"version":"9a695b27f81d8dcf4f4c1ace9362bbf86da55a561333ec22b028567cb77ad167","impliedFormat":99},{"version":"69fa3b563bb3060113c7d4962a0da9fc7d0c1981c1781bfbd776d06761ba7afa","impliedFormat":99},{"version":"45adca51ef386ec97e2b6adeb30bb5bdde8fcacc9b4e6bd7d36b293acb3f0037","impliedFormat":99},{"version":"edfbb6fe54cdaa69a492cc37a4ba702f9352324cda30260ba2dbacc1bc60bf49","impliedFormat":99},{"version":"1648015d9dca4246be02ed5099672f2fc3d2e1f96c4bf38cf312d52a9a36f821","impliedFormat":99},{"version":"f018e038cb0609449172a9ad1130578c92e65939b70374c0847681ce9eea5a53","impliedFormat":99},{"version":"63ed53d5a157ed6749e873d0a4dca4338b86aefc123a605924e4ecc31fc7d3e0","impliedFormat":99},{"version":"602490794c1c63022ff8f03a43b9e3c9464df0e23b2cbfb9a917fc94f4e1239a","impliedFormat":99},{"version":"790d855ffaab4826fab11919a2424117f1df2263cea00515db5d9da27c712f4a","impliedFormat":99},{"version":"b16a21c46be0065ba851366ed933110e7fcc047cd619859cee6fe6043f57eb5b","impliedFormat":99},{"version":"973bd4ce6836a2b7be4281e63be46a02c73c7cfe0f739ec66fba5f64fee42172","impliedFormat":99},{"version":"c95df61205ddee55c0f36488d45eb36923536d1ee7fd2c123765920d0170b903","impliedFormat":99},{"version":"921bc19e17c9b55643dd6bd629c1821a17b9113ed445720c10fe375513aee2de","impliedFormat":99},{"version":"2a596c7487235cc254faeab8d49e084d61d53ca89e00f86da42a5eac0e702cc5","impliedFormat":99},{"version":"063ca493742dae520916addb5eca31c3e0bf6c58cd0f10f5ed8da071d8cc3e58","impliedFormat":99},{"version":"de562ae720d5251cdb274522070f5f5f89a4ce3862043eed99d9456e63a212d4","impliedFormat":99},{"version":"8bc9b778acf6bf5388bea5499cd5b0f4bd7d4c8ed85928dcab7e77038b1edef5","impliedFormat":99},{"version":"17f91aa1a354b537cf970a819537ce1e7a2b2dafca7d4b1b4d0894995c804b2a","impliedFormat":99},{"version":"048f232a281458a0159f024678c5c8651274885d45f86b0b21ca75f5b5ee6c53","impliedFormat":99},{"version":"1bceba42b5425ed2aa158afe916e36e88ee0de33e2f98b390247cfe045061f12","impliedFormat":99},{"version":"e32e2962a30e289b4da7cdc87eb2a79d0102b9abeb37b302b433554faa32721f","impliedFormat":99},{"version":"dfbf0fe6cdc27050ac69f83fe64bfb387dc40cc653a0b559f3ba5185e1e78472","impliedFormat":99},{"version":"f7bcd657a6dafb80e582d43f4bdd33a83b58376734faa9e20cfa9b6030c7a2d9","impliedFormat":99},{"version":"25357858ef183078a99a52e14622266027cd95e2f8a81fd280d539aed447eac6","impliedFormat":99},{"version":"e3b6ecd2f0ec008a7c2f8471bef625c49277dca2a6173a8cffaf920b2b1bf9a0","impliedFormat":99},{"version":"d446c4afecf40dfb4ba6b32a7ed85b5767c145e1e223d17d11fb6a8df9919d30","impliedFormat":99},{"version":"a2c036eefd32f7c0634e1f8615c4b485341d4f1f6fdca5040e62f943972013d9","impliedFormat":99},{"version":"5f3e074edd23169f519bb36e6acaf261a3a5c33af12a8a7e2794b68255bff5e5","impliedFormat":99},{"version":"fc11706b5365ebd58c9434fd9529a100a3d24165e4b04488608b5d55a77d6472","impliedFormat":99},{"version":"909469ed8952f1951373d3294706b50e94d25389326856fd7591557a88ded44b","impliedFormat":99},{"version":"28c160c147c1227df8f46faf50c70dd8ddf0870604250af9eec7f52302b9acf2","impliedFormat":99},{"version":"27a08c3aabe4b415d824042ad958ad9d3d0160c4d8ee43aa8e4a1aece2b921a9","impliedFormat":99},{"version":"4a802c8b0235155d9e45dcaa4d37e46e0eaaab68ef94eeffda6267963267a272","impliedFormat":99},{"version":"5aa4a3727ffa52d1a593b24d6f6e0fd5d3ce859d8ed9bf2367203343f79c0f26","impliedFormat":99},{"version":"964d27f5310297b03bf276d28ba974f3b7c972b532421108378a00cb97fbfe5d","impliedFormat":99},{"version":"315c0551400e653c864fd689e86cd344e9d39de0e1dec74e73b21c6d3602c775","impliedFormat":99},{"version":"5a9636f37a8cfd40dbd97ba1b222f59f085137d574bb90047e77ee853578bda4","impliedFormat":99},{"version":"8b21ec05d818baeb23822106dc371592d197782c61ad6c867bd00482420dee3a","impliedFormat":99},{"version":"3b515b15b0dfe6f511f280d2a66e6f5ec4518d4bbe43699ddd2ae5803ffc2644","impliedFormat":99},{"version":"5cee626e1c97f9c9f634a90e71286bbd32bc84d4391e06286ef7161dce6f8d74","impliedFormat":99},{"version":"77d5d4fa63a5492d5eb299f89ff6440eed60228e4ae45e4675491f1117f95634","impliedFormat":99},{"version":"9caca9e0d06aa45891b2cefe548acf95759cce8be9ba6288d3473ad9a79cfa9f","impliedFormat":99},{"version":"1601c9e2d2fbc602e8ea3cb856609e5dec0eba0f0b4a4190bd1aca8d7e48fd60","impliedFormat":99},{"version":"874f6ab7330b8e05a42d56afd3f77c1758c2d1027e3dbc607956412d503517e0","impliedFormat":99},{"version":"bbc8bbb97a2d014f591b7ef7f12ebf08f8efbdaff154339b89917e20ce586381","impliedFormat":99},{"version":"9ddf9e6cf065d0114a2e03ad2f24db6e2a0b7e0495fb830189fc2b5ef45943d3","impliedFormat":99},{"version":"34392d1d35edd9eefd7aed8c7300e7bd79a486f93a536c641d6369d9de4b7845","impliedFormat":99},{"version":"6ed5d4d4bf431b0b5f10140a0f9e5c91385edca2cf0ba86d6666d966b327bc93","impliedFormat":99},{"version":"e53d7929979440f295425147feebb4fd29ca12b76fa8eededb805974327afacc","impliedFormat":99},{"version":"6ea1298ca33b3ddea4e9fcdb064e1e6d9b3914a243a016b5450ac9fffecaea4f","impliedFormat":99},{"version":"0a613e62ad948843eda4f5becaf2ab18f6edcfd1de024355c74dc28a08a5a101","impliedFormat":99},{"version":"6c21383843396d5d6d1e9fdb594b7f27b1960eaef2e6111d25819b906307eaa3","impliedFormat":99},{"version":"1c0f7aeb0da8e115b03abf334d85d6a3cae1c5f10fa294d6cec1e68109bd9aaf","impliedFormat":99},{"version":"2ad7bd84e41f4ce032f286215e8c9ef109d0c56940423797ab899a69a55dd002","impliedFormat":99},{"version":"aa38b4ba91620fceb0b7c8052a5ca30c5edbb6369ca6953076f394008391a0d6","impliedFormat":99},{"version":"8c9828ec8c751df4156fcc16f2ca81c496563b63439fab6c930dc0a6bcbf01bc","impliedFormat":99},{"version":"398667a797fbed20361065e368e68f5338e88095338c7ef350ab43b52d82cce6","impliedFormat":99},{"version":"06671d26d8a24d55257e7ab611d40db8cdf9da156d53141c13eddf8d22292b67","impliedFormat":99},{"version":"62acc211029559045b264cd8e14413f3bb6c3d197d73ca9919f28def1c0bfaea","impliedFormat":99},{"version":"49ba58fc3f7ea72d435a44695e789d55e9d3b75688a6a5c94da5b65311e965a1","impliedFormat":99},{"version":"56df491b603cb98cc0feee1786f0ccc67cd7044cbaeba753e4a06be0a3dbcf15","impliedFormat":99},{"version":"e789c4c4f3767a04d475ff841044794cab5cce1b464beec3c4764d27c6336ef3","impliedFormat":99},{"version":"c3bf0b45bee7f76574762527d9e8d2b5ccd6b71bc924a4959f566a0e080cc718","impliedFormat":99},{"version":"17730b23f90142bc9bbba78a8ae3e810424683d70fe6868430172bde298cac7f","impliedFormat":99},{"version":"36b0961fdd22011c8b57ad05992a5e786c10a0f7247226df011c9250dfd7dcac","impliedFormat":99},{"version":"012519723b27b0b7d34316bbaefc82b2bbc7b5f09b29f9420b25e7f7f98e330a","impliedFormat":99},{"version":"26b153c6fd0a13b446ddb463c54b8b89391d66e99a883e0d0a5b2a3bdfd4b4cd","impliedFormat":99},{"version":"cf8e9163683d25c0653dd6383d1cb61527ffc680341cd70f2ef82428d174cdf2","impliedFormat":99},{"version":"6eedd556ff92351a9c0837b704da3249fcaa65b249c360f02537e2468cabfd94","impliedFormat":99},{"version":"d7a754e661206c750697768311597e3d333418f1da4546786b2ca315f884b00f","impliedFormat":99},{"version":"0bc0d9c8e623b0adfc5b0f70e52fdc623fe19cf05d76fa28513c04e0e99f2f18","impliedFormat":99},{"version":"8aff0190bd76f107c999b35145ca236977f5c75a6a0b93b3e25185de07a762f1","impliedFormat":99},{"version":"94e5221e731a382af91ce0ed2dbfd0c6c0cade705ba4708937d2d8e7a81c308f","impliedFormat":99},{"version":"0b861aa0e40121348cd97555739762407c59cf6d4d55ff921d02626a5557f951","impliedFormat":99},{"version":"313da5d83029860437f6c9f4a6bd5589d410d10cb73caa89f465f0b92a092dc1","impliedFormat":99},{"version":"1ca3de4ccaf5b8e3b0281ffeb987418b8797cdd38e6f2efcdac6d53be08d2c70","impliedFormat":99},{"version":"3733f99c5ab34d7f5fc2ae62b2cecd43aae1dfe513f1f198a6f23198ec85a9ab","impliedFormat":99},{"version":"c8f01915efe59d2e3b89262ca71462a2ef267a46d85e64d423b7eba5e56fa806","impliedFormat":99},{"version":"0ea8152fddaebfc02cae376261b581493403faa3315b561b7b28c2c9adb8870f","impliedFormat":99},{"version":"366feb5801697dadf2242184050cedb892219e5ad944d958b9f48c1a21bebd5b","impliedFormat":99},{"version":"18e1dbf93c92d56dd99b4feffb6b5f4bab3942a22f9581fe2fb01b6ef98a3127","impliedFormat":99},{"version":"af66f338f282f6a5b65afba57694316e83ffc482b8e43c2157053d2786f5ccbc","impliedFormat":99},{"version":"e5c1db417653c4d348ff6759b3b669721e2293b6123ccf2ac5aaac850727ebc8","impliedFormat":99},{"version":"3d254354637d43b0c1487e8051c4c362db794ca2b3aed7521aad0c4069531af8","impliedFormat":99},{"version":"a164aa58fd0a25722fc159f19636c34df5fe7e606c306459378005a38aa08621","impliedFormat":99},{"version":"1a341bdbf910ac2f80d1787942c77826b17f4e530cfc238a882a2e2eb0e49e42","impliedFormat":99},{"version":"cf4003a33c1a404f35f5c9854f26586497799eb9a7c8212f734852fbee9dfc2d","impliedFormat":99},{"version":"7c24222292e3d201b8b69b2ce466790a5c0abae9de3cc58f7bbeb0678d9909cd","impliedFormat":99},{"version":"7f06d04b0e1a40fd6541852620024acd484fdd7d7652f92debbd27b9540a662a","impliedFormat":99},{"version":"aa440a5a66fbfed7018a245b23f1200cdcffece6fb06d4f6e3b8e47997eddc02","impliedFormat":99},{"version":"e392dfcd88e18a82ba1195eba06545123e53b56f7d1a646f0d47aa2db930dd18","impliedFormat":99},{"version":"271dfa798f98be4ac49dfd6a28cc486f999f3dd7a9bb6f5f23262c64a65cf50c","impliedFormat":99},{"version":"82afb43c3ca6e0891d0c2c78e8cfb622ac3e69c1505dc73e40b3f5946e5a5de4","impliedFormat":99},{"version":"a45a9e50aa41d0939c42d3e02758b105128e57f9eab790e173090273a8c511a0","impliedFormat":99},{"version":"e3a3604500700c4855a5051e5c49caa41df1ec83b30262c1ccc8e34a1cb1471b","impliedFormat":99},{"version":"b5cd0fcbbedb59e0647a66888abca5ca4507f885ff3b286e0c50b842eaed1e98","impliedFormat":99},{"version":"84226fff98bdf61e7b9ae1f51b39fdf4d909393523764457aa2834d50aa91ec9","impliedFormat":99},{"version":"9fdbe407376b7917e668f04ce8d7c23cc952ca60eff05f055d161121d7ca4d4e","impliedFormat":99},{"version":"586e40af8284984647509e8bc2316ed15eb9fc471a5fdb5a1d7324fd0cf8cbc2","impliedFormat":99},{"version":"78b9ad684d80469986ed95aeefc63c0a897a625d9c30281b60a6c67eee314983","impliedFormat":99},{"version":"99a49d4c4a5ecfe3d02d1c9678741b48144d60db675105428cab22623e805fbf","impliedFormat":99},{"version":"c7b9f04d2e9d9c104e124c812cd95e35f6b9e96208cc7261e3fb8cd6341db30f","impliedFormat":99},{"version":"d59ca233e5c37a734e7ddeb14073c661b571a005b65d291bcc7a70c578bc3483","impliedFormat":99},{"version":"f7e93f9af1676ed9f0a505df3218f62b0cde1e898a9f5020fde159b2bce4ec22","impliedFormat":99},{"version":"99a9e52464f5d435e1291dd0cc343d2650e1aeea2faf8c45da0069b79983b189","impliedFormat":99},{"version":"5b182efb3a0328d1185ffd4b234079624ffc3642ac2bdca05f2d2a39f489cf6a","impliedFormat":99},{"version":"3e051f1ac9abb757eba36a1c653079c05f8e73d47f468d2013e5cf91155edbf9","impliedFormat":99},{"version":"50deb81f9a86d6dbc990a3d2e21ed9e95c7053b4f602cece0eb8a66108db32ca","impliedFormat":99},{"version":"5437c62288e1d84f352d1336b2c791e514164f4b76df2b54b9b80b021d14144e","impliedFormat":99},{"version":"3cfaf2c2f604b6e0c3331d6e10cc12a4c2ba9004edd3edc4f34830c12a00de76","impliedFormat":99},{"version":"1bbffc55483a2466f7ed17d1ada9eb309510ad7ddbda8b9052bc0c4439466c98","impliedFormat":99},{"version":"1637d9794d76d852553adec7dab806e435c1d15742c74b55e7f77c6b3f0b4d99","impliedFormat":99},{"version":"94b752027cd750dfd3114ae4bc354e3136af339314e3c0f910df7fbbcc282193","impliedFormat":99},{"version":"f754f4a9815940934f1b958bbf2d2fd8b1463fd16a2e3cbf01a488867a20d21f","impliedFormat":99},{"version":"4ee3b34d2261742bf67c8b5e5b3bf826391e22edb5ce1336d6a8c431b30dfa87","impliedFormat":99},{"version":"09129fe38056fdcc77ac298e218f9e6175608c088d01154047e0066d38285004","impliedFormat":99},{"version":"32a703d65ddcc443c10d404684ee4078f615c02fe7a2dcbe3cd9c27515359905","impliedFormat":99},{"version":"05facbc54c07763ce59d8ecfb9e7236a2490393c772c99a534fc06d300510a28","impliedFormat":99},{"version":"29fd1f159df8bd374aebb398067eecf5daf749d2d07f736e7c7287dea11f4a33","impliedFormat":99},{"version":"2f38b4fbba29ea39481bfc318cf7a1b42d50a1690465a446650948c20650df5b","impliedFormat":99},{"version":"058107de646aab069d38c35d68161a2d4547c6e78c3a3998338b9768ed1b7022","impliedFormat":99},{"version":"65e05ffb34f0189582043596cd28453d050b8b47d89b466ef61953134f7b46d0","impliedFormat":99},{"version":"e08f74569060491151e951498fa46e391320fe652c887b3a1e387bface179f11","impliedFormat":99},{"version":"25abdc000f2be90eccb17eace14443091a8791d79ec423e4e3d06954b0a3e50d","impliedFormat":99},{"version":"95a479f1804f59bcd6cdc1d097157b402f3b9f56923aab18fe4911817d3008b3","impliedFormat":99},{"version":"93d9b928e4b32600406d7a4c854f009799e805da06573fcf481d5377c1759d60","impliedFormat":99},{"version":"24d8c68f8bc95ddbef68bc3dbc659876fc18a15c34859c4c21e002483c0d22e0","impliedFormat":99},{"version":"c9646244df7474905bdadac34ffabeca395850d5415e13bec0cff6884a3a1bd0","impliedFormat":99},{"version":"91bfd8b9c1596dc378f4a21ae2915dfe7f7b99cc4b48042e91d6f473259a50f6","impliedFormat":99},{"version":"40d8306d908ea3ac5af2e34093c8c589d22481d89e60b7ad6360e4ae418b42f6","impliedFormat":99},{"version":"96aadc567cda6bfe77accf7107137390f03b3f3e2968b06f881beebe8efd81d6","impliedFormat":99},{"version":"1231da3d8a3ab8b836808da1363b33f1064eacdce90f6ce1d8dad2a378f2946e","impliedFormat":99},{"version":"c3edc858676c0fb57372e7fb39a8367636c7cfbd532b9882a722b27bc901d0e9","impliedFormat":99},{"version":"2018b6f0e2061de5d0127ea5ea55594657870b0551d6763c47b5947add29c7ae","impliedFormat":99},{"version":"98feee5573bce5dcfeb11863da1463b0e5e4c088ff76d0010676cc297399aa33","impliedFormat":99},{"version":"9c7e6ee655cd3e2c3db15050e51cf28350e020ce2b5ca030f885165bac4afdfe","impliedFormat":99},{"version":"ea7570613b458b3b741acfb597f2ef75c646fc5b8de31f24f3e11e9ecc3e2626","impliedFormat":99},{"version":"6a8bb162513430ef154922525aea4d9c9e0e300d33b7e6c7dfdc21f1267e285e","impliedFormat":99},{"version":"5e756b34bf673b63c88b52377d9341a86e5b85bc15edcfd25926f18ab7562fca","impliedFormat":99},{"version":"8e978604cbc7a505aea54c40a6a824035d019de1a9ea073a8d89056d6e75d004","impliedFormat":99},{"version":"88a6d094de07d3bedda43a507010a4e496f83f898c8b30d51d1d66147317d389","impliedFormat":99},{"version":"0247e5036245f425410b138acb7eca8eff452e90b60ce15a6d14a79254dca3be","impliedFormat":99},{"version":"a917a6578721f88084be79236f3c3c6c8d0626d27b9d29eb8c7b40336d24a665","impliedFormat":99},{"version":"7d4d7cda987e76d83992b3099170e72f1238abde39b07086f2ce1c047fa5305c","impliedFormat":99},{"version":"7a6e19fb0b63f9a7f010e254376097051865a6970c698e0c4700840ffd88e646","impliedFormat":99},{"version":"c1214f047a9cc3b256a6babdaa062b1cc112f9e11c3276a585607b4c034177bf","impliedFormat":99},{"version":"645cd29e13ee8b379c92b7e016f80f020d596c0e329122fb89bbe857596b7fb5","impliedFormat":99},{"version":"85aa829c49948a7745bc9fe8349bdb83bba19763c3a8d6b30129498714c4a81e","impliedFormat":99},{"version":"0883b415bc4b8364115b3a74c2c5d14bb6852a1a9810d051ff426c51b7865e3f","impliedFormat":99},{"version":"57bcaba92379c398572ceea17e80cb9be80b0915bf2022fe99f360875e3ebbb6","impliedFormat":99},{"version":"e19c31cd2a5d8e14a56d40d9f8bb4819f70ee3290c71fef47d753cedbe1b8403","impliedFormat":99},{"version":"7867d743e0710c8233955855dd1c8143c76bd169d31e1b81835dd849d53f78e9","impliedFormat":99},{"version":"d953795eecf54315d23cf13cfef3aaabd521a305b71a67d5939487d9f8f781d9","impliedFormat":99},{"version":"8632c3b1a88dea3b5c07133a969658ade0bc8b0add4a160ecd12b2e8ff73f147","impliedFormat":99},{"version":"bbb8f4a3cd8286ed8d4615d7f6c0cbf4753c761e1d3ec18a20a161d276b6f617","impliedFormat":99},{"version":"9643da030e1fa23447dacfec5a005dc51f0f7f651dfb0c063d4195eefc8a098c","impliedFormat":99},{"version":"f78f0984c1045c759eca17c87d9892519813d7ccb07cb9f10bab6239274ab3bd","impliedFormat":99},{"version":"8db95a03e8ba40f5fd72926a7df034c42c083b264fdf948382eb2281cc3aaa81","impliedFormat":99},{"version":"a004f52364ed07ec22dfdd34041dce156046b1fd1d9170ca99748e7ebfca1b9b","impliedFormat":99},{"version":"dbff933905f07dbe803238031f124892e8ada74efd4b284287bfc75d2409e3e9","impliedFormat":99},{"version":"0941bf71e7631df878e29025c83b35ecf2b4fc730f61b008590fb0f937aa63a4","impliedFormat":99},{"version":"3e8837d71c58e3ddce51b55e29ad7691eb15aed51dffec5fb7bc968898abf6b5","impliedFormat":99},{"version":"6e58caca3025448230faecb5efda0951424ba15b071ba13b00f3e7f1cc6bdd29","impliedFormat":99},{"version":"b3a3f55260b2e4384b00a7bfc5720e9866074ac18f715f3e505c7593aabce3f7","impliedFormat":99},{"version":"3f8c206a53bb5a8370f094ff77ea9e95ee8c3661e248a1b9c0334bb71b641907","impliedFormat":99},{"version":"99853f693260e3d76256576888c7cdc5515e50e399fd3111afc514f0ec9e16a4","impliedFormat":99},{"version":"480cc4f99dfc33fbe523f155018e66bed504bfce061031bc04f5c47b7dd2ce05","impliedFormat":99},{"version":"bb0cbdbfe7201cc8dbe28f10d4577316d63a633b746f9e9ea9ed16a0400a0347","impliedFormat":99},{"version":"6bc725824b4ba929f44f3973aba69c3bf4c9b8c667d6c5bd257524584bcf6112","impliedFormat":99},{"version":"1f2a20e400e1df467ced60364324445d04e36184a2fb9bd39f32f85058d72b50","impliedFormat":99},{"version":"83c5f92d12a9677cb376404aeeb89f910efa2ceec8fb200bbfe59c508a5c86a0","impliedFormat":99},{"version":"91236eb27c0c580c803b9581bddfe4dd45623093b8638c8cf39148d193b185dc","impliedFormat":99},{"version":"ba41d9506bcc55e2043d0a9290c38a4c1fb4ef82fb2562fc2039d55ee0a80a72","impliedFormat":99},{"version":"214c9af465ad68b1750670de18e62b8f3e7749257aae5ab0d7170b994e9c7e5e","impliedFormat":99},{"version":"62e8d8503a51a24c9f0947d88edb5837357f4149c5103fb304bd31f8b5d4f4f9","impliedFormat":99},{"version":"5dad46254873e5e5c4b8b80f6776dac8bcc96b059cf6ceafd9be54d24faa3dc8","impliedFormat":99},{"version":"88ece1d1c523d9eaa48c8c3e0c4cd359e9df8a542ba6eaab5dd3c523d5fa9826","impliedFormat":99},{"version":"109ad0355918e4fad1bb9d849f8b72a6894b83d20ca6ad72e73124fa380a6c64","impliedFormat":99},{"version":"70b68b3ac7d6b03ae36e80766d62a666c407f455476283ae6ff219e49f0eaee6","impliedFormat":99},{"version":"bb72571132bf5160ef9af164b82f13fc74c332d11c3325e4953ede73627bda0b","impliedFormat":99},{"version":"e4101333a36a93829170c6e0c66ed47c648571502b484d22e4629317197a6bc3","impliedFormat":99},{"version":"f0e5a05eebeae29b22dd7b3747fdd6503bcf7ad42df8ccb0a8129e98ea112408","impliedFormat":99},{"version":"658adb3d7d4017bf38ba1d1eded9dd36c5be121eaeaf469abc11c2494c13e12b","impliedFormat":99},{"version":"f0c39b43f5d2619c66118d2441976b4f1fcd100e55be80bd55d8279b2776a082","impliedFormat":99},{"version":"1c116a992389ab12f4612dd3cc9b4d888eef454eed3794c1f515cd76d3b17cab","impliedFormat":99},{"version":"0d9ba4647605be41ae36c09439b623c174e7227c8943d15b1aad049c5932c2f6","impliedFormat":99},{"version":"ea8f99323ac76484ca97cddd57ed916dacd4ff2805c446a69da9e728ca8c9b43","impliedFormat":99},{"version":"18e80e1593efac104d1fdf3fe5c02023be9f3dce93ce3a95df75ce1c17b98332","impliedFormat":99},{"version":"68a5340b5cb05677b52fe5032e5b8ffaa3d02a98baa991ee161e318e17d0e74d","impliedFormat":99},{"version":"7a1457e43136d0a005af395929abb59ce7ea3712b1b4091040fe700c4386ff6c","impliedFormat":99},{"version":"8aafd0a8486f554718b22d02cb662b9964fd6b6346ac7c46573182ed3102e9b0","impliedFormat":99},{"version":"ab0f538caa8913de60351b4d993393ba30d485461dfed416b11fce206820974a","impliedFormat":99},{"version":"7d703f987cf71538d9b4e0388c4744fe91db9b4bf5fc0b5ec2120e523ff036ec","impliedFormat":99},{"version":"779e6f8af772f3f63982b2d907c662ca37acb84e7ce1fad4de9c8417142504d1","impliedFormat":99},{"version":"e1ad3c3b80b5bf0ba59c3b33878afb16a906c09840f8d1b072af14e587360f6c","impliedFormat":99},{"version":"51cda3a044b658d58c693647e19ecf16c29f5ae67caa0f1c619c7c76b93f1af3","impliedFormat":99},{"version":"e6b27342fc513672b2fcc831ed5d48627817ae0b26b3a487643b61785c52cecf","impliedFormat":99},{"version":"582d84dd785b99a7180701aad4fdfbe3fcb26cc07fc693032361070ed0369759","impliedFormat":99},{"version":"eb5888375a2ecdd0d3502cec0f77e1e42eeb2169c76b79b61add3ea0bc95ba4a","impliedFormat":99},{"version":"ca5f212e869dedebaa7bf73bea3c6e8ee886ba2f0601d7e789ce38e2db189600","impliedFormat":99},{"version":"d21865e1111bd2be2ee973598343a324713e31f1aa2e6184286e1a8f077366fb","impliedFormat":99},{"version":"3df99ade0e19a8fe6c989688eb5743baf8b02b4cb678018dbdcdabe00fc42879","impliedFormat":99},{"version":"785eab4c4a6166680180ba4e9bf06e6c205862a3c83323fcdeac61e6a76adf8b","impliedFormat":99},{"version":"c1686f9d62d0b94c43faf794bffd8cc24ac1475fc6d536aec3b4480128660322","impliedFormat":99},{"version":"3e050baa6d458a74a769e3fb50ff1051723dcfccfa8a31b7ac9ed0c990d5dbd6","impliedFormat":99},{"version":"bbc4cd67ab91a186d1a4aa1f65a7971ac31bc2d8c63fd2bfdf02e031a85be7f3","impliedFormat":99},{"version":"6c09c2fd7f6c0512ce2ad6e17a0d9f9203f47afd3b83742d4cb4db2d3ea821d2","impliedFormat":99},{"version":"16e3734500cb5fbe0cf27f4b61cafd7954db45c3facff516d8819bd63482a8f9","impliedFormat":99},{"version":"352036160d597fb56e03591325761d86221f6b4facdb0c7df4b790c4891f4f18","impliedFormat":99},{"version":"f93375427ab3f9a598bc14d9bef5a2ebf68c4755bcf3a68b794e131dba637d8f","impliedFormat":99},{"version":"798b8feb569f4b4c521cbcdb9651d0fe799ffad794284dce8a4e0d9dbaea95ca","impliedFormat":99},{"version":"3deb173189f9a8232ce9fc11ce610c27435f5e6b86e507f9c04d5fa275974101","impliedFormat":99},{"version":"de8156baa4157a698a1bda8e6b4ba62b8bef4972808f7450698017219f539475","impliedFormat":99},{"version":"65c089d5dadc1d26c81c374f00a8e11b7178900040dd7db065d8cdc405c65e15","impliedFormat":99},{"version":"66003617c7bd48b01253f0f0ae9218f15adbfd66a04a74d04235f28f9ffb830d","impliedFormat":99},{"version":"8733c82576758e483eaa077a95d1b2d1715353ee0d8f148d06a5f979fb71f22d","impliedFormat":99},{"version":"0dd5e215565f19d57d88f9c32cd365fd451207bdef48519c47b14dd4ddb301df","impliedFormat":99},{"version":"8b86136891f48d0e82f8fdcd623268b036b2a8ddc22241a1202576792af0b63c","impliedFormat":99},{"version":"1a080e2c42f4a16b339bc45067ecce29ce25040cb5db37f5f3f19413aca07e09","impliedFormat":99},{"version":"55f8158fc3e7d4ba4ab62820d32f236a5723af9c721e63879cb9c184292a718f","impliedFormat":99},{"version":"170f5fbca91c30969467442f813678c86d2d38d10aa3ef8e5fb388bab833fba9","impliedFormat":99},{"version":"224347dccdcf08e3e862699dc9e35f6ae1f9388f56e14db2357bb79b75c1c814","impliedFormat":99},{"version":"c6426d9201d8e27551ba9a0b5f4d9103dbc2b2057801a621098116a042b03b92","impliedFormat":99},{"version":"af11c67bbdf99195a89aa5233d8641f6e1d2f7ff712e941c773e16f602e7f96e","impliedFormat":99},{"version":"1dcc9bb3542020790409937565639ac7c3bb8b239ff49c65ee20d5244ced1fce","impliedFormat":99},{"version":"64fbdfda1827a333790a0cbcd36e40bbdab461df37fccbf8a2c12eb0a6450b19","impliedFormat":99},{"version":"9f40ebfa8a6cca16447d13bc9265e7b463b1d4e7265336e287d2ed2867af60b9","impliedFormat":99},{"version":"cd77f74ca7acac465b9b6ffc244a574cafbf16ef35bbc834c98314d1603f6b52","impliedFormat":99},{"version":"6a3fb20abadf78d08de29baaea62889070d505f35ff5140642e8074db0f5b7e9","impliedFormat":99},{"version":"75b2e385d7de8d6d714ff9aa6635bc494a6f90a6f33d2c5c7710bc71ac19d9d1","impliedFormat":99},{"version":"f5e7ffabb3a1e6d3c0c5039912c8a4ea8d753722c9a1e602911c100b7ec155ac","impliedFormat":99},{"version":"09757c18f7d7af16d89b75b643ea4b3a0e195d67a63c03755a07dae726e1148c","impliedFormat":99},{"version":"3b2b48dbb70d48112b34b4a73280990bdf75147d75659cd5280e846513abc75e","impliedFormat":99},{"version":"e6f905bb9525a266c16e4350cc4d4020689a588f2eb6d6b64fcea6d037a05977","impliedFormat":99},{"version":"4a988db19eb645816e40e48b945226f76de53f1299131003aabd37a013c22590","impliedFormat":99},{"version":"9b7dce8b944c33e20355a272f156582fe0198bcfa395d7082e6a66eefbe9ce57","impliedFormat":99},{"version":"13aa72dbe072780b3f936dea5d1c3094af3b6c186c24bb30d404540ec447b3d5","impliedFormat":99},{"version":"a280303c82a34689c9cc178068e6c012c6f582f80cbb6a75530bf552313090fe","impliedFormat":99},{"version":"715ae11614ac2209e2b5f3efb19b270603f8dbf20db559b76b02a47d6f3fd77d","impliedFormat":99},{"version":"d337b0c28c6f35442c1f88ff8071bcf9ef2ff381aa37fc30d323c6ac25e23708","impliedFormat":99},{"version":"425e7209aad5daaeeb4df06a718f8f177db5f0322c5fa94573ec739aa7f02deb","impliedFormat":99},{"version":"cab30bc9301ed991270375c5a0a011c897307e0e2158578e96f93b497121a3c5","impliedFormat":99},{"version":"00e136dc2bf5a5dd51fa6c5e9c02a7aff6ce4bd111e74b0f0d59c87494af51dc","impliedFormat":99},{"version":"4933c98fcc77ebbf6166152f65916cfbfd8f494bd14e6d8eca45ee231f1f2926","impliedFormat":99},{"version":"55b7f086a4ce7f130df29adbac36981bd64051263686af3271c604cd9591f62a","impliedFormat":99},{"version":"7d8fa714edcff481c66fcbd1c29b0856edb5591d085096c98613e9b701f55676","impliedFormat":99},{"version":"879e9c1b88c533389580e7178497ea55a36c95876fe1d94c6bf73883b6d29fdf","impliedFormat":99},{"version":"7c22b12c9c17dcc2317d52b83d8662c538fb337adc8c1ce69ebcd8f8394e2064","impliedFormat":99},{"version":"199e6d1a80d19524ea70d143488d72ef5e8ffcf6bbfe82dbed7f03d603948d18","impliedFormat":99},{"version":"691ca73f83cfe3262081875b1096744b1314860e6e216de3d242b4c76f8c1972","impliedFormat":99},{"version":"e43105a7b869d2963215dbbad1da3ba79108c3d5361df4e11d6c1bd967890bd4","impliedFormat":99},{"version":"c3d4d0c0dc57d5fed5a7af5ec433a3fac319c640aeee9ebd648066535cf9ff51","impliedFormat":99},{"version":"6c7d9793b68ed844870b77a5d4e6c92dc5d3b10d3c09007bb54b4baae99eab62","impliedFormat":99},{"version":"98a99b89bfbc2d9b7c3df85ee85f5fee6e9eb7309dc0e8b08b77b34ccb85957e","impliedFormat":99},{"version":"026567c73d9d23ce133aa2f27da3ee815b724f52548bf5579746f41c60723fcf","impliedFormat":99},{"version":"df6f1df7a3ae263e071615559e198d94c6fec45d33140fc3403eda6f57c5f8e0","impliedFormat":99},{"version":"5208ac9b4aead07702be291e7b6b2aa5936ed9ab8b21de6227cf351292246f6a","impliedFormat":99},{"version":"8d04cf9b77d2bff28760b0126105d3d5a34a1a1e8cc2b4966a9e1859bc55ba71","impliedFormat":99},{"version":"e7925d61f7792610f43a39d58dd58b804bf43d6071a09439d44063e35e5ac244","impliedFormat":99},{"version":"4768ed15015abc1826477096caf3c770ff266bee8136ebec5cebb929d6b9cdaf","impliedFormat":99},{"version":"7ab6049d6f98dba00f3e379c0bb149eae6d2bd3f8ead7de9238bdf65bfd5d791","impliedFormat":99},{"version":"b50f3a41e80fe4e8218a7f6c70257ccac4c3f360d318da72779ae1dcd0e787c5","impliedFormat":99},{"version":"a7b7342414754d0fc6a9560c08e5add2eaccb9570cd39c53a5155f368e695f14","impliedFormat":99},{"version":"e574749d4ad0f5b989632484891d00a6ef6104b3b014e9eb9bfd1f3c811b4a3c","impliedFormat":99},{"version":"e5352486d0919edaf6b57caf289859980db09607e225f70d21ac9b06ac1ef04e","impliedFormat":99},{"version":"23c0a2c86cdcfcbdfc95f11c89611f04cce0f593b5140a2e64d20556af3f159b","impliedFormat":99},{"version":"3cda756ee1dfe94744ff3eb6a11b7e5d41f502d0f8c46aae5338bc82cb929602","impliedFormat":99},{"version":"5a2db3c589431fd671ed605ecf0725decd9acf0e239966bd6d91eb2cb115a0ef","impliedFormat":99},{"version":"eb63ec99b754ca1947a7fbae5d7f461f8179f647bd0f665f6008443b7fc8fa14","impliedFormat":99},{"version":"5d4e4b5cb1a17707244dc479b464e537237ea4139b4235d703ea83a4a9287840","impliedFormat":99},{"version":"97bf370e78bc695d758542bbd2ecde489a61db5d678c07c85bffa91459343c37","impliedFormat":99},{"version":"e7789e4369b2c26d9a8a1150141b146932565ead086e2b4e32ef145325a8894f","impliedFormat":99},{"version":"0cd7b61de09c45306ffca7ca80fe784e56623cbfc99e1fa4aeee7ec924353d47","impliedFormat":99},{"version":"9862a6e0ad83a3b3faa226fc86fb1940b3a9183c0c29f5bddaa9b2c5cb813a05","impliedFormat":99},{"version":"d85d1d741211343158b4f64b4fc7d0ab347a643a3bb8f5a7c80fee1cd71ddb91","impliedFormat":99},{"version":"b30ca3d58261de8bfe7b599e9806d710700b58d7f7c7c1ebec13838bb1c36d76","impliedFormat":99},{"version":"19c817e65f0d10c305c29e01d8fb6defa09291fd67bd13034223cfcccc14f8fd","impliedFormat":99},{"version":"d72ed5882015b367986f5ec7b1cd58bb50bd1c51e941ff06729b67ab66d4342e","impliedFormat":99},{"version":"8e395378f1ac56cf2bcc7a2533cfccd2f431e45b91340418148c99e16eb58c6f","impliedFormat":99},{"version":"cbc884601dbfb8d72c8f8877a11179ab41a9bfa0b8c8b345184b835d09a29354","impliedFormat":99},{"version":"b3be68eab5f7e270ac467749dd01ca3203c572cfc511da099e028a930e933416","impliedFormat":99},{"version":"ff1f6fda8fed4820f48b922caecbcb6f74b81ef984908ca213d09463f9f7f937","impliedFormat":99},{"version":"e8c989cbc67a55209e74d695ff91a888e229d67f0cbbd8b49544cc07382c877e","impliedFormat":99},{"version":"838930a4a3fb335a78cb1d3c4d5b825ee7ef1a7ee2136315ba38346aae1683a1","impliedFormat":99},{"version":"883985e0a6918e642e1f416c6afd0d0e6e8033f3b29ea5a49cd808c88874b7cd","impliedFormat":99},{"version":"f126c52fb0b87a45252fb6397c2326e3f8c3947d01370c2b4d9b1d4d9196f046","impliedFormat":99},{"version":"99e693a698fe25a9924783102421e87d02ed7204369d0c499b207f4fe872f168","impliedFormat":99},{"version":"a70e24984ec50f9c0e02dad3f4b5bac149f10165420d266ba199e50fe45662cc","impliedFormat":99},{"version":"633e63e5fee829801ddef23112e30073ea34d9caf5f464d478fddee93e627475","impliedFormat":99},{"version":"0bba6e8d47afded79ee770ec90c46fec57cbdf69767306b86aa9b32e6f4dab11","impliedFormat":99},{"version":"66b7c3956758bcbad1eb7c0d4fb0d779323a825b8adccdf35ea7946eab476b20","impliedFormat":99},{"version":"16a1a5d4825dbf2991c5a8a2090dd84d8290b19cf541e93a7c4995b8037a627a","impliedFormat":99},{"version":"35f172c558295a38b28a149069d619f658efbc69ba9b672e6c853a489ba23457","impliedFormat":99},{"version":"3b2112cd844c32dbf3dfdfdf05e14b396f46ba6e61bb15d82374cc8aea2f272d","impliedFormat":99},{"version":"17e59b4b305bd2ad32bfaf62350c305bb45fbacfe5a4b0547369259d17775e40","impliedFormat":99},{"version":"c11ab5f4ec79d50b9741fe66527fd19c09cddde5efc4cd39b5a60df69c38b604","impliedFormat":99},{"version":"5530eedb6ac3365bfe20f117de0db075971955dd763eed8b9757ecb5b5a3e57b","impliedFormat":99},{"version":"fe847d09c50c98d93de932ab711c1569cccf7100c0c7ea59e4feaadde30ea048","impliedFormat":99},{"version":"a691ed743148b3133ff53977947c447c5951382aa75ff41926502195592854c3","impliedFormat":99},{"version":"00387baba842e4e7cf442edb72abe67bf2dc4c48b02a44175ec2d82a88847e88","impliedFormat":99},{"version":"b9f3a62b5a7b9cc2308ff957f2f9400431a484d44dd02b7518abcb08b387528c","impliedFormat":99},{"version":"1fd1e1b625fe20a8a1a133bde76da9140312d4fd0da1c18b27d23002d4d62243","impliedFormat":99},{"version":"38d0d481e741ca7daa25fd987f3443ffe351cd0a9c0b4bacd5e008c51f24850a","impliedFormat":99},{"version":"c07fe65c3f45091dba97d40b4ac07c390ff625a0dd92295d76051f0998618d80","impliedFormat":99},{"version":"b27a05e307131d13c3ed496e07dea2b79331aa1ac9571524fbdf16cfb3ebb1f5","impliedFormat":99},{"version":"0dc1ba6bb0e4fd00bae62403c87047bc5be4ab5943ebfacd5348ecf74c9b0328","impliedFormat":99},{"version":"ef60c0d7cd61055e5f06a6b76cdfdfe317460d5ce787efb48f35b3618bb68608","impliedFormat":99},{"version":"8ec7a6d53714b3c8d7a5e1eeb36108ea511cfc7fa4d7dd08229dc906568548ea","impliedFormat":99},{"version":"f84ef63eb5e13857e20ead0977940fbc9cb65da0e3705c04f0f4fc4326937c1d","impliedFormat":99},{"version":"827f0625ef8bbfa8997f91b8832626a4be0742fe3c9c6309cdca802c6edda8bc","impliedFormat":99},{"version":"1a535ae8a6cad0882e7c7189b21f2986dcfd5c2efd9950b5b04cae15179b27ce","impliedFormat":99},{"version":"47d40b96045ac0f52537cc2076f9ec1404fc444861191043c348d8afa0c6bd73","impliedFormat":99},{"version":"58b86a43ef8dd9649a6b67dbeb512f01f934ad78518ac89683927afed03b1de3","impliedFormat":99},{"version":"47c563c296e93315d38204ff70b2a3cd6788fdab9ba388791fa1c82987794d42","impliedFormat":99},{"version":"69ba23cb263a0916bc35457477b88e05eb35809c58a17449974d174a2aac6059","impliedFormat":99},{"version":"b1e445ab4f7d5c6d5094c3d6560539f641987a9acf82a0784b320e01c803af90","impliedFormat":99},{"version":"9e4c32ab790364ad67b078d569d273225d28a3cc5ee1b683e096fafdd0372745","impliedFormat":99},{"version":"3b9cf1e4cae113f06b84bcdb48dfd99a04b8708e4e4f48712ab94b7e1d11c884","impliedFormat":99},{"version":"52fd16e0b3f6582c3b545957606e1e82e8e7f666745a2c0a077eb5b534246109","impliedFormat":99},{"version":"28782dad7cc918112ee88ba23ac97f11a56c7e8cdaffa0978ca83751ba49b741","impliedFormat":99},{"version":"d707632c2500c4d4c503dcff2794e15c9102624ce70c9ad1662e396169290a46","impliedFormat":99},{"version":"798d2a4e66eb3726f8e07b6d94341a008ee974b8f86b8b98550f79b3b0e43390","impliedFormat":99},{"version":"6e36459dc4168e7f2c37a9f9e0971c3aa8840e753519781d1410c659c1fc525f","impliedFormat":99},{"version":"686a3bb7e54b8b622ec5bedaf1ec597633a9e75dba1261e4017e2e8e8ae47314","impliedFormat":99},{"version":"220ffd58c0b38bddd8d79d16b48aa3c71be8a0e8d01d45ad65396b453f6f5ad8","impliedFormat":99},{"version":"9fb07cf95a99e80c1c0462eadfb2b5d9434dd20817ef143ff67def7742b9760a","impliedFormat":99},{"version":"c70bfcb0d36b6e0d59a413e23016e402ad1d5afe2e5a74c1da3b52d271e4d4b6","impliedFormat":99},{"version":"55712f6dbd9fe2bccb9cad293fa37c294dd8bd9067566bcfab17e5596aa77bca","impliedFormat":99},{"version":"50f6da711079ff42be35995fa2dd810a629acee19248c4476ffbe1bdb324b7d0","impliedFormat":99},{"version":"be3455c8c848814f4b4f8602a1088411fed507bdff9a6cc9ffd176b9092ac401","impliedFormat":99},{"version":"16cec8c852d6b1cc27c107593c3087c901ce3e1f949b3560729d9702e26da2a5","impliedFormat":99},{"version":"1f0351b420afd408a267f3c3f578c788245d29d71da0809221f2f9b3dce475d8","impliedFormat":99},{"version":"b79b1fe1036d51b099e6ac6b3fbfb4eb36562c783e473e40c6cd2ec65991ecb5","impliedFormat":99},{"version":"b6964ec91330fd27e71660b777624e9342a2919c08655a7a65833324dc9797cf","impliedFormat":99},{"version":"5bb8a642d5b1f469421f30108f24d5443a83b2d59a385191e93ca650a9dede54","impliedFormat":99},{"version":"b76f9204bb26386bedaa9e14a23b3d9b1c6fae28c86f7fc484df7e5b74ae3844","impliedFormat":99},{"version":"8c1eea73430f19085f52a279f92371409f9c3c1f1cbc6187e474d0ff158b6b98","impliedFormat":99},{"version":"b288dcb1d03d232ba58b61e94124feafceb0d411013f603ddb2068702e5a13f8","impliedFormat":99},{"version":"a9ef5666ecbff227e17ce51d833b7c8822189565799aa860a60efa8a4f02ff00","impliedFormat":99},{"version":"29670032e04e02ef8a2172d3d637ef989b2896718852dc0d6735d47b1d8dbea7","impliedFormat":99},{"version":"c6308f587ff90713ac3832efae5b7aac09d6b067c2bdfac9637583a668d470c5","impliedFormat":99},{"version":"45c053bd25f8463d059c805283437436b640af4a28f9e10811bf1cd122d03790","impliedFormat":99},{"version":"bd4186ec52ee4bb1e07d27a1534eaad966bc9ba6cd65a58a63ff6a45632348b2","impliedFormat":99},{"version":"742c77f3420abf97cf51c552d99d0d935808b33c612e530ab54d115c513692e2","impliedFormat":99},{"version":"1b9af93c3bb53108e109446a0127b8dd6b9ff7c5096319fb6c4d63b9dbc8ec75","impliedFormat":99},{"version":"5cd660965157b9211c61f131ecd876143129cdd528e49099fc4610da0e7c04d8","impliedFormat":99},{"version":"a5664583622519fd735a5b89bde8040e93d11015e1559270008ec3722f546188","impliedFormat":99},{"version":"290d2e6293e262efd3aee13536268f8e96f31794800a1b68e392f27cdad9bc13","impliedFormat":99},{"version":"6c5e7e9f0c8361569abb01b323d939288ab7d2a16ec74962dd0981496a3e7ba0","impliedFormat":99},{"version":"386cf4e0b737544dfd42761ebb6159bc987b0747faff23af0b73b9003e00ee27","impliedFormat":99},{"version":"d7c7b4ae623b52e885e1aa716cbc22bad409d36b7204dec4004ed9c55133933d","impliedFormat":99},{"version":"dd0645217ad10dc8360c7d83bd6c6f36750b7df44c48116c883024dc8e125329","impliedFormat":99},{"version":"516294ca25dddf7d44af05033e856e5bae0fa1cd7a9bf255e601fb77b5b9f597","impliedFormat":99},{"version":"8e4735e0c080d09cc800cd41f646de9d759167292ef7cbd76a0eb57f8c4a00d2","impliedFormat":99},{"version":"0cdf249a81c98a3c6d79dbd0c1a65ff377d6879658aa3aeb8704a1b2158f2230","impliedFormat":99},{"version":"fcac8bc31559ac91ae0a65ffbf43779dae8a9c585f0e3c188fee07eba5444ca2","impliedFormat":99},{"version":"b37597091430552997625ea0f386da6b0ad725704274564c17282a06d77eb585","impliedFormat":99},{"version":"3aed368622022abff04f6298dcb1236b389a26af3a380ee8f06256850a3d99f3","impliedFormat":99},{"version":"be4068251ea5a2a50fb0e075ebd0b2cfc773bbf3f974b97b110011736b50efee","impliedFormat":99},{"version":"9223c5f0109bcc0868650998613d670c302abebde0cd2fc806e42a4b0bb5c9e8","impliedFormat":99},{"version":"0571ff9f961401db433c7c248b16bb2bfe65c0f09dc68543745c16f2176bc8ff","impliedFormat":99},{"version":"05626dcdef717ded5ea6fec10e3c6a6c7acabe92bbf36573470212d5d858ae62","impliedFormat":99},{"version":"64b324dd74b6cd0073181992870983522182d33cf0b9b3c1b6d503e4ba854b79","impliedFormat":99},{"version":"9e6970fc8d80d18a58c0189f4900317554e328e624da3bc78a954ea08b166599","impliedFormat":99},{"version":"cc1dc46db20468ef6189aa7cf4a8f4240897709753b2895e5400d20b6e4cd554","impliedFormat":99},{"version":"30986d936b4fb9b78344bba08e72ce06e731d10862061c2848b019d74ddb7eee","impliedFormat":99},{"version":"12dba10fdfe2dafe7f69c960da3f694723d82f2856ccade9926f5d04ce16e975","impliedFormat":99},{"version":"06a1c3af923cd00e69142a78418edb82c211cba3e72a91be52ae12bd845550d7","impliedFormat":99},{"version":"13405921db76cd2ca52f50b8dc8819884bb0fdbec7feaa8b0e5256fff635b007","impliedFormat":99},{"version":"474fe5758f25fa31f93293163386bc120cb3a859628146216c27d20f9fb4e11b","impliedFormat":99},{"version":"797a5c56e7b68fa7ddaa736ea0f84903a0280b3e24f7edc81dc65d59397c1dbc","impliedFormat":99},{"version":"13144ada8c1a653103674ae05cc08441800d8db5ba8efa624158528eede15884","impliedFormat":99},{"version":"42bbc0317d36df458e7e5cac3fe51a5747aefab3abc5cb3f238a944d5e32a91f","impliedFormat":99},{"version":"678842cc3c2c69a8c2fae8353656a1e5337828124b7b53ddf41689876b70c57d","impliedFormat":99},{"version":"f0e3459d79ea7a69f7a334151c38ca830c4541a030204e44b680bb47cb357bdf","impliedFormat":99},{"version":"9ab46babfc3d98ac7a31ad70b4212562ad36311d9fa235871dada95e3379fd09","impliedFormat":99},{"version":"92e6d6a927ad3d48d91b7fd3f31dda6e078cf03880e0bbe9cf56abb5919efab7","impliedFormat":99},{"version":"5b61720af63c370f28be29a1e6b60eb3d3cb6df1c169e467ef4241f9abe68272","impliedFormat":99},{"version":"f622b88d462a91a84c41747a8c097347cb08cdc26250ee5c86bec180d4587ed1","impliedFormat":99},{"version":"b9688f18464d0182fce970cf89c623191997178076478d5bf5604724a73dbcf4","impliedFormat":99},{"version":"811068496ebe65d79774a99796223a0ba3efe04be587f6ad9fb2c71865d5fd75","impliedFormat":99},{"version":"1a42ecff9998af3ec2d7465bedcf10aa76420768fd31143fcf0808dc7a3630dc","impliedFormat":99},{"version":"0ff2355b4074c3eaee36f435bcbbfdbcba07ddfb4aee5a2c37a2eb077fd024fa","impliedFormat":99},{"version":"ee053e4fb378dc43221a892e05dd65c664cea6947e5c3819ae41742643d70743","impliedFormat":99},{"version":"eb1359bae2c2ff3dce1258094f234a8de89b7b5afe89bd73e725a46558fc6aa3","impliedFormat":99},{"version":"73c08893e5e403971e0b2dff8f14d9ce3543b1a821c0d2fb8e5ac807a5d14603","impliedFormat":99},{"version":"b886342183fcbfc5fa59216cf405970b992eda93bc8a89094e1d40906dcd399f","impliedFormat":99},{"version":"5170f383ccc3d8431414b298aa1b9fd8f88f9f6ad0f6b40e4f78b5ed98d0f12d","impliedFormat":99},{"version":"a3915cd0ac9bf5441edd5b60f3937d89dafddacd7c8d5f323a279f0647e95712","impliedFormat":99},{"version":"77fd5aedd61211bb0978c3e3763876b18c55b73df2da0d7e17e43a13b3a1a617","impliedFormat":99},{"version":"0138f7ea01346ce60e52fa324df54ddfd6c9c31495fe74ca071bc44674287f26","impliedFormat":99},{"version":"95299ea6874a7330630d89d2ccc3aee12945d0ad632c72a168513f491ad465b0","impliedFormat":99},{"version":"8e11782e3f5c78b28558dd617d0d2a94a524b5106e0e8a6826dd71964cf2b172","impliedFormat":99},{"version":"7238edd71a373627b2d4980472114a75a861fdc5a66efba81e6e61fcb40d3c3b","impliedFormat":99},{"version":"02c11188c7c7ce94d6dce4fdb002d717a9b672ea1293b59dfbf9b54345ea9dc6","impliedFormat":99},{"version":"3cc667806444c6727c1a8106fdaf37c897c25d6a0747fea8cfd817c275309494","impliedFormat":99},{"version":"ed78cb361de97e537cca46c234c5f0bcbbea8a77f88d3270142ed0993373c159","impliedFormat":99},{"version":"e15f2519cc04d4a30a0543a349ce4d4ed10a8cef76e8bb226a624d75b7b24a7a","impliedFormat":99},{"version":"a8de68a1e6c0c3405cff69a6a82dc596d576b967c9b925c494ba30f8782193f7","impliedFormat":99},{"version":"7c6055ad026fda59ef8ac871f9aebcb080cc4ee084af8feb3dab392e603fb9dd","impliedFormat":99},{"version":"3227e142b26a6ea9e1493c950c238446a8d0297b093c8c95261d4f3e63312237","impliedFormat":99},{"version":"8fa71f3316151cd87f46e0f50aecbf39ba424790904b6dfaaec6fb3ad9226c8c","impliedFormat":99},{"version":"f2bafaf9d83cefd7f55bc2a480d1b5ec302e7615b30642bb00bfbf58b567a61e","impliedFormat":99},{"version":"a605659c69bb74a59b480f20bfee716dca83d289a814ff696273f71aea4c4abc","impliedFormat":99},{"version":"5886ca453462c07e9a2a6a835172a5f2149bc2b713b618fe452a94a126eeb36b","impliedFormat":99},{"version":"9f1b4ed0bcf465a6390e7c3760e0bb339f3d61c28e78c95024a3afd3ed81fdcd","impliedFormat":99},{"version":"5ef95b011a154332189243cfcf622787a41f3c646b0e3efeb347006064b91197","impliedFormat":99},{"version":"e6eeef06ce02234a927acdd50fcf396b8dfd8e8ca34c082cccd253193b4102f9","impliedFormat":99},{"version":"a5c6769e0465426998fb1ccba6db4a008cd9fa4a477a8bbfecf5b130c4031734","impliedFormat":99},{"version":"d29f65daae852c87855413712c3e9e81abc8577579cd40aa28789910c149c8cc","impliedFormat":99},{"version":"d4c7c425aa0d74cb74b92a6ccfa740431b48d3240ac7a2219556720a107ded33","impliedFormat":99},{"version":"4de384bb706f41f1515883a6d30cd1eb5d149729a79b1524e1bfe86acd103d68","impliedFormat":99},{"version":"b8f2296cdc94fe0a235c1c73a2f97d394050ca3d74b6b45b07b563a0c2892dcc","impliedFormat":99},{"version":"0ff56c8f9f2749a7c23c130450f0045a19ebdeab9f94f9b6f051e2cad105a1b8","impliedFormat":99},{"version":"e5aeb2f888cf51e86a5dbf26f710608140d6f85847f935b5dd218fc9b865d820","impliedFormat":99},{"version":"f111b5055f9e48983310817bd14978fe8b44600383b2fe0f0f8f82c501034f96","impliedFormat":99},{"version":"d0b4fe6473d64b2505e3b842592c6fef0491b2ec29d3d43c74289428e5649997","impliedFormat":99},{"version":"83e50a8f0a133458f603dcfc3aded3d8e6da48ea71d3a1aa5383c71bd12dfa51","impliedFormat":99},{"version":"4ee9650128cc8493c1cf7d0923004c0c5e6ef374f838dfbbb5f510eb9eff21d5","impliedFormat":99},{"version":"9ffc8307fa2cf2c934da7979c96884df4a36b92b54ca97325494ab2be78fd8d9","impliedFormat":99},{"version":"6c71102ccf27db42765e5a9369c36c4aacd83a78883df87ee103d11be3d951c5","impliedFormat":99},{"version":"57ae79de0948c54c7a75ca0d72d0c360906c1dcf8e6c31f0da121e5a9d382717","impliedFormat":99},{"version":"55cc4ba04c0965890e2e46a092c7b76e92cc9812a12a10d510e75047e5c2edcc","impliedFormat":99},{"version":"977c1704104f936a37c41eb6b0d24cb413e20e62337f7e2be588d3ba55aeebba","impliedFormat":99},{"version":"c75bfc4a6eb3b39957d05a6b9535b424a98bff9f27518e39a7dd4830ceff4eb1","impliedFormat":99},{"version":"f8b79f53b95079b96f81ce308a927651d791c2c3e5f32c103e90fd61fd58d3c2","impliedFormat":99},{"version":"46f6e8a12611611d782904154ebda6e4b6596cd026b7c09e2c145e418fc8a2b0","impliedFormat":99},{"version":"a4560c1de64d671a8d837a6f0313669a7aac4ad7d0efa2be1dfba29f8d61c5f9","impliedFormat":99},{"version":"f193467f1cad8f2384912bbadceea4cdece962b9033488994a7405c29a623d8c","impliedFormat":99},{"version":"a846eff5c1eccb86a3ccda935896a80eaa2beab68c64e68c244908c86ed87906","impliedFormat":99},{"version":"a97588cd01375653ed51d1dd9bdfbd30467b130ad60bb5b6f981ab2170feed3d","impliedFormat":99},{"version":"584115df5500463c3fb9102a3a74bb4cc6a75a9bf6300701e1edf81da292ae57","impliedFormat":99},{"version":"4428ab19274a44693215f4f4195800c8221e1ac8a5a388ea534c4e405fac5f25","impliedFormat":99},{"version":"5ea28a5de3f28607387d210754f9d62d2cacc86684e89df9e74d412bac974348","impliedFormat":99},{"version":"a496ace7d44a315f014ae2711927ff48fd4e3583f4a1e42df356c37b2c890ba5","impliedFormat":99},{"version":"814ac5e9d9bc60c105e022e08d3a7590a5a76c93fce1572bdcc574f25e6dd68d","impliedFormat":99},{"version":"3eaaf422f4a8fd066e5c0717382129da90b5699a86f3326057a2ff4abee0c40a","impliedFormat":99},{"version":"8ec41716dce70226a7069735300ac20691513226c614b03b1b1aa7bf4ba3b2df","impliedFormat":99},{"version":"1ce271cba70878af1e987d3a93fa539c9e35facd9b70580f738242059416c010","impliedFormat":99},{"version":"9be30526f3d636f4b421d3e06983744be0b4fe245025887ba5a032cac68c2e37","impliedFormat":99},{"version":"990e4693b03502adc4038f04ec6a3db2458fa1a998edb9edac75141fe6e8f9d4","impliedFormat":99},{"version":"59507e67ece04e5a994907e68042d704641ae844108f8af9edd82350fc52dc02","impliedFormat":99},{"version":"a36d1226856f16d243adff16b2f5603ea245f505af5a85d5485ab275ca946b1d","impliedFormat":99},{"version":"0baefe65a675b833470b248451ae56f35697581018bdcd2880e4031cdc0cd6e0","impliedFormat":99},{"version":"2ab5a9dab4e1c3780ee8c7ba280aceae11c699371178c253cf5a36341a864af2","impliedFormat":99},{"version":"a80518638f84ea8a15c155eb5841341688ec6dc8a04c5693281632dfe555c3ef","impliedFormat":99},{"version":"27ba80c04731d0ea2f37d6b6f5020e45e60396f5c2f4666e57b03238ef0a676c","impliedFormat":99},{"version":"26ad6385782d8b51674e22f236866e7e08119f50a131cf5a091c8c576202f9e6","impliedFormat":99},{"version":"b8ebaa198a03eea15385326815ff31a01cb63c08bf69517e3ba4e1c8296fd14a","impliedFormat":99},{"version":"42e3bbbb7255cc27270fa12d31aca9f11abb56e985d30c1bde3f978dc01d15b5","impliedFormat":99},{"version":"5cd09527c5bd0e6a9df18ee2a8ca39e1a1af60658e50adfa3b2155c09f731a13","impliedFormat":99},{"version":"1e470d86477091005bc41203654b0f310ca7d45bc0c7952f8f93edc6d0613397","impliedFormat":99},{"version":"d09c441b12602fffde7cc009f9e815eda2e8c85a7ae1fb9dc9add975a935a4f8","impliedFormat":99},{"version":"f3590539d69e5d3c824a3dbb93015c58fdc8d6b90abe7496c770a32f20c559e6","impliedFormat":99},{"version":"4fae33c695361a6438082ebde9374a3779df07ea561b1ce08ae4f514bc6520f5","impliedFormat":99},{"version":"88a1b15f496a6c998663e6486b78ebba1c2abda9e220fd8e377c11dff3e4e836","impliedFormat":99},{"version":"654c457deece232e59f6d73733254cb78bf6b043d9d3c43abf0fcccdb245838f","impliedFormat":99},{"version":"45a99b02acd1f5580b93e48b5c610a60ebbbcb075928cf80cdc9316cce1bf840","impliedFormat":99},{"version":"ddad6719d075ea89cf7434c7a318b69d9bf26c330465ad1315c8ac4d059f7270","impliedFormat":99},{"version":"8c867515406eda8230bc6c852eff20073178ceb2ce94d6b9d0f5678ed13c0b8a","impliedFormat":99},{"version":"b430de30fb510ddbcead861351793cc62ea9d97ca3093dd202d349ee96ced493","impliedFormat":99},{"version":"f4080510cd7e9c65ceccc843f98f68a31fa74443ab7154515308c104c8433a14","impliedFormat":99},{"version":"535919c7119b5e3da3211594c3542946e1b9e5102d78614359cc5d90a78c6a06","impliedFormat":99},{"version":"ef7e1df5f97be321ccccbc8d0cad38c301bf55be0e5528d20d4e4479ec7021ad","impliedFormat":99},{"version":"994bb550d4082528038ce02c9c9e63ea278a31094dd526d91e559990b3ad17cb","impliedFormat":99},{"version":"df03f5b8e177d1c904928545a74d3831c90bbf653fbfdc29241b3ae20a5be486","impliedFormat":99},{"version":"a7d3f59a29ca31f40ea37349c44a7441eb10f1bed068a011760f1204dadd7a23","impliedFormat":99},{"version":"1e7d696c473c1e054caf2a8cd2759d09f442fac69473293ab665d6f8c714b774","impliedFormat":99},{"version":"df9f8f3d97d404cc47e4407b1a5c21c223b30fb6f1f74af05da01effa0cb56d5","impliedFormat":99},{"version":"5e923de3cf0d71e5d9a5ad4e90d4643490c5ac5b3f58ae66bb95f21dc04d6831","impliedFormat":99},{"version":"b486e2e5a4b514d179bb1503178694bf9560ef6b7660a027328b9eed5e73a627","impliedFormat":99},{"version":"d53dfd573a2464c977c298816c36231a62ae3dc27553e27a3c71f6665b269baa","impliedFormat":99},{"version":"6fd3ac9e256753431d1dda6b26de5e0822aa85eec20bc6c14436a177f4279be8","impliedFormat":99},{"version":"a6817b104cff6630183738048e9e2e5ab6c5bbed9d46bb528f80258b0c4ff757","impliedFormat":99},{"version":"40319f136f28e2db970e46b7e72b7bfd5a772a0df98eec5405c47bd6c9917c9f","impliedFormat":99},{"version":"21baccbad8315ef36c9d7f50048abd08ba81a3afa0712ef258fa6c061110e0e6","impliedFormat":99},{"version":"a4705cf973f4d37ce92c052828e2513fa023ccb452f94544931ba2f917b95f56","impliedFormat":99},{"version":"02cb56c5871a710fbb3f0080324577411609df19bc17200156e46a44fdf8d27a","impliedFormat":99},{"version":"57dba29ff984d0c81bb177bdfa07ab0e57dda6dcaea83067ad911eda34caf8b4","impliedFormat":99},{"version":"e576d9071ca12c411674ca573eb789590fb5604ae8037a2572886cfc293282d3","impliedFormat":99},{"version":"4d818da244b409a389c73e3eb75824fc0381b6e16ddaf80a8d15870c597a0195","impliedFormat":99},{"version":"f66b41b72c893a7d90bb8c6399a737e3965292488394040e509496a366edd1a4","impliedFormat":99},{"version":"e1b15aaae981aabbe7a8df2826f4c6f31aa0afdde1e23d15434e2b221a851887","impliedFormat":99},{"version":"10e6b4bb2c98b99ad66f1be41b2597076ada2dca513547292346b211caa31f4f","impliedFormat":99},{"version":"93abea381fd60a80fd3fa282b931d3522bd1e4caabd7dba49152f8f365106d82","impliedFormat":99},{"version":"b771ec60b06611363dd783f8e323f87d9c9432aca465f3cd3d39a6a4bcd79ade","impliedFormat":99},{"version":"baee3182e5624f4bbe9bbdb3512544bd77d07a08e803f36b898c292cc4650de6","impliedFormat":99},{"version":"028de137173a15dd541f16c8fe03c3da3f11a27d0fa1c58c336be102c440db89","impliedFormat":99},{"version":"be06143be469d3d0d5ce6a2ef95e368ae8454bd498d0c0b0be43aa4a6ecc88de","impliedFormat":99},{"version":"6b39082cbf46f9c0e56e73efefea459d85e9431ada4122755e51c4fdb35688cd","impliedFormat":99},{"version":"35ffeb76a74f42b609dba599cc110ff974c04d7382fc86d903fb5eb08787cab2","impliedFormat":99},{"version":"71ebf7cfd3b6d1e628450c875307a3c2b69aa9e94687237bb775a57f25cf1361","impliedFormat":99},{"version":"c0bb78950621e72b06d7e288280b6109885c5f7be8b583659d65e49d9af011fc","impliedFormat":99},{"version":"c6fea1f34d6a312a51ead56d2d9dd487cfe2ed243885b2da89cf51c1f4a3d811","impliedFormat":99},{"version":"9789a429c5349144946f1aeb7578a9cf517555e7bd2a5f8fa6889e8d58a8847f","impliedFormat":99},{"version":"e8388203c62a6cc3e00a4497fdf292b96954c99aed9621197aa7ad3a9845e3b8","impliedFormat":99},{"version":"0e7e5b08261dfe5ccdc9d9cf8b2aea22fa5345f96c317f03f5a0977acbb99e32","impliedFormat":99},{"version":"db4157be7d269a6daab5481918c15fbb5136765c3b8bb49489d3ee9626491b4c","impliedFormat":99},{"version":"b3fbadc1bc66794f535b892f5523ff3ae360d65ad72806bd24ef4602ba81c2f6","impliedFormat":99},{"version":"1f24cff6fb79847abba14795c5778f4cb1f137efb4e353e0a3fb1945e725290a","impliedFormat":99},{"version":"1056e3bf6369fe009b40647744c8402df6d0ba1f2d3a462a2aeffc0c83027934","impliedFormat":99},{"version":"b0da0c17082d1fcd42ba9a147e0002ce2f8e7fe8d7d1e2e9810b14331ca10cf7","impliedFormat":99},{"version":"f8a0dada5366f74f29291c9e6eeea48d81bf3b6dd7fed6aba386e60ecfaf2057","impliedFormat":99},{"version":"c8e93478ba85d6a953b7950a530a3228fd2b0663e76a42551fcf29599e5ae269","impliedFormat":99},{"version":"4e4a901be180e8174ebd0115037bee49eb8d0a5123e81f3a57b1b230f2fd7bb0","impliedFormat":99},{"version":"f4fe5a6412f5c4c2ce54119325f77804a5ff523d5e5d935e8a3c34c2d55e3a6f","impliedFormat":99},{"version":"da16b5d7e423e5e0096fce83b5703fd052ecadbd39573985baeae406a3ff406f","impliedFormat":99},{"version":"fd214d4558f949dc7cd55d2657db975a9272ad99a0a02f31af2100df72b0099f","impliedFormat":99},{"version":"ed2438ea4315e3db2767e79082e0066845c46ba040724ecb49b82d0597057987","impliedFormat":99},{"version":"e086358dcd0e44204dd616f050029b80e85d3df4e454d772ffc1580d2c42205c","impliedFormat":99},{"version":"b992f4b97e7284088fe8cd40efae6ceac1271fb8644173ff6015fd970d916818","impliedFormat":99},{"version":"cdcd99a7a4fcb0aa03586e374cc03c29edf192d79210d46ef2bd97a4e2ef6bcd","impliedFormat":99},{"version":"dd1a8059730f96916cceb5d6c734f59239cdc7aa6f5b0ee70fddbdcde89b1da8","impliedFormat":99},{"version":"82d3606bbaa51acea77cdfdc2e3199c4686ec2c6f71927972ea30ad6fc7b4a62","impliedFormat":99},{"version":"7588bf02783961f2b6871ecc6e9c60363ac79a0ea63ddfe4fffdbdf98a628a8b","impliedFormat":99},{"version":"f4ec160ed91e9bc5654099b8c9fa3c549e731c7a3f0de8d2cf14982dd126d1eb","impliedFormat":99},{"version":"330874360097f09e8cc4ab9c19d0c29ceb099ae500ab5d6461c9297b8c3cf0f7","impliedFormat":99},{"version":"1c4da43c55f396f8fba432a8827eee9fac8acf6c1494a57ef48177b57795abe0","impliedFormat":99},{"version":"1ce9891c6838a553f39b2ddc6f83075f3850a512475af31beef974f6f7cb292e","impliedFormat":99},{"version":"7911c13a1992969635e30dbc94eee146269dcfdd3e07e0e265daf5caadbd5f0a","impliedFormat":99},{"version":"6701c151b37be87bf02126312f18e65b3d91a2c20a7377833d33d919fd6df9c6","impliedFormat":99},{"version":"2a2283d33979c27b027ebc07e65a8f09d2f307f7a9efd1e07f0b29d88f787aa9","impliedFormat":99},{"version":"3c97002cb68bd560118a6c8cbba6ff2509bd73d87dd713edb3acd83e66bf4e35","impliedFormat":99},{"version":"664417e248d43d27f8a04b9ae5e1f3ddeab5aa6d9b41284ffd366705f18f4885","impliedFormat":99},{"version":"019210cf7ccaf627a430edae894d25b312db658fc0f2c282c36d04920b3f5ae1","impliedFormat":99},{"version":"a2ffa2e737e5b56c638654d4cf91fa6724b1b95c7be50a82b7478dea04ea8a7d","impliedFormat":99},{"version":"09abbe2cca1bd3d538def48c19a2fe62293bdf1f97504a52e26d40a3898144c3","impliedFormat":99},{"version":"3649547b460e5c107b12f6f29be60d9a81a59fb3034ef197979a694c687e77f1","impliedFormat":99},{"version":"f15d3906d25d611062b5c89d3145dd4a2ac60bf68f6f5e7f4d48d1189e474f46","impliedFormat":99},{"version":"6661f2e5e07a92e4267191cc689ad3930ebcc316cf44221ae8eb951e3d4612de","impliedFormat":99},{"version":"076c0467d216ed21904db5ac256da6a4fc1115cb870857be9905446da9e81341","impliedFormat":99},{"version":"1f659ae61eb9ef9148eafa4ec4171525da55c35a298b720f215ab0a89c045b8e","impliedFormat":99},{"version":"91753dc66ca2ba7e922513c6d73c9dbd3d6336ae1ba310e2d0520c99b93b99cf","impliedFormat":99},{"version":"ba087ea4f7c6bd22a19cb28e433a592ade91ebdfaf0d37f053051b81b3fd194c","impliedFormat":99},{"version":"490f6527eb1504768f00b4026ece4d21edd1949d5eca11bb76132330b5a8a55c","impliedFormat":99},{"version":"30c18c79997adc72842a146fef84249191bd8ee28f3456605c014b1e316dd7b3","impliedFormat":99},{"version":"fbd2d2f4374aad855367713ffce22d19c87e9483da420533084c0c734df1523b","impliedFormat":99},{"version":"b0e8320ce37a2a4f1cabd3d9c3e7c72065b847fba43562fa7680e436bdebea3e","impliedFormat":99},{"version":"9a6cd826888b63061bda7c34e6bdb871e7c0b01c344f15fcc3330cf5c0f99594","impliedFormat":99},{"version":"36101c7781871da39fcd8939e8faf083c606a896553fdcd7eb4b2f7c6ec47a43","impliedFormat":99},{"version":"ec266fef2d1fe9b7c49ae608b3ac60c4713d8d1cd2073c92f5db1a73a39556cd","impliedFormat":99},{"version":"34bc73c461e18692931c22a7afb38d308f7deaad9f21de13ea54dba0ca4336fd","impliedFormat":99},{"version":"574f30981211d1f1fdce83fad7c9b093ce986124480ce09ca871633ba6a17489","impliedFormat":99},{"version":"2083cc805a15babbf2d0abdf9d702fa6f75711590bae3fceb699ac26a4c76621","impliedFormat":99},{"version":"c51825d37dab66cb975c5442706fe6451b9213e098e3ff5f846e5a77539a7421","impliedFormat":99},{"version":"9068ab90549e1a5c0f165b9798650d60bd38e7f8615c05862edcc7d6dd10d5df","impliedFormat":99},{"version":"9def60b8c5edd45e81d01f89c7717c92584cb821b0fb14fbd025aa10b0ab3bf2","impliedFormat":99},{"version":"c7a0c488560b1dc8baa8c7f399cc474a221f7cc7b7380d0fe6014cb35f807840","impliedFormat":99},{"version":"81393d54e070aa48685972683871a707f160f408622954ede5d2a2f34ad332df","impliedFormat":99},{"version":"238e9e20c91fc4ccc94d52291ecef44762188d515f649095ed0ac99d59ff4fc4","impliedFormat":99},{"version":"77da76559d39664d9f60942ae9c12082b4fe475a67773bdae73073fd11b3888d","impliedFormat":99},{"version":"cf1189bc5bf2286b630f33be24872477888ba5d270b763e8f240a20e0fc5957d","impliedFormat":99},{"version":"f8081297cbe7510cabcd2142cc09294c076194a782ea5422c1fa5aa02bb662b6","impliedFormat":99},{"version":"1426f595ea21e6da826b69bd69c046e7443a77bf7a4434a9183297f46aabe509","impliedFormat":99},{"version":"79a74245aa7b853b8b5b0c249243529340426691807c11697106ed49d8c96381","impliedFormat":99},{"version":"e849d81b6db10ba5f8b89b25ebe91fbc005fb531b8cb417b6dda49b5263bf485","impliedFormat":99},{"version":"b19fe3a95a628557bca5b5ba33377870602b9c24bfb4eef0085fa0f04bd8f7cb","impliedFormat":99},{"version":"ebc64c6ca9a2f7942c0aaa51bac5d44d5d714e562693415a158dd409c2959de4","impliedFormat":99},{"version":"fdc0a465ffda280fc5c52ef5862816082f737f07f24086d1899b3b90ee03581a","impliedFormat":99},{"version":"e15e7ff9e28153e0c1c0589f35d82929e7918cbd60436c837c5604288f1572aa","impliedFormat":99},{"version":"1466a57f95e268e08d94cf92a030e469d4e6f8c180951d9c0fa453ab0a38e1da","impliedFormat":99},{"version":"d234d63170c7f6c82460feb9e8e7df14e38a694e90bb780f5054081847e8a971","impliedFormat":99},{"version":"5a353f55cd755eacd87196e6d14e8e8cfaaed147b08b19fe7e8721e3cff7e4e6","impliedFormat":99},{"version":"ef65b08cb958fa2bd14dac358334d8f500a3583fdb0f5d0d36eff66c6d2926b2","impliedFormat":99},{"version":"bc23a8c1d5732b76eee7581bb2757a98cf02048426f3a6f94632ad25547848a7","impliedFormat":99},{"version":"37820ebe50ca6e567034fba5022c1b2b35fc2dd748ffdab1f6a267696e041729","impliedFormat":99},{"version":"b7f56968a5aca1b41d82fd99d0859c366bb1e24c21610573ac69717c6c69cfcf","impliedFormat":99},{"version":"90889a47a83550ef4d6bb79f61a46f790318a5810874848df91ab1ce83bfa5e1","impliedFormat":99},{"version":"09ac02d7d979b4bfdd333f1133c60731f8e02f77ede93a58eee7c2e2894f8ed0","impliedFormat":99},{"version":"3a9e9b5c7ccef9aa106e8d38d624fc1ef7b61023f1cc99e49e0d34d806717aa2","impliedFormat":99},{"version":"280ca67f0276359a04f91526e7904723c634b8307f36e88b8ed1d55174d65646","impliedFormat":99},{"version":"56380192c5502ce3cbdaab811c03293ae0a3477dfec5ad999e0561c7a55cc049","impliedFormat":99},{"version":"a75dbe057fa7121e02effb45fab47e3c12d43063707c633be18c92b0a863a37f","impliedFormat":99},{"version":"813ecfd69412e80b3dd7bbf45217ed1f76d70a939457bc4213a38db216bb6e4c","impliedFormat":99},{"version":"8f4accfb1ff30808a2aec06f15c493e88683a576c5ef4871388ce7cf0dc80da8","impliedFormat":99},{"version":"1cfb8467bd39e186a94ead6da6a9549de18583c998a3d6ca7a7bf92922b7cfe5","impliedFormat":99},{"version":"095a8f6c16d323c4f1899434f895179bf3a318302fbbde8e25a0caed37832c41","impliedFormat":99},{"version":"e68ac7298d9feab31b040212b8c81d47146c89e2f0b044078413ab84dfabe024","impliedFormat":99},{"version":"669aa01bd96f92b0fa1f9f5dfdd7b974b8823e876d690d2aec1c480b70d2d178","impliedFormat":99},{"version":"1ad942280beb8fd7cb99508d5ec9ba3c0034523bd9ac8cb6059cbe9545194977","impliedFormat":99},{"version":"4429304845afb8cb868836562673380e3926380bd4600fa7a0a3ca8ef546cb7b","impliedFormat":99},{"version":"55d10877f0755054ab010faa6273fd064165339b9ff24e9f26a9a57b68a24c3e","impliedFormat":99},{"version":"510ec596cd8c9dc087ba99465428d24b027f504df7d399d97fc971d0aeb961c8","impliedFormat":99},{"version":"59cd0cdf1fa93bed733d6d84b0d5147501d87bfae8ce4668b4b093c22e16c563","impliedFormat":99},{"version":"854a4ed2d2ce9680a275cefab3ff654fd76627503ee311d19c0e2a25069627f8","impliedFormat":99},{"version":"edc84564b1feb469da3da5b7e1efcd6f48fa2e04f006f07aeef4c6a5659c03e0","impliedFormat":99},{"version":"b076daa179dc64180ee9538fafa4fa6e5796a3a81539eb3349737ed08d57e2c6","impliedFormat":99},{"version":"80c166471e9529f02214e75d8bb590870f822fba82a3f32cae59795e26d660a0","impliedFormat":99},{"version":"25aa189f1265f4674b7fcaa43004d7c7cbc46b6e1f3460fc413f3cac61c77da1","impliedFormat":99},{"version":"97b2bc30456543136698a248328f804aeeae10ff3211f5368e96bb9ddf532272","impliedFormat":99},{"version":"c9f27874d3107b39c845a4380551ffcdd721246db1c82bcf43cad86d66ea1249","impliedFormat":99},{"version":"451418c8195f8533477ee9f08c4642f7a8ffc9813d73a747d77fa70eeb66a79b","impliedFormat":99},{"version":"e768ad0e15d014a0a09e47dfb4c3d6c9d5247b186ec2887f07e984eceeeadeed","impliedFormat":99},{"version":"77d3636c84e22e5692cea961ba0957d687aecd776ca48cfa5f1c38f3fef6ce26","impliedFormat":99},{"version":"119c30d96ef567d5eb44ecf9caeeccdfe4b4af9321fe687d10b238f64a1f16c7","impliedFormat":99},{"version":"a4191368ca3082ba97bc919f8de837e29cedd508f11c20d559421ed90f5648c3","impliedFormat":99},{"version":"7424fdb26b9156029a3d0282191a16576bb1656f7e77c1da961b9e44e9fb502f","impliedFormat":99},{"version":"94e8a85d1d6ca2a4fe056f231dca96691acbefb9b0273ee551109bf69ce53f11","impliedFormat":99},{"version":"b78d4a5e2cbc7f6b244155da9fbd30d6016d225739abe750b47d443ad0057289","impliedFormat":99},{"version":"289db195422725ce6e021fcd4ef250c4702248355515ca1648227723a98d618c","impliedFormat":99},{"version":"41f3b546b4f1810ad4ae11e8a32664e70ec2b118d72e7d6cf04c85ae29d8a43a","impliedFormat":99},{"version":"ede452edea60a6ba0214f4201f5b959d57e7c26a743a5936a05f182a4bc55930","impliedFormat":99},{"version":"0a52552297c1d0af3470ecaa85e504b3f87af7fb327da36b5ef63ba06f135e20","impliedFormat":99},{"version":"ea7dbb28b1ebb092c8f974336a33caee5b29145b699ac1e8ce765979790bbb35","impliedFormat":99},{"version":"acc64e8177b7e4fe1a2545bbdf2875a89852ca5b042471fb05901c5e2a6d8fca","impliedFormat":99},{"version":"eea6ac739630bfb73b2e59bdd90d6f278a7ef0228f09525fd6adaf09d90f7280","impliedFormat":99},{"version":"d60137c38ec70f4d3495e2cf71ed73bf75f405f78c5c8644165bdfb0d9eef528","impliedFormat":99},{"version":"b437a4e1ad5b26d9c0d740ce71f956181c74bb283abb0d2510360ff065b0ff3c","impliedFormat":99},{"version":"7639369a2b3ad9cb82a464144871c8f90990443f92320c1fd3fd777c2ff5bdc3","impliedFormat":99},{"version":"c058887683e283e99658518eafc8ec08754f5b16792662b09a638d5a317a7719","impliedFormat":99},{"version":"1dee7843ead6b1135839649de81ec125fa95b5c7c80c52b8a08d8fb7c40ece9e","impliedFormat":99},{"version":"e3d7986bd6b3e8949cf921f9a50fd955da64d399dbd817f5394f772f7d52b6d6","impliedFormat":99},{"version":"4115d25268a762ed4b047dd9d5e29f46687ba84049cff9fb4c68b18d60e910d9","impliedFormat":99},{"version":"1ed9ce51e9bcac81f9d2c4d27ac2704c0a2267917fbd3dc6d207413cd7a6b272","impliedFormat":99},{"version":"f13cd5883699bdc863db77c15ba94050548e23e98fa5a5b26455fa105fd15549","impliedFormat":99},{"version":"008f7e03cecd8c3a12015053fb8714b8d738d35a35ed1bfe4c4b02be0116b89e","impliedFormat":99},{"version":"d86f6bbbc83f4d8944ffba72ebf5b486afa462fd26b9e04fbb199b4d2dd920c2","impliedFormat":99},{"version":"4bd9c7cecc7b653a34f633729cc1a5a954f95320f2dae63c48e8416a625b36de","impliedFormat":99},{"version":"765f3ebb07e1f7568ef6ca14d2e7f80900ac5dbab6dd25872f16e23fe5ef4d34","impliedFormat":99},{"version":"6cc361c360c6fe2723b28810e8725b894d9c88e4c4cbc197a8a6ae304452ceb1","impliedFormat":99},{"version":"386d783e5c235fc03eb9d3be33a41ed3a3744708c6f184fe9618a25b6caf9a58","impliedFormat":99},{"version":"fae4c4fcfb20aa2860f880aa7807d8d8102abda026c539c261e4da53997d9659","impliedFormat":99},{"version":"a4a256ff9f03798c49a001a0d4bc708e96a20be188d80d3f4d6f45e1d2480b91","impliedFormat":99},{"version":"80cc57ed8b2f7b94ac5d6b61ffa975eac13e39cc55ed9d32f2a4c03b07c9ce2c","impliedFormat":99},{"version":"6a1c4cb49ef0a86d4985496e9250de9fb38f286fa7bba5c2556855398e24d98c","impliedFormat":99},{"version":"94613cc30b4c60bc7b5e41f0014f5afc1f70238a9f3112400e2ed802b9edc2e5","impliedFormat":99},{"version":"81b5b7d4670aab0d1999b8480d483e3b0d12a8ef6a47238737bdbca5cc5b3af4","impliedFormat":99},{"version":"0a75c44f704965ede175c60995e6ef37943ace985c82b37ec004a02a23d6f54f","impliedFormat":99},{"version":"dc6fb79c45f464f758e0e2b6a09c34e0e88c980e3573c04333639d1061479dff","impliedFormat":99},{"version":"1d49ef9a4c8784be6dd7d91afb621b78c0fc8017769b9dd846ca63232e905467","impliedFormat":99},{"version":"db18fc8ed779ede2de97ba0e9bc61d13d1e971711bb85fa085d2be02376203dc","impliedFormat":99},{"version":"d92de62f96a0cf43b1625552f278468e06061453105a5b60ff78c6f09c30ad8b","impliedFormat":99},{"version":"4a0b017378d2a33ab5c70d2d885b9db3c10846e1f0f9b685e3be5edb59ea3eac","impliedFormat":99},{"version":"1296c3ac64a45bb17d8c90629bd2241c5942ec6ec51a4013ba540f2aab85eb07","impliedFormat":99},{"version":"7ace98c6830758e1f8e22def1defa1b77ccdebfd07d3508a1fc455cf098a62a8","impliedFormat":99},{"version":"e7f716b3a0ff08e8d289109d49b76e90644247d65a80cbaae2f476d3ca954bf3","impliedFormat":99},{"version":"5f5f47b68b781eb477da7e844badb481b34489a6e8686c1ba155bbcf2e7a133b","impliedFormat":99},{"version":"d863e0158ab6d1a5e8f315f879c255c7c64a4e45e5a980c2739c73c8ef9b1813","impliedFormat":99},{"version":"bffcf294ab82808982b50b77c7ed624a48b51b362e5108c98e3dacdd6b96605f","impliedFormat":99},{"version":"9ead7db9d2f12cb8ba534475698fa8d5cf6422e6ea0e77584b31e59ecf25eb63","impliedFormat":99},{"version":"1e2c2fc640fe117bc83f6320e5fd181f6e51a86a7eee232d510ed0c8b2909091","impliedFormat":99},{"version":"fe6441d144419863eae6218ec1ab1fb52af6c4f2f2c9993980038501ddb392e7","impliedFormat":99},{"version":"f928aeb71d76a99ebad7fc109e96135e497989ddb0c0ee16465e35de24f0cfc1","impliedFormat":99},{"version":"c883c7b209cf3e51a693023de37186e199d7e2342cd41f1f0b8ab9ca34851cf6","impliedFormat":99},{"version":"0fbe381899728959b9f565371a95921f76e4255dc9813ca95a423c7a81cf157b","impliedFormat":99},{"version":"b1dffe769906190c61aa2de29ba423c0d9764f05aea7ebbeea97f8323f6bab95","impliedFormat":99},{"version":"5f697a3ca1e2ce84e63f5c6fefd55ee00f4608a450aa765c923abc907f0032fb","impliedFormat":99},{"version":"6c8bedda58d1dcd5cade3495643fd5a1d1e6af75f50b961922b4214003c34daf","impliedFormat":99},{"version":"3d8ca129295708074c35d06f4e9b40e11e9ef3233df0dca6ae1aae5007bc1235","impliedFormat":99},{"version":"310790a0b20dca6d4fa1884837f48775ac1a7b711dab75bb25e203544c82a46f","impliedFormat":99},{"version":"03889f2863e44d756e96e03ec365dc1a299653897025ea1ded75e99f4183026d","impliedFormat":99},{"version":"66d50cba2ce96e5ff08affeb25d1ac3e6206887585a650ebf6cd712935c7bdfb","impliedFormat":99},{"version":"f72510666ed2d0ac50c3999c90e6a628ccfddeb123783759aedf8b64afc472c7","impliedFormat":99},{"version":"82d2796cae92487b15c2ce8bbc220d9d6ce8e6dbbe521b08f1d52eeddd83627d","impliedFormat":99},{"version":"66b417c8ef1aad8f6f4c2185673d0a3702aae2bee03885edd91a8df598d087ce","impliedFormat":99},{"version":"a7b40cfee10f0cde9d2d0d368e143a879af2fb8eda8dd86d722010b37d67fc22","impliedFormat":99},{"version":"584c9a3408fa4608c3f267c88f9942b23f3ff65a14daadf3eb1d24e1109a34e7","impliedFormat":99},{"version":"3933b9179508da74e2466ed5af41790695e53bef479533eef408ab34c08c3805","impliedFormat":99},{"version":"50aa52ceb6b51e337f65deb87aa418a7c24794fc0f9eabe41357461fa3106dd5","impliedFormat":99},{"version":"f6867a201454c8b60a78672c53367fa9dbc1aba247db27156c3e5a2fb3cbfb55","impliedFormat":99},{"version":"2bb7ed42f0bb306c77fa17ee006759f2f3cc6b6817fc4337d729ce010e7f8fa9","impliedFormat":99},{"version":"05c7c9e1d12cf2e7ec37dd86fcca61e4b1b4a2fff3fdf3648ac7210b5c09d944","impliedFormat":99},{"version":"8acc21ead62f0be0b6f6415e6dcf89ca2089d262a5d9da84f4688c9a37a9397b","impliedFormat":99},{"version":"e6b5b68af5046e3d64bbff45d3b41e0fa5da06f85efda33a07aef32c2fa4b54e","impliedFormat":99},{"version":"bb3b6049fb550829d304ff7024150cd4acf3650c11d9230e52d194caab19393b","impliedFormat":99},{"version":"bb119081db62488bf65a9563919a80d270201a1aca45d8ebd25a6eda85af1b78","impliedFormat":99},{"version":"abb85df073721c70450b1af3713cef5ab1025a36cc1566c31cf39475c512e842","impliedFormat":99},{"version":"8407d00275c737789e3e526a2f63542d43240d39cc813e0f391d0791e796751e","impliedFormat":99},{"version":"678b7873a38230d98a4ef8879f1b84b658b01506519e05a0291e887b914a4924","impliedFormat":99},{"version":"f474bc9e506a7690f97b073c4c93f16a67fe1859a7610cb08b54513b62f076e9","impliedFormat":99},{"version":"ce2b604e7fedfa087e8fc68f7d850a2208f2b0e5393db59a4fc7e21ebf04fa60","impliedFormat":99},{"version":"286b74c5381464962234a6e6fb7e41b56434a0466c0b8072f7762d102941e4e8","impliedFormat":99},{"version":"781a8c3f0fd5a7bb5c73ef1a804a09167ee6e72031be68af06d09a85d38003e5","impliedFormat":99},{"version":"d514bd8b93d377b46f889857af29d5a56246697136b3640514b376e15c88d9d5","impliedFormat":99},{"version":"e8e5d7cd52764357a339835c34327ebb8634c5cb932d2709f93c152f2abc3129","impliedFormat":99},{"version":"79583eda9e4c9b3807c71fac738a25625b11fa7ed587b2d1b5be69341219365d","impliedFormat":99},{"version":"e839194cdc481038bd67ed3c1566f5d29390c2af98f6b580115e7515ff3120e0","impliedFormat":99},{"version":"f347639fa958762371b8408e36bb5342a58affa28768131a834797c2a9ed07ad","impliedFormat":99},{"version":"053994ec3f7884ddb63bec643bf2276a4da93210bd12926ce4ea51cdd2f1af00","impliedFormat":99},{"version":"38bcc4a561e59c0a0e6304fcf9ed28df9a18471a06f7d25d4d839aa0f273b4e3","impliedFormat":99},{"version":"fa83e95a170c109f9e52f892a28a59973a8454979a2cc4161c765173e67f86ab","impliedFormat":99},{"version":"08a5be336a00293970580fdaf71e1197054ef2c9cd87769b19f7461cb3c617a5","impliedFormat":99},{"version":"ce1ec8f1a46ac13b31b36828b903ddc8b5f0a4f1cd59d6a1b122221fd0da4164","impliedFormat":99},{"version":"361fdf24d40b6d7f3250dac622b9d37becb257e4f855f85f0a30dd6558d50e99","impliedFormat":99},{"version":"1758b1f8898f121a176226b62e39461eef854bd33ed0552991c64cfcfd237826","impliedFormat":99},{"version":"7ef114c434371bf0bb373d8afb5e7cfdc5e1b2678683b92b89588a359bc01d46","impliedFormat":99},{"version":"56ec6bb075d116568daeb44a00ad710e9645b9e383fb61d83078a9e0d59ddac2","impliedFormat":99},{"version":"4d62e78223979185bf0fcea9a50e9eeb81e20e4df1f924511efaba42333d39da","impliedFormat":99},{"version":"170475025a9322720723114bb937bfacc4e5096666073e99d7f1df897cfaa46f","impliedFormat":99},{"version":"9dc2f4660f991088c1df79af054f451e7763b592b83911b2f55865282e5ffeea","impliedFormat":99},{"version":"3efbb5dbfabf41264c36e3e8c154ca2ec5a28dbc3dae87eb299ce826cec00eb5","impliedFormat":99},{"version":"ce0d485a733403bf3c9a552234e183b2f6b4f2a9f392ce8593ee52d09a7b4227","impliedFormat":99},{"version":"21d7d9fe27f34102a480656861392bd4fd37ca27837f9dca3542854bab35ed92","impliedFormat":99},{"version":"c0561d3440cc84a4e1a289b392dbde82e307d89eea71449c3a41b609daa9715a","impliedFormat":99},{"version":"ba519b143a14fee5242e9e8e549c5944bc6e6361af1162363c8336bb83289708","impliedFormat":99},{"version":"ddba8d73eee6f39c8a1458c244b9bf5cc54749f9d96b3e4c020bc27587afc05a","impliedFormat":99},{"version":"abf16ed784c74fd46c656cac5f5f7128a8fc9ce72bde1377d83333b32e160824","impliedFormat":99},{"version":"297f7c98f8447d6d8171c2c830a5769ac5d815ffb1a41b31ceba551b1869f91d","impliedFormat":99},{"version":"73bbe80f02235433340e10426a1a30e4862f302d8a4773d391544c47f6c44a59","impliedFormat":99},{"version":"091f41ba847167078b84a31ad0ff781613e22926b1aeb2bb9c5a5b81829e950a","impliedFormat":99},{"version":"fd8c79b9432d4c75dc3a776ca5cfa6c0cb01579667604ab7d3b564adfe02a1cb","impliedFormat":99},{"version":"a474af2d3c46c4ffb9ac67c35794d3188c99d0a9e29ebb851c925e966376d867","impliedFormat":99},{"version":"43f3124b51d65427823ac7ac1a77ddcf9d1aaffcafa199307c8359f96579d956","impliedFormat":99},{"version":"8b5edc0c16524d7ce61e374614f7be58eaf72aa31779e7995d75a48d8d321153","impliedFormat":99},{"version":"833d94d5f9ba3c0a0e0a9d98c79f4ee0ee099ef6f1aa6c594f98e9a503eb20bd","impliedFormat":99},{"version":"1073447c7d241622e16b03261a5a35e1ab9511a726fe2e9d9472a98531e08ebe","impliedFormat":99},{"version":"678498cea62d90c7b6f6fe6d77d6913a498207292fc5ac085c3ca6673e1ac390","impliedFormat":99},{"version":"07ce6472819a84a9553d35477e08e843adc1c4e06f706c32aaa6bf660573abe8","impliedFormat":99},{"version":"b49acee84c7c1e58e404d674f3fe8a72a73bea8f21a3384739ffac376dbc4d9d","impliedFormat":99},{"version":"e05f80fd2fd9766d62e58ca19f342980594df24b67d046b2407bdbbc642c20fc","impliedFormat":99},{"version":"31150f240226c796b061e96be00586546e8691604f5e8e074b6eaa1cf7ccbda8","impliedFormat":99},{"version":"bc4051bf149154816a313aeaafccd606224c7dcb5797faaf1461c7eb7b9dd32a","impliedFormat":99},{"version":"8384d125cdbde45b18b143511eecb4cb1a630393b20e0f384da7ce53ad500200","impliedFormat":99},{"version":"bd1bf0a339f399d23c42b3c71077411fa0516521cf650f2a979b6b3ba1bd5776","impliedFormat":99},{"version":"7382c0c8f0db3389d4c2deb5555a6a13510cb2f3b1299a5be434c78959065adb","impliedFormat":99},{"version":"28d190c786629e69bd4663183c7f6cdc6f6d95661abb33c4acf8e864bb17d6c2","impliedFormat":99},{"version":"f7dc29a70b2c22eb0ee37c58fa13b92c56a03cb8d07e517e50745fb8f3180fd0","impliedFormat":99},{"version":"2f7ff4d8be24993c494c9174d6d0d567a2f777d47ae605bdcd1f369561d3b1b9","impliedFormat":99},{"version":"3c5011b14981547a762a9a4b23494d3bc56000e016c61cf288cc282cab4565a9","impliedFormat":99},{"version":"31d6f6e05ea3be94b6d0958fe20585c624e37139efb2714c6d4716ee2e83e39c","impliedFormat":99},{"version":"46380cbf7064fd9f0c68902243e97e6c50d044c1c2075c728f9ec710b3d4764a","impliedFormat":99},{"version":"ad56ff5c498ae19096bea2f2e48a483496a69e7f882378ec702d0dab3431adbc","impliedFormat":99},{"version":"33ac964aaec5d03a7f2bdf85235133d8f8a542ee8cd53cdb6370cf12a5fbca1d","impliedFormat":99},{"version":"cb525b94462aaffa1953f40ca64f30af02c056ddf190a005c9372140edfb1520","impliedFormat":99},{"version":"f9c4cad499989bc9e52fb20fb79ddf145e74f0bbf658c11e632ff27784df8d67","impliedFormat":99},{"version":"8b2416a382ec4e4d165f2eb918be0656e116328a89517376a344e155f1f20574","impliedFormat":99},{"version":"c2b0b8ab0839ebf444fb041be8baa98073cf24e7d34e9d7b025283bcc19e4f6f","impliedFormat":99},{"version":"6b83038103737e8a2a62c32a504cb51ec0ae8c290245392b1ed3ae6422dd92ad","impliedFormat":99},{"version":"b8c56895889493917c83e010b52a20503a577fb619bd2dbf93dfc96194d1507e","impliedFormat":99},{"version":"a346e366f5b0bc3b7ed1246b37970ed4cf285ae542a9322fa570461ecab1ce50","impliedFormat":99},{"version":"ac08e7ce22f77cb1c3e394319bf6c5e91765260cfc1592fe4ec98d7b6dfce063","impliedFormat":99},{"version":"936087f474e826b32d78e7207349207d340d684a888366348c4f21e588b80af1","impliedFormat":99},{"version":"7a62127857134b78a06fa3eaf7c5cb640aaeb4c5f68b286dde92530b0d91439a","impliedFormat":99},{"version":"d68366828b46684cdec1e5167d55a37eb12ba413a8dfa7a9f3bcba9b0eedda79","impliedFormat":99},{"version":"1e29f487dba82fe1e01581eaa5452778335c8e8035dc7bc897a08438349e015a","impliedFormat":99},{"version":"436e04e198949a9de4d12437e9a0e22a635cd572f6a1e7569c69367aebd564a1","impliedFormat":99},{"version":"2de8f43486975db9345e8671658af70b6732714b921338efeee75332fc9d2b9d","impliedFormat":99},{"version":"a6042f5361a7d639267a950bfe7b8071c8fac0a22c3460516aa57132e5e8d783","impliedFormat":99},{"version":"994706f0de0e7cdc500fe2df830df39e8673b95e21052b860833bafa47e3b3e9","impliedFormat":99},{"version":"eceeb11208e29be4ee959d8601d44a160e5891acfaa2890ad3381d9ca4552a3a","impliedFormat":99},{"version":"1d4ca5e0f18a48b4c9bb13d191acf780c72d6364f53bdd5cccaae3da8dc75ec9","impliedFormat":99},{"version":"9ce599bdf3ac0d33b85546886643ef3826894f290a8fdccc09f34673a773d585","impliedFormat":99},{"version":"1aded17d799608c0ba976d9c017b71b552428923ae175c4ce2d03c6c251fc293","impliedFormat":99},{"version":"110a028c72500ec8975f8fe8e9c58bc3c53ad1b8cecedfdcc9b13d01c82f9fc6","impliedFormat":99},{"version":"0f28079d1c3a5b121158ca2992b65c9b39ea578d10f9a660edf19f0a3df74d72","impliedFormat":99},{"version":"6c543b666c02de0cb90fba1b177ad2e0f38c024b2d35baf7866d88a772335013","impliedFormat":99},{"version":"22ec74740daba4895b82f71ee7a81e8d84ebf8ca87d750a9cb7eebfb53715cb5","impliedFormat":99},{"version":"a99162f184a2eacda687014590c6cdb127b309361ca893876df04e6f42899933","impliedFormat":99},{"version":"a2e7959cfc8d0b1e2a8fea12946fdcee9d3d39ec83f9f69fd4fd3019dd729b1c","impliedFormat":99},{"version":"f75cefd211302ed3f67f21cca8fcec37a261f61aa74c8506818a594d63987c50","impliedFormat":99},{"version":"0bc6eaa27ee3b289d58fd31f9597fb3786fc5109a6fd247f2d18025cd8bdbfdd","impliedFormat":99},{"version":"2c0c57f8d531e350d0bee50b45fd4979073ad3e6770fcc82ee5d9434d33f01be","impliedFormat":99},{"version":"657ac562004aa622ba5adf699048ec590ad1b4c0c47c77ad86b8d3f491c068ba","impliedFormat":99},{"version":"b95426b6a6ef360b38ca32ed9a16d827079eb9f9c2e018be263a6f9a6d4dc62e","impliedFormat":99},{"version":"9c7501860d6585a6fb5d1021c57a33bb953398da637c5dec0effcd7877a52bfd","impliedFormat":99},{"version":"618c2d571eeea05cb19350eb01fd25e3362faf19fba3465eda421d8ee393bdf3","impliedFormat":99},{"version":"55430d53d77e54aeb57496d12b469c03d1970776db91e2ffebb9746f67f49db3","impliedFormat":99},{"version":"7aeb98df985447d55fe2c79d236a8ac5d9298828f5f64eed23943e28c91aeb1c","impliedFormat":99},{"version":"724c015bd6ca5e48308f10bee2f034ab5e0a5b13452e36bf05fb5f455a1e9ef9","impliedFormat":99},{"version":"8637d71c11793ae5cdd3171517ab0453ddaa84a916f6069cc7d21756f01c9a13","impliedFormat":99},{"version":"7b564c8ffe25811dd0300e0fa16d9e1bdb57307300a569aa4442a39c794b38da","impliedFormat":99},{"version":"b9aca9e3eae5ace3397362e7093655ed2f2000a3299003a0be3ba63fcd8a1cf7","impliedFormat":99},{"version":"1e06d6137283a011ec689e6ec5e76df3dd46abd938745d363a260adb54b0b6e3","impliedFormat":99},{"version":"9e0bc78a0cf6afc736a142062ad0186d496b8ef9dd72b5a3d2847508ac9281f2","impliedFormat":99},{"version":"12f6a1fc30cc21367b7bd024a1f234f1c87c2694e5123791c4c1336008a97ae0","impliedFormat":99},{"version":"62a82040362407115b58a948e8f2cfd6a8c277a2d549c18fc06d21de2c3931fa","impliedFormat":99},{"version":"1d5cd62166fb45b821f6292a748f772620525688c4844ed81ddca13253ffe99f","impliedFormat":99},{"version":"b6007d32fa2029a0a1aaa3c405900ede8b00d3ab717af0a1ce3e681bbdd7cbd2","impliedFormat":99},{"version":"d430e252cb220478232cf6783dac71b221875110f508ed3ffb4dd58a93cd3bcd","impliedFormat":99},{"version":"0ad363f8072fbe9e4f6a5b90a870ff30f962eee48c5fdb543445f709675250bf","impliedFormat":99},{"version":"5dd81b737d6f72e731aaac4678c763af0ea6897d7fd8196f04e5477757db3e16","impliedFormat":99},{"version":"c9de554ffd41dc88473b51f8a726f8e6752ba4f8afa12100fe24b9f18ce2794c","impliedFormat":99},{"version":"f7f004f03df753e513625d261669f87c49d92bc2ffff624b809d5c8c8068806b","impliedFormat":99},{"version":"651cbda75a78fdf3da54f3a7eb5de87652390a44a34140303fecc2de9983040f","impliedFormat":99},{"version":"200120eafe59da37d180ceea37b342957849377c7215d2efa9294edf537e745c","impliedFormat":99},{"version":"54b641ae9f41f8d05b6141e6a02acda77b0743e1c3b0bef93e689f890981b1eb","impliedFormat":99},{"version":"b231fa6be4cec8a6b90f98071335cb22a6196b64724af56b1e851851fce48bed","impliedFormat":99},{"version":"f9f8a5c8b56a99c29943ba345f03102f159330ee4984ac0c06a4b93f3f9d6c1a","impliedFormat":99},{"version":"2cfe431d866a5dc7a23dcb1cd896aeb995dd1416a668591c79d7c0548b2bc8e4","impliedFormat":99},{"version":"8def9a68149edf17ea5df2669bde45acd5cb3f5d28ad79042a428e830a24af1f","impliedFormat":99},{"version":"a00a6157f232a7d36c78bdd7c85d34c8c9e8dd7bd11cf2540748f5fb9030330e","impliedFormat":99},{"version":"2b08bc4a87ef16bf7f587f074bb9194c112b69462fda07d5410476c8513f9a7f","impliedFormat":99},{"version":"615607b6a29e3f697222ea2b0109a513d0c7a961969eaf9498917d36db55ebbc","impliedFormat":99},{"version":"343987d6dae8f8ec43e2eb17aabb2bc5a15be8e7c797d37c74792e4bd43ca839","impliedFormat":99},{"version":"a80b7bb7f5c675ece7f1e73bbf4f4f9632ea8e919c1170ed9bae7cc6fb707e77","impliedFormat":99},{"version":"c27b0fbf9267c8beebf4ce5b1696934e7bc08847f28b2c9249659441a94b9da3","impliedFormat":99},{"version":"24ae591b2f12be72a4a9fe993b92135d6bb3e5e1bdb13cc8097625651404c4be","impliedFormat":99},{"version":"0a1a4813f7ecee0d0c39fcd50583dac3b63d408a95072905ba16d469919985ae","impliedFormat":99},{"version":"7dfa35a959b024bf2fa0483f2488ea6ed893ba1277ccc075e018a1730270d544","impliedFormat":99},{"version":"c0e98150595af80066480e9583f89e6370693cd0827fc89b68ecabfa5cdf4a72","impliedFormat":99},{"version":"10134fdfdf255c70e3e1838ad9ef47047677383aa6978883c15eb0f5c17c3563","impliedFormat":99},{"version":"39056f5033d13f6d66c2980c3e0e20bf84ceb64d51567156451695773148ab1b","impliedFormat":99},{"version":"4d79b57384b19b721f29cc120ccd4c8aee6c2351d8626a6bc3c43e1e7dd555fd","impliedFormat":99},{"version":"ec110162f200873cca9707c8be48a53fb509377db99e916b839b1a36332a655e","impliedFormat":99},{"version":"77f67d567cba48115ebc24484cbd0ae510dcd1e41f9508ee1f5bedfc0a925520","impliedFormat":99},{"version":"45c5df088d0b2fd4aaebabae4ac3e69f05ead369aa0e146067dc7f553b0a8279","impliedFormat":99},{"version":"6428679bceeae2bc835cf15366c8219a9f4a3baf9133cd2004dac51a87ebde8c","impliedFormat":99},{"version":"163555f7f6c6ec766b30dd054fab8fa69f5184d82698c4f5e7457b4f22710fab","impliedFormat":99},{"version":"8a92621a4d723e1c2f868b18f4ffba76739a0c93e8437850f99f5009499b8848","impliedFormat":99},{"version":"2bbde324e159d9f59ab56a4c77128737958704e9c4331caf646b075c93d58ea8","impliedFormat":99},{"version":"b7c30808b1e0801ebc2929e8112cbc6471473f227d25fa3c338ddf8b4f2cc972","impliedFormat":99},{"version":"a5e0dbdd3c5ebcc3561c493df20fb1c35094444b67e5cdf894d5ca83455f0dc4","impliedFormat":99},{"version":"bd0ba36a4dc087b698b83e8f210c57b5b8cb29c4e7cf09f52b9c53cfc8527508","impliedFormat":99},{"version":"40b46bcc69563ffbeaad0e7a9be85f7ece978902c7e17481874d3e40b6f30baa","impliedFormat":99},{"version":"24fec85fbf17a72b387916cb73ac60273753eb2f75bdba89fd9d8373d2f8e823","impliedFormat":99},{"version":"0fda2c7354705f8e3c7e88495fc14af255e1e4fc6ab5fd2510b0ded969550128","impliedFormat":99},{"version":"8364b396dccb914a0fdd1a43ac72eab807e2285c023aabdceb4a90175ea1fcaf","impliedFormat":99},{"version":"267fce2a9d770603e7497ff4d2ebabd4ccf6b6c40b379574fab6e7bf16df3635","impliedFormat":99},{"version":"6a92542761d442813efe8c992d14ecdb9f0f88d29128ababad72d5372d377a41","impliedFormat":99},{"version":"a913e963331c6b05ee3b833b505855f555b1632bb7784fe97933f01569b64221","impliedFormat":99},{"version":"a96a6cc809d8f09952880373df55104b7869b601c45b58be80ea50ad46bbb0db","impliedFormat":99},{"version":"6f13100cf876318d0f1e8f3600beafb1622b52c8ecad388f82d5c8a7ec5c3982","impliedFormat":99},{"version":"12d6a24ab50e258055b79b5620e1ab58fe1d5e53f816d7d24bf63b2bd41399af","impliedFormat":99},{"version":"4d71c3f2950cceeea5a198a49f9548d4c1744b2e08e799026b2c01e26c30ad84","impliedFormat":99},{"version":"56e5330e3c63f36f57cf87db4674dc0da3e84c9745431e3ee4eea014372e6ae3","impliedFormat":99},{"version":"06454590fb020d26332487e466caa15be9d4ab476cd1f2b502d041df6e73ace4","impliedFormat":99},{"version":"0af1d04da71458d3b24e9d4fccebd327335f147e0b822871151c6fd74d8a945d","impliedFormat":99},{"version":"0a65f24484673f5ff0ffbf09d8970f509a4c7140cf4a92fb94f4ea8be759b150","impliedFormat":99},{"version":"c3ef829e6159fc25cbb3ffb2392cdc65fa0bee0de45d1905eef39ce72a30b223","impliedFormat":99},{"version":"1a2dd1a4760aed2479369e605adde150d3425e1081be79753e3a1e4238bbd4d3","impliedFormat":99},{"version":"8a580ca7dfe72e5393d3e68f966e9a6770a0a67ed31a06783a0fc2d5f1bf41aa","impliedFormat":99},{"version":"dc5c180e2c2ecfb91687fa245c0ef789abb2f258925c53a46c40b5be5cc06d7b","impliedFormat":99},{"version":"2e03d78647e74bdec38e689f32471393ec37f477b75305ca299fe7afb378a900","impliedFormat":99},{"version":"d7d10dd270b953176d3633315fdacf90cf2c3fdefc7980b948e7d242e1f83d20","impliedFormat":99},{"version":"5e2880e69e0b2f69713a7b5c8595d08353d8129e62f14fbeac5e3a7a2f350111","impliedFormat":99},{"version":"26541ee839fa2672a9aee147d1bd77c92304c07c9c794f4e70d98fca4ac4d5d8","impliedFormat":99},{"version":"e6e906f2106464f4ff8ff9d653ab434dd57d14f1a86ac364f0d764633cbd4fc1","impliedFormat":99},{"version":"2dfff976aaa4c0efbfb0694bc9a0115b3d745473b9ce80949fa02f249df466c1","impliedFormat":99},{"version":"4eb1912637486f1b4a360658f7b1899aafc2b07761ed99f60617d11f596ea58f","impliedFormat":99},{"version":"be1378176c12ced525f5dfb672356dcb44c3ed826b76f1ed7c2e14c17e698690","impliedFormat":99},{"version":"6bf5d30cc1060528895d2db6fc2039c65606d2871c575667913cc67f28a8e2c4","impliedFormat":99},{"version":"a6cdbac42ca8870acf70615c430f50664236917dbadda4829b2ca85e22f65dc2","impliedFormat":99},{"version":"1a35594e8294b60a08ca90d0712772b851d3bd3343fc3f40c517e8f91118ae81","impliedFormat":99},{"version":"bc992f34d8a1ea9b6bf76ce99de74c427f5c578c36d40f0f47627d7edefd1396","impliedFormat":99},{"version":"239c64046bfc03eaddf462ca3bbf31f3f629f47c42292681eb8174ab4a1bf080","impliedFormat":99},{"version":"e9922a0bd88d88458bd8e1b59629942989a4606cdc73104549b1d975e11c716d","impliedFormat":99},{"version":"fa4277a5b715291925ee253e3d5f4ace6858e30527cfce7c981dc3d910b282f6","impliedFormat":99},{"version":"32e62dcf57f473fe959c8e55c05b85fc6f2c41bcede76f192462688214ac52af","impliedFormat":99},{"version":"fb4d45a4a5a2d6bd3e55a9bac0c06fdfcfc437364122611b4eba92331b550764","impliedFormat":99},{"version":"fdd44bceea44c6b6bbfdcc60b21a749ece00c3c9ca39effd76728769abdbd3e7","impliedFormat":99},{"version":"5c3e3cb494f815df8289e573e275f5af22b90f65ea4f84ccc02db71cfda44c10","impliedFormat":99},{"version":"6156ec87a3211fab0e201ac49e23bfa7b305e4e144bd3096e6fe134d7b89b608","impliedFormat":99},{"version":"bd18b9e00a08f52ed006cecd5278e09636c9a8f554a53cab6980436f6ddb930e","impliedFormat":99},{"version":"76f6f91c693dddac2959f67e0dd1694cafe5a60d648308ba37a8b553157e08cd","impliedFormat":99},{"version":"dc9283c141dbfa236f0cc44a80455f03127d43f9f099a347d577499131bbf0d1","impliedFormat":99},{"version":"2408d8663c9b5598c32539ce5a2e63e61d3a9c783981441be2ba91c59a214bfd","impliedFormat":99},{"version":"c54b3671ac0947ccb76b538e9ad5c2c7bba2d6b4e2facfe90ed3883e5fc870c4","impliedFormat":99},{"version":"acb187dcbf37d298e9aa2188723266fd52bc691c21368f7ffad1f5cf5efc8f23","impliedFormat":99},{"version":"f844308a48c568900f8e3618cf117fc82ed6a6436b0ac3d2965af03d14c6b096","impliedFormat":99},{"version":"b0f5047634786f8ca1ed076ef456eecfb357a7bae8696f02608d996ad8afc52a","impliedFormat":99},{"version":"4dbd3281bbe8fa74971a802b8c1e1a0747d2e70762b9a4814f7ff442d3244230","impliedFormat":99},{"version":"c364b9476c21321d77f63075ede02113e60943fbbde14b86cc910d7891a5218a","impliedFormat":99},{"version":"653fdee1ae608312fce98873672eab205ac9c27b23de1b2791031a0f6d2b98a7","impliedFormat":99},{"version":"e4fd76ab9cce4eb4f61d7288e26bb8a5cafffc80954c3ff18fb27b89204d3442","impliedFormat":99},{"version":"ecd553fb6353dc1c393f596ef37cc74ef38fc0be23e07e780ff49a39bd448ed2","impliedFormat":99},{"version":"0e06ba8ad31c92c97ff790a2bf54d322d8b926d7a7a086ee0982e955a7399bf0","impliedFormat":99},{"version":"1a9e58fb9dba9bd5f06eeefec5c2f8d5292781b06972fb9c9fb1bbd8f15ad5f9","impliedFormat":99},{"version":"1dc595c305bbf9c102977fb7970e0374d1912357e17b3a95148580c6ab46b5cd","impliedFormat":99},{"version":"af4ded8d2788d0228df353955c9a0ad6c24ef0cef8d63b589629cebdf3e5c0dc","impliedFormat":99},{"version":"d64e5c26af45311b8642ff27d05c49d8b94b9828d353540ec1c0084b92e2963b","impliedFormat":99},{"version":"ea52f5796671ac35eb62145531a9d9b33e18624c7ea907b2ba054425865ef23a","impliedFormat":99},{"version":"63ddc0222be4dfbe4edab975080455c6753b18fde348d060a6d3ed93e0097cb0","impliedFormat":99},{"version":"79cd6112cda96ee5a62a7c4ff9f7b08651af15fe45205751655cd4b4c2d1b58e","impliedFormat":99},{"version":"2fafc95d1afc4b30891cfae6447b0547fb21a4eaef4e7b6f47b789eb07698cf4","impliedFormat":99},{"version":"cecd28c55bb2e5f66e02fa64c5eea751f9273c8515918820ca2409cbb90c802a","impliedFormat":99},{"version":"4e0d0aa02da405ab61975064bfff8a927b493239e7c2694b091ce3a87512b802","impliedFormat":99},{"version":"0bce1056c3ad5ae75512bffd21985a8107b6c7520bf073e4d1fac19ef1970c4b","impliedFormat":99},{"version":"dea629ac95fc8d6abde0a2486299c3f3c7e6ddfe7c14c3ce1302ec17c025a021","impliedFormat":99},{"version":"e6ccd8091551b224ce7989079751cbeff03207986bc77aee9e1d50c0d4eaa6e7","impliedFormat":99},{"version":"7d209a3d160e8412a320568c8343f4aa5e2a216e019f1e382f3d4c6b1069295e","impliedFormat":99},{"version":"aecc37bca58dc791685b01ce18238f8d2a03fdeffd4ec2f316e37f5b7307334c","impliedFormat":99},{"version":"7b8bfb125f9de5a4ba30f5d81b5f671772cf76987d880147f786e33fa878a5b3","impliedFormat":99},{"version":"d57d9fbf2673c403c6bfcc61ab70c16c4984a6880e647dc46c51333b1f812833","impliedFormat":99},{"version":"f3354264b00ad46c7f3949220e00ff70955bb67889e0a23e0d055153966b6294","impliedFormat":99},{"version":"7fa9b40a7f202ccd3c9a22a5777f02e50d0c009f633d23c8d8cc7877e95c89d0","impliedFormat":99},{"version":"1186f9e84db92d299247dbb5a283311055f82de1f8808dfb048566d1207fdb82","impliedFormat":99},{"version":"c30f1892b9de9bd083512b9fb5858cb5787481ecdad8920d7ff75586cc88d432","impliedFormat":99},{"version":"e43ce675a68782c430c60de48267f8268bdd682697cc38e54eb9d03f53b7791a","impliedFormat":99},{"version":"df1d3cca4a0197138ca725503c85ae59e06cc21e1dba64dcae94b8e8c2b1fffa","impliedFormat":99},{"version":"6d74fdf0a7ce758f316a2b2c8960ffc50235320727326a35f580612c0d593a4a","impliedFormat":99},{"version":"ff77c6d5963b87d427ccfe6509756ef01de1aedd685d186e177603ed741c93c4","impliedFormat":99},{"version":"62c3d6cc07f01164ab6b94b1daff72afbdea44a42efa750673e7c65739b85816","impliedFormat":99},{"version":"f9aa7dd2db630e9ad51214d9d0e14da672a82322684d908e361d1bae4bf64e01","impliedFormat":99},{"version":"eca2962342ba258932717a7431741e432d275beae879d6e655eea3cddd69ca91","impliedFormat":99},{"version":"807091b6bb063571171ba50d3d7da2daf4e718de25a32b4d168d9b2854b16979","impliedFormat":99},{"version":"2256e03055dc0cfcb237da2751a763b4c695f6c6c82003cf0d36851cfe77384f","impliedFormat":99},{"version":"af7cc8267b8302c517bd0a7367642bb1cfe2e516b00bd855ef85f2bfdba59c60","impliedFormat":99},{"version":"fc490848f97ceb21a52dd6afe32a9b98cb1cb2e6320806b5fb9c24757a1ca769","impliedFormat":99},{"version":"c06313963ee23643e32bb4a0deab3d69453115fc1aa59eb2e9699516fd9883bb","impliedFormat":99},{"version":"31ed9dbe091c55243ca0d95c7d3796448367091cb1e958033c3830cb58d873dd","impliedFormat":99},{"version":"ef436a3aff282493c3ca803abc5dcb5f678be8c2345cacb3a0356a47c6bad708","impliedFormat":99},{"version":"75c0e4d715175affceaf29516d4f46316bd757d2e30945ccce722aed9aa91ec4","impliedFormat":99},{"version":"cc7ff6ad94cae8f1b6b73cb0f3694e1bfaaaeb7db708efb3aa63d657d453cd05","impliedFormat":99},{"version":"4dea994dff6f8f13a75aac0d5fd62f867790c72af9c49f2153b48f1fbe94a545","impliedFormat":99},{"version":"c119857e4642cdd774d2a84d9547ea6daecc225b2273cc6e57035bd422f5ae1e","impliedFormat":99},{"version":"04127fb55c6a3db066777ec0efa8196f461e779ae5f25be1460c5b77bc6fffe9","impliedFormat":99},{"version":"0c11f88d9828efad354c4cee7152ee7eaec74fe59ae13ae29eaa68c1e65521a0","impliedFormat":99},{"version":"df3e033a605c7e31ac7a6e7517c422dbba4a940c8aaefbb8a64730bbcdbbd199","impliedFormat":99},{"version":"738ca31cb07d20afdee107364e270214da052479973e0b08a9a886f8f90d2803","impliedFormat":99},{"version":"bd034d73f41f90d3cfce994b51a759b887632bded1cb36f1dbe2773973a1ddf5","impliedFormat":99},{"version":"35a17e3fc0e53518707795ed24bd428633c45a4e8fcd4657eea2963b42366eb2","impliedFormat":99},{"version":"b3fd4b12bc1b4f73f1a110ccd99ffea459864afb8e682768d641de1be3508bc7","impliedFormat":99},{"version":"d1dcfea8733aec727e00c133901efd6974a9f7601c4c19ccf9c2e99616efc104","impliedFormat":99},{"version":"eb0b20ca60a2a2ee1daba9b78bfe8ae99487de57223aa6f0f6b99252b0cf9363","impliedFormat":99},{"version":"8c868f22cbfa1cd301a05eeef770dbc4453dc42f065fce267945674b4d82f3c0","impliedFormat":99},{"version":"e6ef6469aa806b0907c02b67207a86bdfc8a9291c5e63f3ee0a72437df56c055","impliedFormat":99},{"version":"85c677f795a09a5134768fc909906713afa46536fb7c178a58a2dbc8f981ee41","impliedFormat":99},{"version":"97ba6fc4a6c7975f53b16bc72ff0ca05d41104ef2a605f07f219ff23ba6643fc","impliedFormat":99},{"version":"59e5e42bd98ddb731aee619aa16b1d7403e598e54bf75d2daff25c57495a13d6","impliedFormat":99},{"version":"54e79ba20e85bc05dbf39eb24f94c0bf485ff76b096654420eb32dbd7a616c53","impliedFormat":99},{"version":"80a960337c67f64bddaea4ac2fd437ff09dcd64c50ebef0ad0e5abed981c862f","impliedFormat":99},{"version":"98590ff94f0c45c9da700bf9c972714e4367cee30842aa130ab501f463574bbe","impliedFormat":99},{"version":"7d4539a233361ed71d335793db13085aeafbc1e28920a9784b01a8b2d461d354","impliedFormat":99},{"version":"a00c3e3e9475eb832c523f41734a7ebef312b1ac1d6ecb68d22df37798e18c02","impliedFormat":99},{"version":"275d39d59a9a5c7ad2d13757bb35bd190f487cf5fe221b8a556be2117ce6c2a3","impliedFormat":99},{"version":"072c8a4438ecf5be5f093ca727bd03874fdcf7ea8b810a5f3d5b5e9a1a158646","impliedFormat":99},{"version":"6808d7d909f594bc5e0695b09e9f663f79b08573e3d95b837c1f15294481516f","impliedFormat":99},{"version":"1c699544448121e8a6521399e2b9e709c9561b768bdc3249b035be0ff5a458ed","impliedFormat":99},{"version":"c0f9d148738c7f952a6e0075e7f7528e67fe813e5473741b6ae2d7bd1c701727","impliedFormat":99},{"version":"9594f39327c3776aa4083e472770ceffc14d45e669998536b60b5de5b1006143","impliedFormat":99},{"version":"4830175a6be86164822a5b2f7fef7a97fee781b33a52453ac8d8b50e3ec91830","impliedFormat":99},{"version":"d82b5aaf44272f7e153a3a469db46739c306b5c173e0382cebea70bf469d3a52","impliedFormat":99},{"version":"65d1b54d95e8712f8bfec843cfdffcbaaf6d92d0a69abb7345ba55f3f5712017","impliedFormat":99},{"version":"80464d99a301e3db960da2a7b010ada29874dd6dbc5f24731473a2641cee217c","impliedFormat":99},{"version":"3834d1fff358eaf5b52078c27881964e31e54fd1b5be8cb097a2ca75c2d97b7c","impliedFormat":99},{"version":"3fb4902f08c9b5a77956e9a6856505e00a7fe476a586e832571c3d4f83ddba31","impliedFormat":99},{"version":"93c8e1d9bdc1cfd856abb2ddb43c31da1adb5bb23a3172422ac3426d80687f97","impliedFormat":99},{"version":"82dc653116ac560acfa8623cd3fb7de40ac5f2e3d65991ce36070364b4f1bb07","impliedFormat":99},{"version":"ed6c38e10a8ef5ff290aa185177153ad1e0a85c7d867047532d33a6b1bbf2c4c","impliedFormat":99},{"version":"bab1312d15063c1685b8aa51252669c1c5843f3f7c3568674dce1a342cd1f7a3","impliedFormat":99},{"version":"023a397566c9c9ba724750a017783ce78006b04d52f96322f42ec104daa1f776","impliedFormat":99},{"version":"10fc62a7f653cff767df362868a121e6d5fe9d872fa4c0fe3de8b1acd4324f0a","impliedFormat":99},{"version":"05346e4aa851fc01eef2d4ccd77b5c8e5ef306ad044ecbc8594aeb7d753e1d33","impliedFormat":99},{"version":"05e97bc0985fe092ec1fa0204a4ba209902419231941be9d7109e759992af3a4","impliedFormat":99},{"version":"d7baa0aaffbdb80eb363c76f5c062841441281757037ed9a740d2e8ab9cdc391","impliedFormat":99},{"version":"cfc54c8c0b1974dab7994739dc901c13fc8cef6ed11cc0ca2777dc0eb1751f40","impliedFormat":99},{"version":"9815136c848202c8b0705c8e56a6546d2052cd0abee889fc0a14a08c8a13dc0b","impliedFormat":99},{"version":"4b77dd1d6d600b721c6b475b9479455233e7a189c1e8db14a53a8e34726d2f55","impliedFormat":99},{"version":"132c03d02eced90fa294ae08f814348ab056e1bb6e041a4802aab72deb61db78","impliedFormat":99},{"version":"3bbe7954dae9f4355f7da44a99dddcabe92ec11239e38a0c3c89da998156d9f4","impliedFormat":99},{"version":"3531151069b2a08dee7b82bf7c3437f707048c682395003a49fd630968f5ce15","impliedFormat":99},{"version":"51a2a1e0465147c66d3b3ba8b6776caa67e473db9aa5ba771a7fd5f661d994f9","impliedFormat":99},{"version":"616fc8cf7d4a11d695eb0e309522f1facee105a1c740e74fee5f710dc745bb40","impliedFormat":99},{"version":"c14a02a41bbdce485e6b9fb0a70c91a354a2044b55e2563e6731d51a3b3ead5e","impliedFormat":99},{"version":"bcdc75c3befba305fa771f51762f600ef3c7f60e12ff9b0ed6eddb3b11da3792","impliedFormat":99},{"version":"9b681badef747116e902328e3ca5bcfcdebb16259a8b8419a7e95132977e9e60","impliedFormat":99},{"version":"fbb328355422560a1393277f37065465dc36c25c5afb716c6dbbb4107f2c3adf","impliedFormat":99},{"version":"f9c5a5df867ff31c79e8dc2e7bcaafdca06d8a8adc28a099d8d05a1a4efae2b3","impliedFormat":99},{"version":"2e3ae086adbdd8ee059a46739badcbc7f46ce88b16dbc2afe2b669b9130a1fb2","impliedFormat":99},{"version":"fddbed7f42a61dd91910d468774ab7aece510e4bfc8acc8d6dee2f0b2cc3024a","impliedFormat":99},{"version":"d0c25a9039e1b56414c154cc2c3e22e84c8be8855c9780f4c1ff793f9e551589","impliedFormat":99},{"version":"1f700fe107041685043548d72a01cd21bd31d658a5a4d6d230367840ce65b389","impliedFormat":99},{"version":"c08f35285d1267946e19663f7c121f6d3e16cc03004a76506abc9f9f8bdc8b6c","impliedFormat":99},{"version":"021902fc642e31e811caed40c5ad07e0b105d7e269f609226b6447fcfbf62733","impliedFormat":99},{"version":"0a45d152162e8a7803ad6b0644e9f2098e28281f6e5ac08f21f14587564f568b","impliedFormat":99},{"version":"5b85b4c8d575c506088c9e77dce0607988506e12ad1a011f7068c9a35e5abdf4","impliedFormat":99},{"version":"d64b38d73752d637905e2ee9f39f7f82e2102dc96171adba9ec6454e396e0319","impliedFormat":99},{"version":"119acee61f07781e3ac933bf590854365e8117c31c896bf4bf1b9e3f399ef492","impliedFormat":99},{"version":"95be4119f87bdda94d4302cc6e6c1e2d1b849d170c5f187815224eadf7b1bcfd","impliedFormat":99},{"version":"ddcec9094b6fe3c92c6758cd338292672416b7d58ee807483dc8ddf629d699d4","impliedFormat":99},{"version":"283daf40cfa9417647d1b581cb734366581a803620bd20c60d92bdb7bb807beb","impliedFormat":99},{"version":"94d19f12b5d52237ee39b35048a1b7aca7c0b8fbfda6457c2dfd83414a6ecb89","impliedFormat":99},{"version":"7b14ae6939622c3fea912933aac786f88fb39c20f115eb7496ef890eadaab2f0","impliedFormat":99},{"version":"1023d9d72029b02a767d59537342fa1be3f3a33d901b52d7e0d47d42269f0215","impliedFormat":99},{"version":"a594453ed8c004df6b340bda0226e7cfe94ee67698d1c0dc1b9207d1e20e0ff6","impliedFormat":99},{"version":"4c3c179bd19a0c96c14749c9095714c9b150fc6ff2506c81b877d25644045630","impliedFormat":99},{"version":"21d8b3b7300ed19f41050d3fd6317f5b7c65dec299e1698502fb6767ac30d839","impliedFormat":99},{"version":"a6a5c59f2cf45d5ab475340ae96ebeca495094d15d72102b7bff05b7f0bf7383","impliedFormat":99},{"version":"eb7628d3567bcd483094fc2688ceb5305ece0563463787cca7035efce9f43c2f","impliedFormat":99},{"version":"6d79ded4df43562c8062badc829bace67925c70d18a7b72c09b7d23797cd0891","impliedFormat":99},{"version":"d25e9a88e761b13dd58628d2d41641195ad79ae927bb268387082656673ebe0e","impliedFormat":99},{"version":"b161e27190a5249c6e5660288e014bcdcb1e5bcc2733f848351b027e0d772ff4","impliedFormat":99},{"version":"ef547825b57ce9440a18bbd170125b89cc5bb8ce7504dfa6cd528a3dc4489e76","impliedFormat":99},{"version":"e74cf49b55f4550883d9d46173dc10ba5fd38d55f69f8617ca147b6d39c8a811","impliedFormat":99},{"version":"339e68b5bb386ddf9129bb3370d01f22f8919cd0e40ca71e06f881f69e02af66","impliedFormat":99},{"version":"8c65681a3636770c9b9b2b52d14ee4d59b48092175ae3124c044a2637fc84153","impliedFormat":99},{"version":"c6bd0b212d354956e2c5edc8e0249adb3c0e9c942e7e1794e23fe9f0dc721828","impliedFormat":99},{"version":"5e3b5745889dc9efc2e6f5515ae5bcb8135f2a8412d4a1facf374e09176f7cb0","impliedFormat":99},{"version":"f0576bc1e933fdc38f0f182775c24efab54f2a7a9b4a1953abe77a620346b9de","impliedFormat":99},{"version":"f061956ca6ce7bd52bf726d2e87e620aa928807f3ae526d3d432742d1da6e2e5","impliedFormat":99},{"version":"39b7dda3a4bd1ebfe126c67d3df5ec230681097b54a4f5a7abbd841cb321c003","impliedFormat":99},{"version":"532cdefa9aa613089dbd91804d4d9de824ed6e9f44980db04a14465e19a9e4e7","impliedFormat":99},{"version":"d0682d488ea0a7deeefc809d65c42e09f339dde50dec54f901104ba35c70f5d5","impliedFormat":99},{"version":"d4bf07baf86b8e6fbefb578d8a630db479bd8feae8a9932175c4d412c21f27f7","impliedFormat":99},{"version":"f76ccf2e0dc0bdbd29cde0b9054b0d510719730e6455db17e98f2b10646eaa59","impliedFormat":99},{"version":"f8dc3be04418be55ab97a46357ad0f6102a118b63e756f0fad0de94be2094d3f","impliedFormat":99},{"version":"3c0ebcc673f72499ae0ed99b0fddd52a60fcda51d50b11d1adc046e5985916a2","impliedFormat":99},{"version":"5395a87aca9a065a768051b7a6c66d3cef55e097ac2ad6ab917e27c3c92c4e49","impliedFormat":99},{"version":"79bcdbfebd96644cd9e93ca0b53ad0dfd398e0382c03e58f3135474ece45ca13","impliedFormat":99},{"version":"a134f53667b920e6ec7095c1c3cf0bcaa95008cd43000d3bdab1e570ac70cafc","impliedFormat":99},{"version":"116808caf097e8bf31ce1cccf1bf4060608cb7d91bc426b92abd97f7287de047","impliedFormat":99},{"version":"bf7b4bbe862d400269c378a2f744a5a94df5467cd18c805ee3a5ad741766ca40","impliedFormat":99},{"version":"2bb9ec2ead61f07ae6d8277af9318e37205b49abc9271604597844346ed3959c","impliedFormat":99},{"version":"d75af40a0b511d0633157556a35141a834d084094b01a8908d78dec2dd80fc64","impliedFormat":99},{"version":"ec2ca88bd0a13a87374a11d5bfacadc96b55810aae7a008cc44279b7757839b1","impliedFormat":99},{"version":"83551e90c20424cc93773db3ed94f8c43b39635830bf5548455d0836deb96393","impliedFormat":99},{"version":"945b49df2cc8667365e20dd3c1f8de64447e0b32f1d91c23ad06b07b0aae44ea","impliedFormat":99},{"version":"d6dcc687d0baf6635c1808c740aba4db6e37a3b5526f2655f0c291accec0decc","impliedFormat":99},{"version":"1dde80f3943e6bb56df526b77bc7a41dbc234d4393f1b8f19cc6672171ead575","impliedFormat":99},{"version":"7811c9c49dcddf09a7b307acaeb6f3c9b4d1d1b97961d87cc94670c46ea0362e","impliedFormat":99},{"version":"25c965403446cdcc5417d13db187a7f0394f7106cd1a45e1b9cd3a2c08aec4b2","impliedFormat":99},{"version":"1dc95a1964076bc1aeb6731a17ae5f7163846f248f60db0e0961ced43db22c6c","impliedFormat":99},{"version":"63482e1d5b8a0f3a669807bfa5355d3a9790856b120f512b4bc40a0dc65c78b6","impliedFormat":99},{"version":"7755b72611ef60e8cb35ce5e7ad393577e7389c485d4683570751e88177b1bc6","impliedFormat":99},{"version":"5445c1b45a4b24cb793cea4f1f8678912f2a7eda127afa72c48614aceb3059db","impliedFormat":99},{"version":"b600d5e621219860f2f6f9e1aa25d777c6b6c469a9048839b0bc256f16e65cb8","impliedFormat":99},{"version":"ed89af0e95cfc3c3aa05ab7f10f39252e304864be96702d3002e3a0b5314b058","impliedFormat":99},{"version":"6329a32dffec51eeb23377ccaead107d1ae30efe84e019e70d92a529df7e48bd","impliedFormat":99},{"version":"05650f29c7b38c2cf78950caa7386acbc7936a7cb08711bd1975cd4818c5cb09","impliedFormat":99},{"version":"12ccebd398ecb9ff4452279fc72aa34c70ff770926652dbdc7d34ad21777f466","impliedFormat":99},{"version":"1c4b858e912db3388433ea26a3b99b2c0ef72048da6946614463596b00298c5b","impliedFormat":99},{"version":"dd531d326e40577dadb38f2a931264139865f767bdd762af1e81f316b0c03a45","impliedFormat":99},{"version":"e8d0fd2600751d9b4abcb81e3067f1a6823763a25322401cc8a0578747692bf5","impliedFormat":99},{"version":"63964d043dc01122a1db457afec463743918a5c71c4beed6e2906f509e015a4f","impliedFormat":99},{"version":"4247f6bd3d537c2f7ee7a31067c7e95b70327bfd55a379fd96a42e9f2aa6d46f","impliedFormat":99},{"version":"e2ebacb3dd831a48a1beb9915bff463005845a398d999214a0bfba620602b741","impliedFormat":99},{"version":"5bf7ca566fde86fd0469836409db26264d98380002545a6b8ad048de46f47489","impliedFormat":99},{"version":"e860b60d342ee8666f83bf61339fa74fe7613f2ac7ed599f0f802ef3d85778a0","impliedFormat":99},{"version":"130de79e5e21aa52d0ac67eaa51182d90dc0354eac7c35a542a56426e93062cc","impliedFormat":99},{"version":"d590eeb146c65796d33043d08e48449c205b37033ac4157af8984bf1019928d6","impliedFormat":99},{"version":"68b3f5d095283a725fed296ee8cd36c1373a2476aec8cd9d8c953de792161248","impliedFormat":99},{"version":"55804c2d006cf28fa308eb3b608e5673ee4ef0e3d9999e532efcb9d703ceb522","impliedFormat":99},{"version":"e1dae322a476b97875adaa6cff6aeca7b3273e2a0fad1e175e329c4ce3d02542","impliedFormat":99},{"version":"36334b90aee2fe5d6f2971f05af58a15c68059aaa806e9a94652b324ea630e18","impliedFormat":99},{"version":"9e5ea08cabcd47ef88501f69a76fd8bcaae5149e6f92251e612e544af3f58a00","impliedFormat":99},{"version":"390b90167c23bdcaaa56041c620b04d9e85957f79d103a0b8a4f40f1159a486c","impliedFormat":99},{"version":"b97b7f29234eac44f2395e36457abffc93f7b709d599821df52bef118009a959","impliedFormat":99},{"version":"d8a29ab9ec8a59aa4f7a46217dd76da3fe9b98cea738f0a3031595e7e3633106","impliedFormat":99},{"version":"1efa9583067ef597f832bca0ebdb5a1e2c940d4b67fcbbbf4679528843e38834","impliedFormat":99},{"version":"ac9e0128567c5676b83e5364abc4230a6610fb9bbc698392b5ed197b99d95278","impliedFormat":99},{"version":"d8250e45341b1e98b42e3008bae572623c2690e331950ba0a7b6c0ce6cd59d50","impliedFormat":99},{"version":"7ff384e556272b468d2710a54ddc17eea161d98dd44485d0c0d4161ac7e7da85","impliedFormat":99},{"version":"1388631fa8cdd249a9812333f0c2ce35490055ad8013972eb920e123bc5eb8e2","impliedFormat":99},{"version":"ee6b73c727bb809d7f5e6e22b4a668f466a5ab65ee1b8f214f8ccc3e311a2451","impliedFormat":99},{"version":"35fb1d611af3bdd39f5d5402847167af43f150716a477de3796c7c4ed071f004","impliedFormat":99},{"version":"708088bfcef2939f193d1ec862b1e576a9f345638600a9e82e398ea6f2e006d6","impliedFormat":99},{"version":"e66d27a7e54d9cd5e3bf7acb28838419aed6cf78c8ef567b9ec1427fac774f56","impliedFormat":99},{"version":"d19cd315889739ab3e1956053ded5eb062348ba8cb9891a661d0186b91770f21","impliedFormat":99},{"version":"a0eab628c5edf5340163c01613f55ac9fdfd5186c52d96372528c576152f3795","impliedFormat":99},{"version":"619c675e7f67b1343992c6f35c4ec9a52fda78bb025ff72519089a6da62d1a47","impliedFormat":99},{"version":"e190626bea4b1e20d7b3e830606a43aadc25815806fa1527d527e961a35e8bcb","impliedFormat":99},{"version":"94df71ff0eea0e304a2ef7722ab413d75adc8a98e0c741a0107643276560c7dc","impliedFormat":99},{"version":"1a6e320abcdea25b8489b6e39cb4a4d8c90d0f0ba9f597231e95420c723b9dd3","impliedFormat":99},{"version":"2f838725d0b114c8aba336f110037c0b00b6248958227a74d80fb62946ee5b18","impliedFormat":99},{"version":"c9dcdbf46d97fb2df29586c68cd5ae6748c081a0c53fc37a6132ffa27f6955fb","impliedFormat":99},{"version":"8f430a46820c2649bc2d07c6febccbec75be9bfaddf35bf264590f8191e94373","impliedFormat":99},{"version":"9b110210c3df13bd4e53db3576f4416c4f56142d50b986f46e39450c88c6b780","impliedFormat":99},{"version":"c2d231b90096f0d5dfce494e54bf81666758316e087781f14bb6da1aff9e8700","impliedFormat":99},{"version":"1e1dade75405e843690120e917bf189f5a1a8dcbe60caa2815d949dfa4b5d5be","impliedFormat":99},{"version":"e7c7954921e22f5f8945230f89cb777668b059a7354b350e0e357987aa0417a0","impliedFormat":99},{"version":"3267519b49ae97bae8a7bcc12936ebb116209c6aecdc9f15234b0160ae66300f","impliedFormat":99},{"version":"8d5680d71ff8394d96be70eb3e03ed4332e1b7e5191502aff1576900c120ca16","impliedFormat":99},{"version":"60aa61caeda93417fe0d1c238def5ac44869ad867bc779a82be7c9a1c06d4734","impliedFormat":99},{"version":"2709edab013c18a69f6bd497d4abddf8368a14e0baf1bb30ad5d0df59bf42693","impliedFormat":99},{"version":"c5c079a529300647828ea61619a49261b416ae65f46f968511519c5f8a79489c","impliedFormat":99},{"version":"a746fb3152e96412346ce01d24d5ed2817daed4ea188016d4aad0eb486d193f8","impliedFormat":99},{"version":"9b5dec24186dbc05171cb3f9e4f1e191154d3d4b7f187dde95ed0b08bd38c32d","impliedFormat":99},{"version":"c21c62c5f7e9448de7e9b0fa9f240d5febe46ef3f170ba4f976b24b4d2270cc9","impliedFormat":99},{"version":"b0f482d75e79e076a84d8f83abe57cf80d75bfe4f64faf96e0a6c965ca6e2a6f","impliedFormat":99},{"version":"2d72fb98ce4a52674d73af8bb2afe471cc480afe5de81e4e1fa40fb576169bb7","impliedFormat":99},{"version":"5ad98fb3cfd67c6c38af8bd2761a605f9d911d6e03955fddb0c7fb7cf824c694","impliedFormat":99},{"version":"67f46739541c08dc52bdfdfc8b77534dd9443e7fb4bc31467a211acc699a02de","impliedFormat":99},{"version":"7d8515bfa58add7868c423b29c77598203e0c7696b7345d933f4dc8b04c5aec7","impliedFormat":99},{"version":"6bf2b0fdef53d2460e7f49b6e7272a25b74ea474c052275a9215f645b60b0f64","impliedFormat":99},{"version":"94b181dbd37f644090e1ef29852d9dd6368d8874cf2f4c0ae0a298feaf991eaf","impliedFormat":99},{"version":"2d74c63d20d7eca1904f26dddb4d8b8d8fca12529bebe4bda7d2d14d24bde067","impliedFormat":99},{"version":"eefefc2f2dfa29e2572a60421e45dcf8530a34b17c3a6d43c5799a5ef0550686","impliedFormat":99},{"version":"8e6551df9921ce8c6a3b4fb520d007e28353a0d3d8a1d06f6778c6f1486207f3","impliedFormat":99},{"version":"68954597f1d7f67b99005951e94ca60a3a62bf3913362f2cc7cc653de3f19496","impliedFormat":99},{"version":"4773ec8c467186c29116dc3c3cbb7927437de3311a09b052687c3dfe067c16d7","impliedFormat":99},{"version":"0c710c1c886c67727bb7deb8197d009a9146b1f5afaff62b7342acc76b73f279","impliedFormat":99},{"version":"4d62901366dbbd759da77f66c16897747f70a2d8dfdacecbf8606e668cc6c301","impliedFormat":99},{"version":"60b81bc5977f284d06c8c89cc04b8cddb45ade80758d1a0455c30161b34b437b","impliedFormat":99},{"version":"56e436146023b114fb661baf70bfce2041d5806721935746699df3d1331fc6af","impliedFormat":99},{"version":"7cd4bc26d96221eb7fe5fa0c1b503386377fbcc6757d26ec50014e176a61b7d8","impliedFormat":99},{"version":"bde451f81ffba547053111934cd0ae75bdde203ddefae30976b77033136de1f1","impliedFormat":99},{"version":"775d6a8112d08a9c5fa5d2cb8aa97881ca81cd2344f6e9e158dc81efab171b07","impliedFormat":99},{"version":"4cd295e5f329970c707fb48b9d88d15b32faa9f760fcc977ee10eb160f4e4a6f","impliedFormat":99},{"version":"fc4038d8a91384abffe294d3df8c3a06e3df7073bcc2251272fb31d912e6fe2c","impliedFormat":99},{"version":"22bf99ca0b3d239d88d6d8e6e2bf155afb30b4aca7fc09335087938467543a09","impliedFormat":99},{"version":"cd3682274f378c2462bc4fbb41ccb5300d71cc5c008c12fa9d35ccc54b34cbca","impliedFormat":99},{"version":"67dc58c0b812bd006ecabbd29e8f9d762c41b0549910fa6991411c16e95bbb59","impliedFormat":99},{"version":"2bc40ddafd68113f9625ab18be76b9d69d8622534674ba26e32123e74fc8667f","impliedFormat":99},{"version":"9c099d02e2e82d484012f3713a164cf55c7f63e5cd9c2fda4dc4076648ab862a","impliedFormat":99},{"version":"48b3074e05dbe986b0228044afa1ab2b130f562920e20ad6199c3219b2629d17","impliedFormat":99},{"version":"c9829c5ebf26778156e33b866eacf467eef9beda248f32cfa5042cd2ede8de50","impliedFormat":99},{"version":"658ee70a53b7a4f30af822940b5863208e367769715698fff58d0f38939ce858","impliedFormat":99},{"version":"ca1bca6d1b00d79b032c2dbb48ff021fa7eaed6b0aacc796ef5d0873933891ce","impliedFormat":99},{"version":"bdde7f266379e321a38646d74c7da791308d1de89073299a9d98e356634f7c08","impliedFormat":99},{"version":"dc3b92eb315796d2568e17de9dbbcf25796565d440e63805642fda5cb68b9baf","impliedFormat":99},{"version":"d486d1fb1ab86280b3f8d730a0770d5843cbaee267a13748c210a49d5d8701e7","impliedFormat":99},{"version":"6a1edff3f864b17879bd05bdf8dbff682d14d2b701892256b74da0693616e06e","impliedFormat":99},{"version":"1c3a5421df2a1a0f71e86ff5aabc804934ae48ab0495acb257324b713d2e5c3a","impliedFormat":99},{"version":"6d604fdd569f7e30ff284c132db9b5185c8f9c80fe405d23e2b07c0e663f6278","impliedFormat":99},{"version":"91e3013fbc9ab4ae9415de1dfabd24b20eb1a448a8aa1227f8a1206975626585","impliedFormat":99},{"version":"e811d3bfe878fb14b09ee7746624751e7f861369ab6301d6e3b340f6c5209873","impliedFormat":99},{"version":"c181f4ed01c4d578e7cbfc15121db9d1e53d3292e0e674e89b8def9afc33f2de","impliedFormat":99},{"version":"3aaa8b4924cfba7d4dcec41271cd848ea987093376caf1da8cba263b1a8f604c","impliedFormat":99},{"version":"5c28b10c7829e6f8961a9a03a600e3037ad4d9e005ba3a177ce88c76cad419c1","impliedFormat":99},{"version":"c93bbc1a968312df22be64d3fa283d9a8794e1619f5413b134d7543884a26481","impliedFormat":99},{"version":"3367c741cb20d44594a33c81189e92fb6312ef1ad80e731e8c3782ea7cdc3b50","impliedFormat":99},{"version":"57da5fcf4facb95e01fdebff97414e51417d12d61948324cb1a504959055efe7","impliedFormat":99},{"version":"b9171a4741909269fe324049b8a2c560aa829a95c1d1770be18c9d137f820304","impliedFormat":99},{"version":"f05eaa4c4151749a5994f68172bae0b621e4000e6763eda011b802f5c675871a","impliedFormat":99},{"version":"d59306afb4025cddcefbb888fe9b5e530f36c13dae04f045d783591db4a09af1","impliedFormat":99},{"version":"a5cefa0fba77455d5ae3fe0b49edaeebee2ab7a02eb1fc3f2c1d1ef0f89e80a0","impliedFormat":99},{"version":"06dce6f582073c6fd71ea95e87fc30681c363f0136aa26aaf01b5072f8a747ee","impliedFormat":99},{"version":"7f39fb5640073a2c8292f58c332b8ea1dc40e6771e82485353ff5c01de3004e2","impliedFormat":99},{"version":"eec3d5956e9cdbf258c3052d1da1be6a5414ce9b925adadfbc69772a0f560ea5","impliedFormat":99},{"version":"ca904be0ecc01a5c6db3cd367b323989c367ec5a1b8fc8b290d566030350af63","impliedFormat":99},{"version":"71be24f0406ad4969187a22b62d5a5bca07a9e69fd4695332b823ba50b7221f7","impliedFormat":99},{"version":"dba595f82dfed85bbf4e39b50e27b9c9f52c20a48c72036dfc2ad8658f835bc2","impliedFormat":99},{"version":"81d42dccaf051e8d599b1d70281c410a8a501ac6ba6462dd95b66e06bc2086fd","impliedFormat":99},{"version":"2ce213f1fa2a9d38a4320272de78bb70e7ed1285090783732457d42064a66ed5","impliedFormat":99},{"version":"bba6e7d4458c3bc24b69f68a5152963eb6e9dd72d38618f83b3698153f635d45","impliedFormat":99},{"version":"0e7650ed5439e7d872df4e6bef9d75606da9c73a0682782f52c700936088eaf4","impliedFormat":99},{"version":"f101801894b206a26d61f2b84d1bd5587441e47557bd946f7d651fe0c6ee1f94","impliedFormat":99},{"version":"f2c1d36ed7cb9f429874c7865ae1d193bb27bf0669829e50bccf4bcb2c6ef300","impliedFormat":99},{"version":"d90c065d25c7043fd9b8d3d44cbc3f082b048a641d491ce27812b97eb5545a29","impliedFormat":99},{"version":"6a81e1ba677caa156e6624eb2ef502cf5b3e87cc259656ce649ec39e9ee97606","impliedFormat":99},{"version":"dce3724933c25a776d212550af0a1da5655035339daddc4ff9d4e3eb22ef13a1","impliedFormat":99},{"version":"7fab59a9579956454a09e5ac8001fd78365e650db43364f6aec5be52ae8711e5","impliedFormat":99},{"version":"b2fccbb91f9a6c85e91da363c4304c76f8a7b26db84f7f1bf8dbe5ffb2d49e50","impliedFormat":99},{"version":"bf890587b717c5562425aa385b021ba18900eaa0f176e3e57f0d1016b647d32f","impliedFormat":99},{"version":"3428eb55e4db8b21337d9bbee986cb297e3bd788e2405f02adcb37ad371811b8","impliedFormat":99},{"version":"abd06f3aee8945b159cf67d05a92ca23d2b35a1163773eef91f7cc2618915ee0","impliedFormat":99},{"version":"c6ec8754b8ce679004c28bf2cb0f2eaeaa8de0acff94ba5082fe16ce642ba92c","impliedFormat":99},{"version":"a3005d619de5f9e5d0208596083fc7110aec861b8dfa18f681395c216cc95fd5","impliedFormat":99},{"version":"5c3ba312c2cef70f54aff90e59fba9b61101e1e941c2b3970c1353c453f25155","impliedFormat":99},{"version":"cc8d78d412e3ae3997d0075078dd2787d842b6023bed13a6d9f3e07bb1e7d073","impliedFormat":99},{"version":"eefb675743146703d1c5a1b3e8171b4b95ae65c41120425312846f90c0132017","impliedFormat":99},{"version":"5928755ef02a97a3831fe4c85f9b32aa157d499acc21b3724c911b7733804c89","impliedFormat":99},{"version":"8627e628c30fc555b119326c656a5101e92548b05ceccbf38d6b5916062fb976","impliedFormat":99},{"version":"184c2412e3d88d7acfe70bddecff9004f4610afc87eebe042272f2fb7e0d3c89","impliedFormat":99},{"version":"6f9b9e4b4f44fef1d0b98eb1bb602345f0e22e0a137ef40a615503d4e8b35bd2","impliedFormat":99},{"version":"cacf616e3fb3ea7c2880cf4d1feef47a3dbed29b0bf248b0bdd47fcd8c8b5945","impliedFormat":99},{"version":"48eb53c96dcd4745c4c8bee4d1104ecaa6a877eefb8a5b57af0f7ddded949a4d","impliedFormat":99},{"version":"e0e35716709f99220dee1be057e8f7f08e1d8134c94ad414129b79cdd9b41f8e","impliedFormat":99},{"version":"ea544bfc5e4ff015c70ef62acaf356dfcbf1f39f10f07ca3f414278094e56b56","impliedFormat":99},{"version":"7d439735b74cc5a3de2a8855fa941341dff0628d1fb2b5f2dadc10a5bb6d9fad","impliedFormat":99},{"version":"e12142b93d24c7c67fca041df35d357f21d3751eae8b2afc5684d49630d12d37","impliedFormat":99},{"version":"b1bb63b43bc374799ca0e916542b0e71a7c8bb06d6c8e5f94848a8969536c933","impliedFormat":99},{"version":"d714669ac93d063f5ac5a0dc8136e796545c6b1358554ab423d07fdcca0bc7b2","impliedFormat":99},{"version":"208dfef5e3aa4e176a5112b91b5d1ca082c28d2eb33bceb82c0261ebdeee74bc","impliedFormat":99},{"version":"8361e095def6c5f9fe7303b341c4b3249e82354f78f8ac9602a076fc5a04e3db","impliedFormat":99},{"version":"e047505265ecef24ac538e166898c74fe4ddde9a208446b298ea9805a1337ebc","impliedFormat":99},{"version":"13a9d63fe4bcf62c0115da6fce6ff28caed592d7cf3718bdf2852616a9318304","impliedFormat":99},{"version":"5dbc1632a49ae9232d2006a9283086c23676a72c2869348fab07a92ad25894fa","impliedFormat":99},{"version":"3b24a42d6ff7eb9fbdaeb74d38c2bae20b455d9ecea76759c3bed2c9a70d3b9f","impliedFormat":99},{"version":"ab15374738d77b8203d5054b306e883bc12ae980f3de59b9bb2ab3c299eee15f","impliedFormat":99},{"version":"7fd8a28b8373c1ae0a9690ea4ad8ee96da351b2ddf9fff8f0fc33ba81bd49615","impliedFormat":99},{"version":"0dbf9eb173a832b643337fab8eb3153f2f82d40648cd9d0b654f37e469dde44b","impliedFormat":99},{"version":"a69420221cdbd10d0e9298b3b94d53a633606634ad0357da5cc541ff814d8c5f","impliedFormat":99},{"version":"e706e16bc6c83d4a60da03d7a423048f244423ed55064134d0db9a0a5d1a31ef","impliedFormat":99},{"version":"b11860ace9980039b9a876c8aa763228cd2eb7dc1ff1d4e4b6791fefa8270271","impliedFormat":99},{"version":"a5f1e4c5671abc235aac21bd99603fd151232360064d2622f6d21b592fde4cc0","impliedFormat":99},{"version":"a026df81b303bd8a8256705e4ba7f7845096a35a20885403143ecc25482fb5c6","impliedFormat":99},{"version":"3ad573ea6506a5b47d89fb43f6e7b3e24c9af3dad66408349bfdbea4d0ec9762","impliedFormat":99},{"version":"f08d5516ded9072f6ef4a9bd95d4301b7f168afb484dfcdc3f9fa0032fff1406","impliedFormat":99},{"version":"3532ffeb99fb16886cad07caf0feb76a19cc1fa14a648ce6e6558b04ba91bdcb","impliedFormat":99},{"version":"825a2b8b6cac11eb3228d4cf75487a3c4ed7daad1dcc1fb01c2f67dc75fa6e2c","impliedFormat":99},{"version":"f6976b6c4bfa0dd945409221c74e8adb9ac21110e43e6de6e5564d71a4faa010","impliedFormat":99},{"version":"3ba19528d73345e791808428a0ac7f251f3a03075f73b89393d5b7df828001bf","impliedFormat":99},{"version":"ed4fe0fdd2dd66de8ce064556270713b73bb71126c016db3a755a1b9ddb2b079","impliedFormat":99},{"version":"94535dcbd25f61962f2b2c9a36098001f0529186fcd9aaefd804bc7c3008e0e6","impliedFormat":99},{"version":"a3394d25f46ed8a6ede20cf2ebb07cad72e0928987c437dbe0a80799177268a2","impliedFormat":99},{"version":"979b1767dec45cc88bf49235cc88b3bf84f9072e0cadcaccabc3f51f37dfb4ba","impliedFormat":99},{"version":"53538d2fe5ed0d17a8fb3ed894b35590af1478a2864273f64ea153dc931f5137","impliedFormat":99},{"version":"ceed655ca8647e687782de09d86aa52aaeb71a04db53f7317b294afd98e745ec","impliedFormat":99},{"version":"a19f58bc593383ee611214b5aff06db6ec0c9abff34fbde4e937d56db230fe2d","impliedFormat":99},{"version":"12f07c1ca64c24a16c6b863a1b39053ba466391a42fb48fcdc731b5fbac19c2f","impliedFormat":99},{"version":"8fa98834033c5bd66d016570ad710aaeae12014b87799f57d8c107931308da75","impliedFormat":99},{"version":"5540adb5ed9f7b2a3b93cbae6ba4cc15c9c5aa0230f0b2c4ea65df9fb2d35ade","impliedFormat":99},{"version":"237549157534b24eb1486ad6338a5b11ad017da0b2a433597ca0d62c3978c166","impliedFormat":99},{"version":"14f449fd97d14b30b7d7e74ccb5dc52065397f5a159f109a81de462cbc2ee735","impliedFormat":99},{"version":"1da1c5e42d173cdb28539057403afbdcdba7ba70d1ca88131d478a3461146631","impliedFormat":99},{"version":"84e2defcc0264038c29dc40ffcb934623cf2906e721546276a11749645d650a9","impliedFormat":99},{"version":"981a6d13fdb1a917808b2c0021adf15df76b77d15ce32942f2567dacaa636818","impliedFormat":99},{"version":"8f2f419e0fefd82149f1bee26ef5962d27a2bd1c0ef5652f4558d603debd0cd0","impliedFormat":99},{"version":"20d839e1a5bd43ca94fc3855594dddfb4b04051ca87a6c0f61dee0d2135afed4","impliedFormat":99},{"version":"1327881b3a0726661b36d1d011b4722b301f6dd1d428b82d78f1bf94d7c5a1be","impliedFormat":99},{"version":"3776840bb69fac8d6715b70c236bd06b99ecdd2c0784a2cf0be7899fb7be13a3","impliedFormat":99},{"version":"ff224ec5ccda457a52bdf0747d643ad6029bc7ac8cf898f94047361fde72291c","impliedFormat":99},{"version":"b58c384659075652971d0c6cd25ad07ae6f193ee37820bd8daee6e3436a8aaa2","impliedFormat":99},{"version":"daecca2dbb88b1b1d759e2172245e8187180d777c2066296762811a33703db67","impliedFormat":99},{"version":"caf1e76335a50ba27f655209a1089e342e44c10410c834436308266cae1a0ed5","impliedFormat":99},{"version":"a89c789678be1cf65bb4125b15b5f74e25e34c138543438d57f288d988bd80a0","impliedFormat":99},{"version":"d26ae4cbfa2957baee8a6f2987a82e597f18ad98e8d08f5b6c627e2abc5e2630","impliedFormat":99},{"version":"b640c9b2090fd42bd34d877b4da63b984f77151e8492d804a26353258e28b7ca","impliedFormat":99},{"version":"c36739f58aadbf09f620712c3531a0bdf944b16b9edcd16a6c1f3d7d935f3f5c","impliedFormat":99},{"version":"94617f973c27753e95a3f78cfa2298ec16fa4c1873360124b3fb135f94d203e9","impliedFormat":99},{"version":"949107fa4b339e34b9fca78611d63776cf5952927e500d72bf108fdef03d3a42","impliedFormat":99},{"version":"5a76d9a6dd9260f407a8dd8657179d972640b1f6736744ff397921ba891d8d99","impliedFormat":99},{"version":"309757128a194184868dbda49a9583104e10d5f8ec0b144b5adea8faac559446","impliedFormat":99},{"version":"d22ca62641feb3cc9320464b4f1599d02791ae81c96b1d62d52cb58589a8aeb1","impliedFormat":99},{"version":"5edb6bbdd99524132182d829a58beca3da66609cf5d4ffcc79798a293b77b41b","impliedFormat":99},{"version":"3dea506a24fccaa833a3b7b154751904398ab945f7507de0e99563509a0bd886","impliedFormat":99},{"version":"44674355dd062213ce67cfd3ef343c59835748de456b5cd2b13ad92531ab5dac","impliedFormat":99},{"version":"a851a0369c9c4ee56abe7ecbf3b73841f4731da108192887bc74e3a9eb3289f2","impliedFormat":99},{"version":"9760ba531aa6e82a8e9a60d101b65ddae9d398e2f91770595d9f1c5f77931e9c","impliedFormat":99},{"version":"e2b27cc682a1368bfe175961bac462ac908de1dfb49590eea5550fd5bf9a6f29","impliedFormat":99},{"version":"49b088cac71a031ab09cc6d68aaba7b57e373693c72ea56dbcca094493a543e3","impliedFormat":99},{"version":"e546f06410e5259e513d9c85f3ffad6074634c223dfedbcc6fcce144176303bd","impliedFormat":99},{"version":"c049f4fe2068d1f7954e4fed057623cb870969eaf5a71bc77b5a677bf5af3f24","impliedFormat":99},{"version":"76cf68162903a40eddf28199107e921b9c08c033eac1c07efd4dc979fd4884e2","impliedFormat":99},{"version":"41c111ea5e8946726775a71c224438a7a7009329d1b6119b1c8dae82e433f6dd","impliedFormat":99},{"version":"305af9c2a8c308a4f430c3f2b81d68119f4920ae5942c3109d1178d879a028ac","impliedFormat":99},{"version":"d0f0427311a816aed0f31eca5bed01d2c720ef0c104f8b6b62c86101556f716b","impliedFormat":99},{"version":"a5bd6ae20ff492c2ec9a0eed17d73cc2a722c09d12a8cdf321d74c40de279686","impliedFormat":99},{"version":"ac25bb824a57d31b55be466a44ebe002841d19291da64cd6ae7ce6525b7a7b04","impliedFormat":99},{"version":"1c51722bdcb3e6bdbfd301fbaf546638dcebae9e6a02716e21373343096851b9","impliedFormat":99},{"version":"49cb95b8f3503bf3bc2481d747603e5ef2d541466b96c4b349c2296def086882","impliedFormat":99},{"version":"cefd620019820a3980cd72b3e41af68a97e42e095026f50cf95e6304307cfe48","impliedFormat":99},{"version":"ed2be494a8b56779518444accb7e616f85dbcbb19043a5adfe2b2ce9d92b52dd","impliedFormat":99},{"version":"79fbdd164ed7ef0ee1d9bdb3f65ae228d6c66aa915a46154912164717a97b96c","impliedFormat":99},{"version":"c542323dc44c3c36a1576c347164ad2167b4fc8c13da9610c7cb866786637c18","impliedFormat":99},{"version":"55b13f885a41b6ae500f75d5c588d05f78bfdb471d9b04cce391aaa9a3f9f05b","impliedFormat":99},{"version":"4c62dee16a75c9b0ee2b7ad080a8778c8ed8392e5297c9e07f001148b59f5ca0","impliedFormat":99},{"version":"48eb9347a1f264669519d10aaa422de9a6053b41b3fe84911ea8898a942b922b","impliedFormat":99},{"version":"afc496c3b64cd9d58d63640845132e08e48e05a29ddf2f1aa23cd46bfac3a584","impliedFormat":99},{"version":"05c8ef07d5dfb84772cedee2faca46c8ca54d151c16871a6643ede5e208daf05","impliedFormat":99},{"version":"24ffd8a17ccccb3e56570ca53ddd02e772f0b6a4ad3c5616ae642a7e508b7cbb","impliedFormat":99},{"version":"fbd75ba9061b448f015dc71a9ea3891ea17d361e6106ac1356aee358420a8562","impliedFormat":99},{"version":"4ee579b9d37f6198fbc1d3eae2aacda3b7ec6ede001f455e6ce12aef07d5ec59","impliedFormat":99},{"version":"3d4a999e994018e2440e6dd37afecaa4154d616052df8a28e2f1a2259f19b81c","impliedFormat":99},{"version":"db1521b3a21573cf7a452af203effa3482cf0a5b8454e2af4a1f484306a64974","impliedFormat":99},{"version":"45c4a138c13303d5fcfbab589251b7869887f7eff622a2efb7925e2ed7797420","impliedFormat":99},{"version":"1b14568ba50dc5776828b4c6e1be1e7b3aaba855680eb3ac9b07c8a00414373d","impliedFormat":99},{"version":"34091b6bb864d275a0c9080bb1e72065a616d176d3eb00d0402326bf83e206ff","impliedFormat":99},{"version":"11b55793b4507c2cc763198427e11a0bd5e931aee36e265bbbf21632dbd4872a","impliedFormat":99},{"version":"e831709cb7f77e60ebdad8982c5751ba745a77573cda6f44f4ab8d4027ae88be","impliedFormat":99},{"version":"c0defffbf81eedd1d7e9f54b434866942c2ab6c5381f8cc6d5022dca5888d07f","impliedFormat":99},{"version":"e8b639c2a91e047787f660a9dd3895a2ce1200fcda7560841d5637f9583c8db2","impliedFormat":99},{"version":"767e0b38fc031fdf1ba7fa253f264912774e67d1d52ce4e852eae8c3985ba6f1","impliedFormat":99},{"version":"8d37af9291a8ac73663132c067ffb65e649254333a9dc86a1b22361fb88cf7fb","impliedFormat":99},{"version":"8cbcd14fda4ed21695f26c27018361772c9d5f754c702620f42436f0f542c1dc","impliedFormat":99},{"version":"90d4a3535371f8c168b88ab0da2562f754810e26455c95698e98b15f11da22b0","impliedFormat":99},{"version":"12df154d3dd79dc0a4b71fc1e3c5d5fbdbb6a5dfd99c68783020e56743fd4776","impliedFormat":99},{"version":"4f99756bda4a08559be8508b7e83a5be0a93dcaab68790310abc4fa09b93a849","impliedFormat":99},{"version":"2349b5b7d1a01add8c3f99286cefad821143f1df1aae0867cf813f02237a1728","impliedFormat":99},{"version":"09e449359562a9ccee76260902e48260ea75ece6e8d17abe9e7593d900a320eb","impliedFormat":99},{"version":"a6a33dc9ae9532283825868ee5af9118ebc26978ced995fcbfdf90ff7c5e2f0f","impliedFormat":99},{"version":"dd4de144ff9e9db9fd3dbca7e1fb7f639b39ef953f2b3c49240c7f2a9736fdc8","impliedFormat":99},{"version":"a0f9aae8392286dc83d0712328a0c066ff43594158ff67606b8f9c1aeb69a823","impliedFormat":99},{"version":"4c9dca53c43c6cde0627e9851dd56d958d6ae68562e2913cc3abd1ef73f9a00b","impliedFormat":99},{"version":"6914eb2bebd7fda4f9fe8db98ee09f5554a1ced9684847b3e27dbefbdb87b1dc","impliedFormat":99},{"version":"dd21f9d9947f9e4a773809cc37b0b094607afdc83cd0e79f2d97250c4501b6c7","impliedFormat":99},{"version":"2066f78e539fc3e9b9a756cddea61af372d9f35be7ffa9497c6704da56c9e61c","impliedFormat":99},{"version":"3d3fa0b2c95631d01e1291b98adf0423f2a74a87db32df8fe0c062b6db6eabff","impliedFormat":99},{"version":"bab5341c5fbc115a91a38bc1dd18a3b8f9e9f6601f898eed8bf6b56d2469b823","impliedFormat":99},{"version":"0d1fd7906b3767ccf874e84b76817b71c81836b02f9c5fae2daade6fc26f1b2b","impliedFormat":99},{"version":"828d97f23443ff54c85832f7fe1bd2aa204cc761193f2fdb95391c3af2eddb4d","impliedFormat":99},{"version":"47aa55962c8eb1bf8f46e84aacd04d89296bb9ca3a2978382cb81bc4f1c726c9","impliedFormat":99},{"version":"6df68fc4f79f293f1ab2d25951f191161ba0b13ab2c06b9f0dbba5513c5a0e1e","impliedFormat":99},{"version":"cb6bfbb5f55783ddb18adfce848dd24bcf85bb777bf77a0d71dd773cc7b62c22","impliedFormat":99},{"version":"286f368d27af276cb55d1130452b98b9956bd59d142a62b514ee6f1f98395271","impliedFormat":99},{"version":"b046da4f4d04ae2789837a9c1c32896d2d99256655b4377e1e47c0945288f9c1","impliedFormat":99},{"version":"0397b33949bba44c603a8919b1cec9428bbf84ee3c90819b080dfbc764e01724","impliedFormat":99},{"version":"7f971c8a26679b60b7765f1a9da29b987a93795909acea2896b2da567b2429bd","impliedFormat":99},{"version":"55db20deb5ce62732518b1fab73d5b3609113c027f58658c1f3104f8c9073e8d","impliedFormat":99},{"version":"279792abef7ddfc2566cf92e89447f0f2fbf3c5e1ce8e01a2325d6b56e513317","impliedFormat":99},{"version":"4d1276fa5da13705e9e23f77bf97aa8686c37670d3dbd6e85d5975387da6e9c4","impliedFormat":99},{"version":"4d43f7782e2ae2d5be0449e202e894dfdf0b5e8fcd8fea275b1b1795045ea3a7","impliedFormat":99},{"version":"4c7e9a01365d12cc787fa964cb28503b4bad69cc773e25f883ff53fcd049237e","impliedFormat":99},{"version":"791d109b55c4589a49c89729aa8df87d88ad546a322a4364ee3770a845f93a0c","impliedFormat":99},{"version":"ad2aadb0857cbc6b0a1177ec6b8944b734f5c6c9481baf77d7f3b2c0fcc5b2f0","impliedFormat":99},{"version":"319d7c7aabd2bacb683985cadfe402cf1ce13185241638ae98ef2467c111717f","impliedFormat":99},{"version":"1d8c74c5747df6706dece004ea4c537ed5080e95b90d1a1620bdaf46bb8d180b","impliedFormat":99},{"version":"2e0fbb883f847aa1bcb15d265757653f79d19fe679c2b84adc5b12a1b83371f6","impliedFormat":99},{"version":"e62d894e8ccc27d596235f8cc055ce0d27772eb13b310577ad7c2bc4e2f2b23c","impliedFormat":99},{"version":"bbb48c9b123ebf986aecbdc2085387b279b9db9822a99075135c164d4bc12c99","impliedFormat":99},{"version":"f46ed0abe3275015dac23dfc2f7c0d7aba8d7378f9dec9a0dea5f9565bd6f582","impliedFormat":99},{"version":"0dd3e4ae8e0f3470f2e5dc1fd95600e2f0753112d3a7e0f61225a81e8389a6c8","impliedFormat":99},{"version":"8fb8660a87df46177ea76db4338c4475b8b8e7e42e36bf124195489cb6fc9ae2","impliedFormat":99},{"version":"55f0aea7d8e5668fd2b74231106be4dd6c75563e8e794a76b8539c5320a3b41d","impliedFormat":99},{"version":"f42f470b9934ca024afac9c220e5c57b5b70de2383a821e3e7d58b5de29f6646","impliedFormat":99},{"version":"22f011193d780d619a9a4dc9fd0307bfc0c7869d496e371245d50b72a0e1d0fa","impliedFormat":99},{"version":"acbdb315c0433c6832fb64027dd421c1d193551e20b5da391cdb20c6b8f95dc8","impliedFormat":99},{"version":"d7ac7a29f5fc29b2b157fd2ab7738946597a29b5670c2290d4b81aada8fbf6bd","impliedFormat":99},{"version":"7f85382c44340cc8c380bfc3dd2e5c69c280308538044601788e2f1ae3e55a9a","impliedFormat":99},{"version":"7d7e9f13134199ed1b0cf6c8775a614c1bf4341ae74edade456500546ab2776d","impliedFormat":99},{"version":"f57d7c9fb29a389e44af09d54c57d9be1a130b80537af035334f9a8f2e55fa76","impliedFormat":99},{"version":"0f76779994fafbeee4d135c21fb87d35b81c6fa913ef85e5b6bccb3ffcbd57db","impliedFormat":99},{"version":"64c4f811d9ace920e8df2522f72905cdeafa7812166416efdd51a9350d361628","impliedFormat":99},{"version":"cdd9d75a5e91d288b1f009ed8d885e29dd61d04cd12fe66645a69e9364fc2fd8","impliedFormat":99},{"version":"c81859d7edd8ea8016c3ef669bde80a68a0a8e85440307d6c07c0e47e6e70a7d","impliedFormat":99},{"version":"a1d72f11de9816b4776ecadea48d9c2a8c486cd8b465b674647557653a151a0f","impliedFormat":99},{"version":"33b8b6eca734548828961d9ecc9c21f729f08bab8c2c9d95c9ce772db6431f94","impliedFormat":99},{"version":"1861836a3156845df6c43d92e7634515050e349eb569d5ee523e25a2ce6dbc55","impliedFormat":99},{"version":"4dade3290736a203b3e191085b91bc93c9a51dfe608211d314ce688a7d1915c7","impliedFormat":99},{"version":"558b4eb69385b53bb4f76dfb9944da2b324c97f4cc915f8a57ffe12adc08bfc2","impliedFormat":99},{"version":"57b6feaf806b2ba926d3000f59436460462d6b551397322dd983bca10cb82083","impliedFormat":99},{"version":"9a599074752221456c6ff3fab0a9e5e42318e610bb21a23c1364d12b0669d01d","impliedFormat":99},{"version":"17854000164e5e5ef1318f5f542b770d80b0374fa95ac023e01a91440d93de35","impliedFormat":99},{"version":"fcaf10eebf077d5a0e4be81424fcc296905533359c46ba1198c41e98e957d70a","impliedFormat":99},{"version":"71d9bf57ae0b6685fa69e800f44a0e040d1418f74737bb590d71374f02a14f5d","impliedFormat":99},{"version":"a3e8df36aeafd3a63629f85cf72f46be39de7f350af3cbc40bc7917f41b36861","impliedFormat":99},{"version":"86fbc8e6592abc2775bafd3c2b2eb5cfd0e37c4d57e0893523dea6d67c19f05f","impliedFormat":99},{"version":"14a976b28005d68fda91658eec498db736406afdd383a6a4e674ba2528815348","impliedFormat":99},{"version":"e6acc224ffaf9707ce0d3914ce46ed083c7b6469a0f195543fa7cf64b648d4fa","impliedFormat":99},{"version":"8ec434028fb805c17f244b68ccc992391ff8ec6ba0c4bed1d04e51741f344e90","impliedFormat":99},{"version":"e2a0a09c2e1fab39ea345ec351a93f93ce425d2c6234606e705b3a6c84b8b46c","impliedFormat":99},{"version":"e036c82ae9c6a656dc591ddd4f68f70377e6900e7e4042769d4cea7d215bca3f","impliedFormat":99},{"version":"248ec4811949f314a09307eae53df10b95aa35546a9e3dc6363a2916b02195c7","impliedFormat":99},{"version":"e435436a95eb581e852b35d6f5eaaa62be7a8bc6da288254ac3394c60511e5d0","impliedFormat":99},{"version":"5cc46f1e20556b157f798a8043e6447c88f118859810d55d9b845e245b17f2b9","impliedFormat":99},{"version":"20fa43bd305c2a0f27590ffb829d5897db5160044136e1c216239a8fb91cfc60","impliedFormat":99},{"version":"3c9a0663746013d1a7f2085ca9fd23d4a1d81831d105f55d83499643db339a6f","impliedFormat":99},{"version":"cbf9bfc5883bd84a49685342affdab376d54622d075c360c1e9ee0cb1a52176e","impliedFormat":99},{"version":"b67eedd415da9341688bfc4897f67951e6f6544130d339d631c578d9ce5179d4","impliedFormat":99},{"version":"74dfe23f35f7f780bc43517662cf6bf54e2340fba8c96499a9345f5c54310765","impliedFormat":99},{"version":"216e481f8b352b3381c3ca193245c9e70eb988f13b11c90560fce7f58046d9e4","impliedFormat":99},{"version":"442f979c95b63bc7545f20a9dbfacf969a8bf8d50f38ff5e8647883af962ea90","impliedFormat":99},{"version":"7ccf30591af329e39435375251583e00f458be6e9412ca78457fd618c3ed3219","impliedFormat":99},{"version":"0e4023053e2d67cb1875fd3fccb3d9719dd35aa7ed8dac2f8f0a6b83eac1c619","impliedFormat":99},{"version":"121d72cd64ce108c997080ae85d77526c0bf92cbabd595e85b22cc5b2eb5356d","impliedFormat":99},{"version":"2e58e0a3705c2c5e2f8990d9fe54a987140eb6b07d39969667fb5a61e0012164","impliedFormat":99},{"version":"0ac60f7dd28ae5fd26af8a26038bff0a0860f2a934bc3eff3508b9ea606100ee","impliedFormat":99},{"version":"23e86961cc70c48457a30b54d2a7cb321db04ec37a2ce709a558935874ef5980","impliedFormat":99},{"version":"813924ffc37d482abc63f7809402b6d04bf10ced84ceb0ce5b593f93000b0738","impliedFormat":99},{"version":"d39f8d34fef31f645d4a5036d21b6d76f1de2fcb002cd976c7efae530249d79d","impliedFormat":99},{"version":"3dde9915cefa4b06bdd06d0afacb2008400eee07fedb70cd4fe895be5d7c37a1","impliedFormat":99},{"version":"6fa7984697bbae60ab189ff7930691b20d214c1fb68ed6098cef5f616ac8d3d5","impliedFormat":99},{"version":"49cb62a7fa903a66787723065158a23455550d938649544b9afa0fddf13da5a4","impliedFormat":99},{"version":"8093af89ce523d6325f01b1502b431dea33dee468ea0b207f3ecb6b8e8c869fb","impliedFormat":99},{"version":"1fd777885f679efef7156857398a8e40a65f48f9d4b53b22188593e5d9a67f7d","impliedFormat":99},{"version":"381f82f83cfc6930ccbc008af3091f09a75242ae15d2e04632db7fc4e44c0080","impliedFormat":99},{"version":"fbca59f60b22cd0db89922e7f057c713b027aaa1e2c2818632fea718f8200632","impliedFormat":99},{"version":"1bd895f3a629b01cf0ce259e989e8091ef945cb39e29bc4f5dbb35ab66718b91","impliedFormat":99},{"version":"ba8d03eb92363477a46ccd739f48b8a87432bc39a4e0ccadb1bf336c290b103a","impliedFormat":99},{"version":"6ec0dc970053f2d727d22210393569ebedb4ec3a53c0a5e4ac0cbfa179814ef3","impliedFormat":99},{"version":"4220374c20d714335b11a60a1b0392dcf3c0ac82f0773466b731c3ff758a1fa0","impliedFormat":99},{"version":"ddbd6bec7bf000be900df2f6ad03461580cb94c02a2b237d601f091c7f749ee7","impliedFormat":99},{"version":"a4e2ebf51e2314ddbd330380d53527e3acbae6d1fb4919c35f6e6ec534eeca88","impliedFormat":99},{"version":"8aadedc389b7ce3087eb1ef11b85a79ab2b15a418ca0d469159091f33359096a","impliedFormat":99},{"version":"351223dcd7cc1c134826feb03e21e9d40d39497e4641f6640b5b5f118c731cc4","impliedFormat":99},{"version":"3b112575da3840ac278f8edb8898acb94b9c79df94b041b4eb17e3fe1263a643","impliedFormat":99},{"version":"902151bc4db55dd270dc9f38cba4f06c2d5fae54c519677cc3e1197335c58581","impliedFormat":99},{"version":"0b82763f479497a1517786b27d8826d3640d15af245804d322eff12710c5a652","impliedFormat":99},{"version":"c1109b59c4c5033695016057e596c8d34abf048a3e2c1b28169c21b22cba56b3","impliedFormat":99},{"version":"8a35fefe86f688f467c8bef9bf7b465e9be8e5b3476504096076e535127ae3c9","impliedFormat":99},{"version":"00040c315b5da891b7368217aa2d92cc5333f92ea0272ef70806aa5a16d642f1","impliedFormat":99},{"version":"d32d0644afaa7ab25fab4ba7f0c30e194cf211463a9100bf406c24f67573babe","impliedFormat":99},{"version":"9fac0a75a13381632fc5bd29ba84c5f6706c11c447adf8f6628a9ca519c2914c","impliedFormat":99},{"version":"986204e16e3297bca5e64b327e52f9fc4c65bc34497227f63b1a48bfcb333dec","impliedFormat":99},{"version":"824b4ef28c193ed47adf80450a0b8738313561707fe300eebac14ea2405eeb04","impliedFormat":99},{"version":"b634c2e561e3bf8efbf1b79060a9bd5b26022a71574900c3e124a14bb4c4c71a","impliedFormat":99},{"version":"76b8eeff48c0605828e2037ed6bf67d25675860a2f6a8fa1848d3930c2ae871f","impliedFormat":99},{"version":"d7ffc3f3fb6c878d49a92e0e90b737cb09642f7044d6ce3bf53f5698580d8066","impliedFormat":99},{"version":"5ffc2c7cc0be96e71688f7a0062f2e85c6e68b21e605d073a5a6c6a1f3a2a5fe","impliedFormat":99},{"version":"8eae28b0fd19d123cc1b99840528bb1cdf76ba7f0b15034da949d73aacb9702f","impliedFormat":99},{"version":"9b4cc49d65c87c5c38ac19676dc14aa914b13b66ab2f8c93c53a65272e0486a6","impliedFormat":99},{"version":"21c521798aeb2a82dac296829f7450a791b6f154b8a8208f77c0f7370d5f34a4","impliedFormat":99},{"version":"a383c2d014291b0810ebc39adec74792c53ac0fdbd1ec39c87779a8d07e7ec9c","impliedFormat":99},{"version":"635d4c2bbb40d953dac16ebcf52b123b4bc86e58bf6564d0dec062a4c489ee21","impliedFormat":99},{"version":"eb420edce35fac3ff390bbb9d2d98325db9c11a2cd60e0cbdfc1b118b62cf1df","impliedFormat":99},{"version":"f4d93034f0661b40ba9cf90d7d3474d5c0be65fb7f4f3a1d5e9c34210aadb6aa","impliedFormat":99},{"version":"1ebee1d844f9cfa95e5315d90140f87eb1519c8739e7a51111142d3838ce31c4","impliedFormat":99},{"version":"4f8e5dda56f05c96d1f567bb96f32e81039d21ac9af5b4d5fec500f090f21388","impliedFormat":99},{"version":"6f74bde7fdc1ca03aafef483d4f1b0ca50489094b0ce52ce5741c71635494d8c","impliedFormat":99},{"version":"189a993d40cf154596f0becfce65d93b0e3d8c62a1852b9506d86d68b3702d88","impliedFormat":99},{"version":"c1d358eb44c209b7b5ad87abd6d889e28513718dabb6802de8844e517a89730f","impliedFormat":99},{"version":"dfce10bb8fb7c37e9625e889e27519567aa36329ef0b42094dfeac141dd45793","impliedFormat":99},{"version":"dae38b8a375a2e3f8820861c302e675fdeb6981276fab61ad1668c7596e7deab","impliedFormat":99},{"version":"4967ae99536846dd5ac9f3af41ad296b9c36d611a52d7b00ad686304bd23d71d","impliedFormat":99},{"version":"e053193d1742f8702be0bdfa544ad435927557f7ffe67a23ef92b176e60a7657","impliedFormat":99},{"version":"4f1c51f422685db98ead37cf10e723379d21cf990db0d6fb8000b48ea6c254ec","impliedFormat":99},{"version":"951543da1d33a4ed5c53714a1f4ec35b51f8a1c61107649c5ba6d9a1d2146103","impliedFormat":99},{"version":"d763c971f8591a21a1f01c101ee8a769e52e412fe7b1a455a9ea71dc88a758c4","impliedFormat":99},{"version":"b4d7cd9d4092923da05ed18680138dfc72f236181f5618553d75e0ab06f67964","impliedFormat":99},{"version":"2784337e4229d64fcfceb6c4ffebec4d7fee7ac312a316f8eac1a4f760ae20b3","impliedFormat":99},{"version":"63db0b6285f515f8ae6397e307d3b26a56a5249fd80bdf7569ab4b87aa87ca55","impliedFormat":99},{"version":"7ba72b7633d2d2e507f07a59027e24505577797753965b575416ce8e89a04270","impliedFormat":99},{"version":"1762e04cb04fc69364160a0e3d121c63b85945f842575f5e3abc0392acbdaf81","impliedFormat":99},{"version":"b96cdad7e7b0fd253213617c6f76f79444e6c151415584d9195266a9203b4206","impliedFormat":99},{"version":"71663fef9e157f5257a31408ef5aa7fb4774f245de27cd9c618d4f45db01f19a","impliedFormat":99},{"version":"ff8a53256414952562c88aba30a7a2492b86575686c997815b307b1f10bdf784","impliedFormat":99},{"version":"279b59c01e2739e252d3609d4408cd1e67ab977df56f9547923496681f600450","impliedFormat":99},{"version":"d50c4d520094e034e1cf9f4ca262a856b53c67dda3a1959057278317206e5f86","impliedFormat":99},{"version":"39637b145df70dc01000bcffbce1990f85327b9e429d5d0be455e7e1efb235a4","impliedFormat":99},{"version":"afe91c616fc7ef7f8e212efb62c66d8ddefd1b8de2582997f98ac57a4d0a7655","impliedFormat":99},{"version":"7326c8eb2ed751b126ca6b25e9d8e9db67556cb40d5ea485afd7518db9657ba3","impliedFormat":99},{"version":"6ff130b441bf073004bb7fdbf5513fa7bededeb5ee9add10e0e395738eb5f4f7","impliedFormat":99},{"version":"272e5e5ad2298abd3852b0f5bf896ee1284a60bbd9e4e45c961d66fdaf309655","impliedFormat":99},{"version":"02ba9c57dfa46d5673abe376a9c35d03f3f4100b4d9c5f9355e0a62bfe8c16cd","impliedFormat":99},{"version":"35a603a9a038aa340a6cfe26b59a9ec2d984da2c5e63a3c46c40113db6635ed5","impliedFormat":99},{"version":"670c15b05a76b37a6ccc1d2e381c7a1780d1288b50751a037c3809c7d4d433f0","impliedFormat":99},{"version":"fdf5d761568f1bb4a318f8ec9f33e99d1f6fe3d7d1a1f573605f29a03b4bc002","impliedFormat":99},{"version":"ae8bef1c3edc4eceaae9df6bf6a046c52bc4838d2344e2bcfd55b84a5c4714c7","impliedFormat":99},{"version":"358b82dd6de9956c92e1f6aaedc4432bde6a8cf4a9b2cc18e6cff0ba2787cb20","impliedFormat":99},{"version":"15811478ecd27a7b1369df74b7135e8ad8a8352c35dbb13035cd5ab2352fa0a7","impliedFormat":99},{"version":"c0ce7088c30ff91f0ff8d514b7f8dcace90b65c554b8a3027cf864f558c4cd80","impliedFormat":99},{"version":"4b49f46b98cf981e1d9f0e75056c458f0c242ee7661f1da3e6cd8eba7b599bcc","impliedFormat":99},{"version":"d7033cf81e6636c20362085d87152a2478c2529db0fcc1eb3b1c0ef152caa132","impliedFormat":99},{"version":"95417c140dd26defda52f7a4bc43500a0915d931308c85866136f829d7d8c88a","impliedFormat":99},{"version":"c2668c0b8cb8bde3d7e6901aa6eba98963785e887668eebe974e8df49a44cdc5","impliedFormat":99},{"version":"bf028ae34f02d5604c337ad4fcc8541ce14e977c3bcb3e7f43bc396af5721494","impliedFormat":99},{"version":"fa1ef591870037b5eb045e5d01902f9015ac8553622a8c18dce178cda3f981a3","impliedFormat":99},{"version":"8a14c1e4557c91f4ca586e0762f423f699e6b97ee5916f548c68966624ed0a0d","impliedFormat":99},{"version":"ebf000b79f4b3d3f8fe4c1d1dee01b12bb82262750598e382444c2f4c6cb6e13","impliedFormat":99},{"version":"df0211c66f5be54737f0d9f4f9b60261365caa812962343f8d4af61c62ffce56","impliedFormat":99},{"version":"2e6b186c28e363aa0f3144864f74bb0bbcad508f91e4f2dd7b652aa32abed04d","impliedFormat":99},{"version":"0593c7918b3a884cbaae040e5c53c8534d0b7cb2dfbeb923a3b6af15cf597690","impliedFormat":99},{"version":"103d7201704781b7503ddeaf536ae71aaf9005d861e9f9f00ba2d5b7c9131946","impliedFormat":99},{"version":"6d39e1758a21b9b1f7a33aa4068bd2fcf9d83f202be716bf03891f04cd11e607","impliedFormat":99},{"version":"4661c14dd9c9cea4b7ec3577adba9b05c554732cd21e51a9a8d67d55cd923035","impliedFormat":99},{"version":"cb7d54e8e4fb0979f0a528e1a40ee833ecf3f2b40cf9db8df5987576359c9a89","impliedFormat":99},{"version":"b3803db7a72c9399c5285a136497140d15618995742f06081d9c2eb52c607b68","impliedFormat":99},{"version":"2a01ae25af7e6725d8d5489fa2aac7e2c521ad4c072fb58f1c6e3bbddf4d913f","impliedFormat":99},{"version":"720a99ab213d49587ddb67dd0cfe541b0cc1b5f6e53ee916b75319bfec2f3ebd","impliedFormat":99},{"version":"81e828965aa9c2efa7403a7a4a533c51e22b618912a03d035a042eb82d1313f8","impliedFormat":99},{"version":"168489d5da3a1925ee759c4ad8d031581cf887a2d305602b4a658cc23ae44106","impliedFormat":99},{"version":"6be5fca571daf55f2f6f44aa9b1497ebacbf3e03e6708e51733a6d84575754e9","impliedFormat":99},{"version":"a623e124420520aaf6077ebf64567f3bee65a6dc3773a1332f0ed513f7f49ce0","impliedFormat":99},{"version":"4d75c7f4eb68d365da1731fa1fa0170895aac801cf21a292dd0503763994f3b7","impliedFormat":99},{"version":"06fe4e250c71760eae5020c88f85742299af8f53530ad9ab937469d5cccd7255","impliedFormat":99},{"version":"0adabfced9aa978628b37783f1e59aa42249a0b7b3fa7b09312c7b866fa3328a","impliedFormat":99},{"version":"7f2cdc3709d66f6004bd77cf9359b467811dd37cb22c00c8af408ab74375769d","impliedFormat":99},{"version":"adbfbc12814060b647c81470c08f3420d89fc93dd1c45c422a10561e3bb6f13c","impliedFormat":99},{"version":"5f1614f54f178ef74d4b33e5871b7a7dec7d330e5d94b28321dd9d32546dae05","impliedFormat":99},{"version":"9f741273f89ff4d1ca8586715c40eeaeb7784d30a9c8324a1dfd39a06371be97","impliedFormat":99},{"version":"9885942ed0ad172b90b45bb4af647ff107a867529534e991f0a112d900d0f215","impliedFormat":99},{"version":"0c262fca6a1199175283b61213d6c6b0646de8584ba0602722eb95b9ad9dfd1b","impliedFormat":99},{"version":"39c1c529a708d276e7f9009b6620063290c18e4c916fc3093d308f32317208d3","impliedFormat":99},{"version":"f2b5e36f2402b53b809e2f64b8e020c6a7637ebce13b95fdac9c26e1a14d7b74","impliedFormat":99},{"version":"8680096ad4dea123f9c49cb0a5deb1fe7a625c93f1a50c4516610925749c03c7","impliedFormat":99},{"version":"0f619545bd7f204050fbe17ab02fd6133ff926467eceda17b430eff646d72ffe","impliedFormat":99},{"version":"923fa583f765c8c013082e64f937b199f980b1b15c40bc3f583b95530e7d2d33","impliedFormat":99},{"version":"505e2b29b7aff97d5cfa06b644a73af595cbb2ad62514705354111eb4a9d25d4","impliedFormat":99},{"version":"0626be91af9b502263c02d30f54462d3b3d441a317caa2fe6c690ab8bae67897","impliedFormat":99},{"version":"0282f7c04ffba8d50df82954017daa8a653cc71c2fd37ccbb87c64105176ce32","impliedFormat":99},{"version":"32d95fa1add10335cf6d98c640a47ebdae0e542b4b2d8c0a302455933810ff54","impliedFormat":99},{"version":"18da6199a28384e4287fbd4035fb4e3602a144975228b436c40371b3a31fd0a3","impliedFormat":99},{"version":"22ff98edd221e6aed5bc9ef9f6a1f090ecb34a1cc465e5b2f7c6b71789238447","impliedFormat":99},{"version":"bcf15f71abb2654e027aa15e6cc2c6b1a56e2a015a4bda6b1215b780b9cce61e","impliedFormat":99},{"version":"b390b5480af1df51fbb3b9bf2140ca51fb96cd6b577ab53de8afe932c1029951","impliedFormat":99},{"version":"449439716e67042af43d9c9290ed4c49bc46854b77273874f7e7f4860b52bdd2","impliedFormat":99},{"version":"a3eeb212864f2cbe7a7e7946425365b0b1bfa25f93e975f622d760738f72cf5d","impliedFormat":99},{"version":"326be804404a38c9b39ef236e45fc4822c4287a337b5d4d2162b7e5577489ee3","impliedFormat":99},{"version":"46d6be7f5084b138f83944e3550fa72591935fed3679ffff9447e2c4c64c5d82","impliedFormat":99},{"version":"b1a7f35702d8f723354d2d69d8f778ef8ceab7f690ddc7fc7ddeea2524c5075b","impliedFormat":99},{"version":"770e7572cfd883ddb4d296572cc49e4183a9299fbdddadaa3f15d9e1643154a5","impliedFormat":99},{"version":"0a0f7ac30acd0ee10988b7fca9c3829610f98b53f83b7e5c7ade6572fd4e4849","impliedFormat":99},{"version":"153bfb6e514f8d63ad14385ebea4a499d36c0385ee58dae66f9dac2337291d2f","impliedFormat":99},{"version":"5d7fb9ed27493c65389f521ca859506d10a6b7e6bbcbdfffb84ae993918037da","impliedFormat":99},{"version":"5bb4df6cdd57a24e32fbd69fb6adf79c3dcbbc4b802a8f3d893a5aca37d3004e","impliedFormat":99},{"version":"21fa1d6be54130de54330ff4c1437ee0baa70d8dcfff4d1c4a85e558875a8c89","impliedFormat":99},{"version":"46439b4d9e8742c3de2c32b1ef4962af91612d83db8a2d77733a21883cb6d976","impliedFormat":99},{"version":"2813b5d86a9e0a9986104fdf4a2a37cbe868a6e17c5e6e3a1abcdcbe1cfc93b8","impliedFormat":99},{"version":"d9450d1102d1179b0faf3baab898c89596c1078885278f94a1aadc4f70049312","impliedFormat":99},{"version":"0f76b54c6dce2b5f193c87e6caa0cd7289ac10781ae342ee6bb7af0ce36cc32a","impliedFormat":99},{"version":"2867f05c20a2225b4c1c8ffb1b5240d30064527fb7a5b1750bd41a89d6c4c8d7","impliedFormat":99},{"version":"1fc856f4fc93907ee59c01efd8edc6b5d49043e3766cec4bea5fa2c2f2f4edaa","impliedFormat":99},{"version":"b864f5118eb5e45345fbd0656be02c88fdb5358c589ad6b5c66ec19f4f0e71a8","impliedFormat":99},{"version":"aac54061ec7558135b4f676212c89b82c969b069342a42f1d5ba6d30f743cb6e","impliedFormat":99},{"version":"d84163e6c4bab8b697bf1c8b0c0908f404df2384b47da63914679f90c0a56bb9","impliedFormat":99},{"version":"ef519ebcf62f2c37cd54485272e42e21bb966b2c203b3565fd265ffee1054a7a","impliedFormat":99},{"version":"fcd2b3eb9f11f2bae710fa4d7a596dadbf1a465b8585f05db63a28d93079b838","impliedFormat":99},{"version":"1d8ab77f40c29a333fbba3585751b384005ced7d895d2c947f146a95413e9951","impliedFormat":99},{"version":"809337f529d322c198398ab84513d7b4dafedeab24179221b6b2f1dbf0f05065","impliedFormat":99},{"version":"60548d44220d4767d7ab2ad9461d664d9aa71afc9e44e03056ad380ddeaa1d66","impliedFormat":99},{"version":"2187909b7da18743b0dacba6e2390825cd20362ff44068e0d47427a607eb22ab","impliedFormat":99},{"version":"962cfde9b7e05989a758aec98703ed75159c40e212df5cec74381eaff82aba98","impliedFormat":99},{"version":"72ffd8535e8e7025b7969c8461c2cc456d23ff175103df046b4fc55a61a38211","impliedFormat":99},{"version":"c890c291338418ad77c86cdce322a8f750bd9b4cd9a456b6363da72549a7a94c","impliedFormat":99},{"version":"df2768213ab37da8fc9ee6cc9f24f4a32c62e874d28625277b6b12f944d4382a","impliedFormat":99},{"version":"effcca1c88335a4448160c1dc7bfe98663f38a67796ee11a04d7bf9c568c8373","impliedFormat":99},{"version":"cde28c7dfb93f6c7f8afb49e6d3a8f8bb577803c7eaa2ab4f02a722d1453086f","impliedFormat":99},{"version":"b05aee7d890773c66fe01f130ee432755c5e0b0e9857e87deac88beea7a57577","impliedFormat":99},{"version":"071091ebb5656ceaca89be5827e45d3367dd7e5f2b51589f5cee241116f33028","impliedFormat":99},{"version":"6324f22adfec34e153717b6d0f416464b8bed2086a7f301fb187cce198e582bc","impliedFormat":99},{"version":"882daca29017e753f72dc0ed60ced6c7ef8c9455bb865f95ce6728e8a7b405d9","impliedFormat":99},{"version":"85cc6f0801e8718042f3116924db65682fc16ef48165136fe136f39d42cf097e","impliedFormat":99},{"version":"61a82094c65a52eff9ff1986c6082e361b0485bfc9eef3c44341f26fe53d1453","impliedFormat":99},{"version":"3b03dfcac351c5c9e0362f081676d3adde495b3d07a8a29f4642bc261091898f","impliedFormat":99},{"version":"909ac7b41961cd1fef0ac5d31d2765c72cc82b4de3a184154b48bc0247edcdc9","impliedFormat":99},{"version":"20fec64d6a21097265323e83c04be9b68e07c26624ba30f05ce53de89fb01571","impliedFormat":99},{"version":"a6d376714411e99706294f4288fa3310d14aa3e211040580102c99fe8c87262d","impliedFormat":99},{"version":"2646752b05b717bd2900551d822735156de24591a2ac17d2ecbf713b5b13f90f","impliedFormat":99},{"version":"ac0ebd6f1cd412a64edaefafc74d7dfb6ce12d03ed88850025622c3e2fa2821d","impliedFormat":99},{"version":"fab8951025c1480a7956466501e4547fa69afacd8a86b6079e1d1a7bfe7c43d5","impliedFormat":99},{"version":"ddd2c8020e0d3e479bd12e739744dd4eddfd512706ee6fc8b0d65b58e234af38","impliedFormat":99},{"version":"027d9c5e810e507c637cb8022c9d44445bd5e3a3c84c5cf55bb2fe67d3bcfa23","impliedFormat":99},{"version":"3679e5a37ccea0b56e73b6d42a36ddbca0a6fcc4119dcb34e652b0e2cb7189c0","impliedFormat":99},{"version":"9e4b60c3965c575a33156eeed8b5f8d5de378793aa1b976189aab25e6ab48ec5","impliedFormat":99},{"version":"73ed3cefe0c0aeb059cf27bafb51e843918087c843ff457a871ed3a074c9aa5a","impliedFormat":99},{"version":"d35344448dcd8a37ee1e2a467f8e9d26c388b712ee3d24a3aa4900585b5545dd","impliedFormat":99},{"version":"3ef76317f676e81cb18eab1fb7e6a1ac60015a57fd679430ed8d1257a911ff89","impliedFormat":99},{"version":"45eee0eb80e0b7604390ddc34fe455d0fc04603d9e4829549272f48c8942c7b9","impliedFormat":99},{"version":"fabe29b682338fa7914d62f21609e4dcbaee9f93ef91adf6db64de244e8140f9","impliedFormat":99},{"version":"bdf0099507aaae26031e61ece1c443799bd090bc99c6e42baad4cf7e9611afea","impliedFormat":99},{"version":"f24ce130c62b7f407a3cea55bd46eb78282ae1e03a5a980a7bf73628e3a25dea","impliedFormat":99},{"version":"cd6e7a1b92dbfdeb233611cb452d8eed24b949cb029f97bef483a5fa3fc7298f","impliedFormat":99},{"version":"09edc5752759a8d4d1fafcbaaa9fad6bc6f1b3ce1a422262ffbd0644f8cf5f30","impliedFormat":99},{"version":"3cd21ebde7071845559b76ee05ad428c5325f788c1fe22e9de4d81e77631fac1","impliedFormat":99},{"version":"542ae7a098c4b7d9ee35b2ded57786de1210bb12845b23605221030b17e04f55","impliedFormat":99},{"version":"8401ff0d8b9f30a8a341ec7269f234d7152177d6a4131e0600e7bdf489233917","impliedFormat":99},{"version":"66a26a0d221fe38994b72cbae8515a91afd5928e40d2f64107ce6bb8fc81375b","impliedFormat":99},{"version":"50594b654ca71c8e7c09bbf175cd34d5efdab5ac423fcb19fd8288456c0eaddc","impliedFormat":99},{"version":"c18cb17ebe4e2417dd2a31a2a4979cfcc72338fde7c9f32768023139c41ee521","impliedFormat":99},{"version":"28d23dbc2c8d8dd21e9b397ae08f5109cbab3a0599df0248190b0cb396c5990e","impliedFormat":99},{"version":"fedffc2d00ff968dc10e3e076f5cdbc5d7ba1e8bda6158bf0ab4286711e1ac27","impliedFormat":99},{"version":"93212dfc3cd9457c7ebde8ed4500164a895d15b55c9f10aa4ac46ffb455c2499","impliedFormat":99},{"version":"f0ab96c940eb748ddfb6f3b1442393759257e624d92034ca6e9698b41a06b2b7","impliedFormat":99},{"version":"823f37edf2f78ed8fd17a59f0837ffefc4b53cc7a0560bcee95bc58626fb9b6b","impliedFormat":99},{"version":"f6439fe717e31e5d222d7e91c5c1082d280f9bf9209474fd7542c4da31b69d7f","impliedFormat":99},{"version":"49126f1c0cd14332be63c13677cbd1543c09362488732b7cd926c8be1fa5a5b3","impliedFormat":99},{"version":"17fd67f8069c954e24fb5ef02e4c97fb2c7b8e5368982ef018232ed2b16b9e9f","impliedFormat":99},{"version":"45be37950ed5f5d4e489a93c3a278811e6685f423d552c33f1518df9c339b5b6","impliedFormat":99},{"version":"0b8f5565dbf8ac71e1f0e36cdf6af9d5e595bac817b1cb49029a387f77a97f35","impliedFormat":99},{"version":"1abd78da038403165528314141ebc823a6adf229febe585828be06e2888b8b52","impliedFormat":99},{"version":"4b068760205754ede438631a83b74443ca9221c47fb5c1026e488a6ff7154dc3","impliedFormat":99},{"version":"10e52fe23fd22927554a0f6637c2f3814aa50f4b449837e84d7c3849c5ace8bc","impliedFormat":99},{"version":"9a695b27f81d8dcf4f4c1ace9362bbf86da55a561333ec22b028567cb77ad167","impliedFormat":99},{"version":"69fa3b563bb3060113c7d4962a0da9fc7d0c1981c1781bfbd776d06761ba7afa","impliedFormat":99},{"version":"45adca51ef386ec97e2b6adeb30bb5bdde8fcacc9b4e6bd7d36b293acb3f0037","impliedFormat":99},{"version":"edfbb6fe54cdaa69a492cc37a4ba702f9352324cda30260ba2dbacc1bc60bf49","impliedFormat":99},{"version":"1648015d9dca4246be02ed5099672f2fc3d2e1f96c4bf38cf312d52a9a36f821","impliedFormat":99},{"version":"f018e038cb0609449172a9ad1130578c92e65939b70374c0847681ce9eea5a53","impliedFormat":99},{"version":"63ed53d5a157ed6749e873d0a4dca4338b86aefc123a605924e4ecc31fc7d3e0","impliedFormat":99},{"version":"602490794c1c63022ff8f03a43b9e3c9464df0e23b2cbfb9a917fc94f4e1239a","impliedFormat":99},{"version":"790d855ffaab4826fab11919a2424117f1df2263cea00515db5d9da27c712f4a","impliedFormat":99},{"version":"b16a21c46be0065ba851366ed933110e7fcc047cd619859cee6fe6043f57eb5b","impliedFormat":99},{"version":"973bd4ce6836a2b7be4281e63be46a02c73c7cfe0f739ec66fba5f64fee42172","impliedFormat":99},{"version":"c95df61205ddee55c0f36488d45eb36923536d1ee7fd2c123765920d0170b903","impliedFormat":99},{"version":"921bc19e17c9b55643dd6bd629c1821a17b9113ed445720c10fe375513aee2de","impliedFormat":99},{"version":"2a596c7487235cc254faeab8d49e084d61d53ca89e00f86da42a5eac0e702cc5","impliedFormat":99},{"version":"063ca493742dae520916addb5eca31c3e0bf6c58cd0f10f5ed8da071d8cc3e58","impliedFormat":99},{"version":"de562ae720d5251cdb274522070f5f5f89a4ce3862043eed99d9456e63a212d4","impliedFormat":99},{"version":"8bc9b778acf6bf5388bea5499cd5b0f4bd7d4c8ed85928dcab7e77038b1edef5","impliedFormat":99},{"version":"17f91aa1a354b537cf970a819537ce1e7a2b2dafca7d4b1b4d0894995c804b2a","impliedFormat":99},{"version":"048f232a281458a0159f024678c5c8651274885d45f86b0b21ca75f5b5ee6c53","impliedFormat":99},{"version":"1bceba42b5425ed2aa158afe916e36e88ee0de33e2f98b390247cfe045061f12","impliedFormat":99},{"version":"e32e2962a30e289b4da7cdc87eb2a79d0102b9abeb37b302b433554faa32721f","impliedFormat":99},{"version":"dfbf0fe6cdc27050ac69f83fe64bfb387dc40cc653a0b559f3ba5185e1e78472","impliedFormat":99},{"version":"f7bcd657a6dafb80e582d43f4bdd33a83b58376734faa9e20cfa9b6030c7a2d9","impliedFormat":99},{"version":"25357858ef183078a99a52e14622266027cd95e2f8a81fd280d539aed447eac6","impliedFormat":99},{"version":"e3b6ecd2f0ec008a7c2f8471bef625c49277dca2a6173a8cffaf920b2b1bf9a0","impliedFormat":99},{"version":"d446c4afecf40dfb4ba6b32a7ed85b5767c145e1e223d17d11fb6a8df9919d30","impliedFormat":99},{"version":"a2c036eefd32f7c0634e1f8615c4b485341d4f1f6fdca5040e62f943972013d9","impliedFormat":99},{"version":"5f3e074edd23169f519bb36e6acaf261a3a5c33af12a8a7e2794b68255bff5e5","impliedFormat":99},{"version":"fc11706b5365ebd58c9434fd9529a100a3d24165e4b04488608b5d55a77d6472","impliedFormat":99},{"version":"909469ed8952f1951373d3294706b50e94d25389326856fd7591557a88ded44b","impliedFormat":99},{"version":"28c160c147c1227df8f46faf50c70dd8ddf0870604250af9eec7f52302b9acf2","impliedFormat":99},{"version":"27a08c3aabe4b415d824042ad958ad9d3d0160c4d8ee43aa8e4a1aece2b921a9","impliedFormat":99},{"version":"4a802c8b0235155d9e45dcaa4d37e46e0eaaab68ef94eeffda6267963267a272","impliedFormat":99},{"version":"5aa4a3727ffa52d1a593b24d6f6e0fd5d3ce859d8ed9bf2367203343f79c0f26","impliedFormat":99},{"version":"964d27f5310297b03bf276d28ba974f3b7c972b532421108378a00cb97fbfe5d","impliedFormat":99},{"version":"315c0551400e653c864fd689e86cd344e9d39de0e1dec74e73b21c6d3602c775","impliedFormat":99},{"version":"5a9636f37a8cfd40dbd97ba1b222f59f085137d574bb90047e77ee853578bda4","impliedFormat":99},{"version":"8b21ec05d818baeb23822106dc371592d197782c61ad6c867bd00482420dee3a","impliedFormat":99},{"version":"3b515b15b0dfe6f511f280d2a66e6f5ec4518d4bbe43699ddd2ae5803ffc2644","impliedFormat":99},{"version":"5cee626e1c97f9c9f634a90e71286bbd32bc84d4391e06286ef7161dce6f8d74","impliedFormat":99},{"version":"77d5d4fa63a5492d5eb299f89ff6440eed60228e4ae45e4675491f1117f95634","impliedFormat":99},{"version":"9caca9e0d06aa45891b2cefe548acf95759cce8be9ba6288d3473ad9a79cfa9f","impliedFormat":99},{"version":"1601c9e2d2fbc602e8ea3cb856609e5dec0eba0f0b4a4190bd1aca8d7e48fd60","impliedFormat":99},{"version":"874f6ab7330b8e05a42d56afd3f77c1758c2d1027e3dbc607956412d503517e0","impliedFormat":99},{"version":"bbc8bbb97a2d014f591b7ef7f12ebf08f8efbdaff154339b89917e20ce586381","impliedFormat":99},{"version":"9ddf9e6cf065d0114a2e03ad2f24db6e2a0b7e0495fb830189fc2b5ef45943d3","impliedFormat":99},{"version":"34392d1d35edd9eefd7aed8c7300e7bd79a486f93a536c641d6369d9de4b7845","impliedFormat":99},{"version":"6ed5d4d4bf431b0b5f10140a0f9e5c91385edca2cf0ba86d6666d966b327bc93","impliedFormat":99},{"version":"e53d7929979440f295425147feebb4fd29ca12b76fa8eededb805974327afacc","impliedFormat":99},{"version":"6ea1298ca33b3ddea4e9fcdb064e1e6d9b3914a243a016b5450ac9fffecaea4f","impliedFormat":99},{"version":"0a613e62ad948843eda4f5becaf2ab18f6edcfd1de024355c74dc28a08a5a101","impliedFormat":99},{"version":"6c21383843396d5d6d1e9fdb594b7f27b1960eaef2e6111d25819b906307eaa3","impliedFormat":99},{"version":"1c0f7aeb0da8e115b03abf334d85d6a3cae1c5f10fa294d6cec1e68109bd9aaf","impliedFormat":99},{"version":"2ad7bd84e41f4ce032f286215e8c9ef109d0c56940423797ab899a69a55dd002","impliedFormat":99},{"version":"aa38b4ba91620fceb0b7c8052a5ca30c5edbb6369ca6953076f394008391a0d6","impliedFormat":99},{"version":"8c9828ec8c751df4156fcc16f2ca81c496563b63439fab6c930dc0a6bcbf01bc","impliedFormat":99},{"version":"398667a797fbed20361065e368e68f5338e88095338c7ef350ab43b52d82cce6","impliedFormat":99},{"version":"06671d26d8a24d55257e7ab611d40db8cdf9da156d53141c13eddf8d22292b67","impliedFormat":99},{"version":"62acc211029559045b264cd8e14413f3bb6c3d197d73ca9919f28def1c0bfaea","impliedFormat":99},{"version":"49ba58fc3f7ea72d435a44695e789d55e9d3b75688a6a5c94da5b65311e965a1","impliedFormat":99},{"version":"56df491b603cb98cc0feee1786f0ccc67cd7044cbaeba753e4a06be0a3dbcf15","impliedFormat":99},{"version":"e789c4c4f3767a04d475ff841044794cab5cce1b464beec3c4764d27c6336ef3","impliedFormat":99},{"version":"c3bf0b45bee7f76574762527d9e8d2b5ccd6b71bc924a4959f566a0e080cc718","impliedFormat":99},{"version":"17730b23f90142bc9bbba78a8ae3e810424683d70fe6868430172bde298cac7f","impliedFormat":99},{"version":"36b0961fdd22011c8b57ad05992a5e786c10a0f7247226df011c9250dfd7dcac","impliedFormat":99},{"version":"012519723b27b0b7d34316bbaefc82b2bbc7b5f09b29f9420b25e7f7f98e330a","impliedFormat":99},{"version":"26b153c6fd0a13b446ddb463c54b8b89391d66e99a883e0d0a5b2a3bdfd4b4cd","impliedFormat":99},{"version":"cf8e9163683d25c0653dd6383d1cb61527ffc680341cd70f2ef82428d174cdf2","impliedFormat":99},{"version":"6eedd556ff92351a9c0837b704da3249fcaa65b249c360f02537e2468cabfd94","impliedFormat":99},{"version":"d7a754e661206c750697768311597e3d333418f1da4546786b2ca315f884b00f","impliedFormat":99},{"version":"0bc0d9c8e623b0adfc5b0f70e52fdc623fe19cf05d76fa28513c04e0e99f2f18","impliedFormat":99},{"version":"8aff0190bd76f107c999b35145ca236977f5c75a6a0b93b3e25185de07a762f1","impliedFormat":99},{"version":"94e5221e731a382af91ce0ed2dbfd0c6c0cade705ba4708937d2d8e7a81c308f","impliedFormat":99},{"version":"0b861aa0e40121348cd97555739762407c59cf6d4d55ff921d02626a5557f951","impliedFormat":99},{"version":"313da5d83029860437f6c9f4a6bd5589d410d10cb73caa89f465f0b92a092dc1","impliedFormat":99},{"version":"1ca3de4ccaf5b8e3b0281ffeb987418b8797cdd38e6f2efcdac6d53be08d2c70","impliedFormat":99},{"version":"3733f99c5ab34d7f5fc2ae62b2cecd43aae1dfe513f1f198a6f23198ec85a9ab","impliedFormat":99},{"version":"c8f01915efe59d2e3b89262ca71462a2ef267a46d85e64d423b7eba5e56fa806","impliedFormat":99},{"version":"0ea8152fddaebfc02cae376261b581493403faa3315b561b7b28c2c9adb8870f","impliedFormat":99},{"version":"366feb5801697dadf2242184050cedb892219e5ad944d958b9f48c1a21bebd5b","impliedFormat":99},{"version":"18e1dbf93c92d56dd99b4feffb6b5f4bab3942a22f9581fe2fb01b6ef98a3127","impliedFormat":99},{"version":"af66f338f282f6a5b65afba57694316e83ffc482b8e43c2157053d2786f5ccbc","impliedFormat":99},{"version":"e5c1db417653c4d348ff6759b3b669721e2293b6123ccf2ac5aaac850727ebc8","impliedFormat":99},{"version":"3d254354637d43b0c1487e8051c4c362db794ca2b3aed7521aad0c4069531af8","impliedFormat":99},{"version":"a164aa58fd0a25722fc159f19636c34df5fe7e606c306459378005a38aa08621","impliedFormat":99},{"version":"1a341bdbf910ac2f80d1787942c77826b17f4e530cfc238a882a2e2eb0e49e42","impliedFormat":99},{"version":"cf4003a33c1a404f35f5c9854f26586497799eb9a7c8212f734852fbee9dfc2d","impliedFormat":99},{"version":"7c24222292e3d201b8b69b2ce466790a5c0abae9de3cc58f7bbeb0678d9909cd","impliedFormat":99},{"version":"7f06d04b0e1a40fd6541852620024acd484fdd7d7652f92debbd27b9540a662a","impliedFormat":99},{"version":"aa440a5a66fbfed7018a245b23f1200cdcffece6fb06d4f6e3b8e47997eddc02","impliedFormat":99},{"version":"e392dfcd88e18a82ba1195eba06545123e53b56f7d1a646f0d47aa2db930dd18","impliedFormat":99},{"version":"271dfa798f98be4ac49dfd6a28cc486f999f3dd7a9bb6f5f23262c64a65cf50c","impliedFormat":99},{"version":"82afb43c3ca6e0891d0c2c78e8cfb622ac3e69c1505dc73e40b3f5946e5a5de4","impliedFormat":99},{"version":"a45a9e50aa41d0939c42d3e02758b105128e57f9eab790e173090273a8c511a0","impliedFormat":99},{"version":"e3a3604500700c4855a5051e5c49caa41df1ec83b30262c1ccc8e34a1cb1471b","impliedFormat":99},{"version":"b5cd0fcbbedb59e0647a66888abca5ca4507f885ff3b286e0c50b842eaed1e98","impliedFormat":99},{"version":"84226fff98bdf61e7b9ae1f51b39fdf4d909393523764457aa2834d50aa91ec9","impliedFormat":99},{"version":"9fdbe407376b7917e668f04ce8d7c23cc952ca60eff05f055d161121d7ca4d4e","impliedFormat":99},{"version":"586e40af8284984647509e8bc2316ed15eb9fc471a5fdb5a1d7324fd0cf8cbc2","impliedFormat":99},{"version":"78b9ad684d80469986ed95aeefc63c0a897a625d9c30281b60a6c67eee314983","impliedFormat":99},{"version":"99a49d4c4a5ecfe3d02d1c9678741b48144d60db675105428cab22623e805fbf","impliedFormat":99},{"version":"c7b9f04d2e9d9c104e124c812cd95e35f6b9e96208cc7261e3fb8cd6341db30f","impliedFormat":99},{"version":"d59ca233e5c37a734e7ddeb14073c661b571a005b65d291bcc7a70c578bc3483","impliedFormat":99},{"version":"f7e93f9af1676ed9f0a505df3218f62b0cde1e898a9f5020fde159b2bce4ec22","impliedFormat":99},{"version":"99a9e52464f5d435e1291dd0cc343d2650e1aeea2faf8c45da0069b79983b189","impliedFormat":99},{"version":"5b182efb3a0328d1185ffd4b234079624ffc3642ac2bdca05f2d2a39f489cf6a","impliedFormat":99},{"version":"3e051f1ac9abb757eba36a1c653079c05f8e73d47f468d2013e5cf91155edbf9","impliedFormat":99},{"version":"50deb81f9a86d6dbc990a3d2e21ed9e95c7053b4f602cece0eb8a66108db32ca","impliedFormat":99},{"version":"5437c62288e1d84f352d1336b2c791e514164f4b76df2b54b9b80b021d14144e","impliedFormat":99},{"version":"3cfaf2c2f604b6e0c3331d6e10cc12a4c2ba9004edd3edc4f34830c12a00de76","impliedFormat":99},{"version":"1bbffc55483a2466f7ed17d1ada9eb309510ad7ddbda8b9052bc0c4439466c98","impliedFormat":99},{"version":"1637d9794d76d852553adec7dab806e435c1d15742c74b55e7f77c6b3f0b4d99","impliedFormat":99},{"version":"94b752027cd750dfd3114ae4bc354e3136af339314e3c0f910df7fbbcc282193","impliedFormat":99},{"version":"f754f4a9815940934f1b958bbf2d2fd8b1463fd16a2e3cbf01a488867a20d21f","impliedFormat":99},{"version":"4ee3b34d2261742bf67c8b5e5b3bf826391e22edb5ce1336d6a8c431b30dfa87","impliedFormat":99},{"version":"09129fe38056fdcc77ac298e218f9e6175608c088d01154047e0066d38285004","impliedFormat":99},{"version":"32a703d65ddcc443c10d404684ee4078f615c02fe7a2dcbe3cd9c27515359905","impliedFormat":99},{"version":"05facbc54c07763ce59d8ecfb9e7236a2490393c772c99a534fc06d300510a28","impliedFormat":99},{"version":"29fd1f159df8bd374aebb398067eecf5daf749d2d07f736e7c7287dea11f4a33","impliedFormat":99},{"version":"2f38b4fbba29ea39481bfc318cf7a1b42d50a1690465a446650948c20650df5b","impliedFormat":99},{"version":"058107de646aab069d38c35d68161a2d4547c6e78c3a3998338b9768ed1b7022","impliedFormat":99},{"version":"65e05ffb34f0189582043596cd28453d050b8b47d89b466ef61953134f7b46d0","impliedFormat":99},{"version":"e08f74569060491151e951498fa46e391320fe652c887b3a1e387bface179f11","impliedFormat":99},{"version":"25abdc000f2be90eccb17eace14443091a8791d79ec423e4e3d06954b0a3e50d","impliedFormat":99},{"version":"95a479f1804f59bcd6cdc1d097157b402f3b9f56923aab18fe4911817d3008b3","impliedFormat":99},{"version":"93d9b928e4b32600406d7a4c854f009799e805da06573fcf481d5377c1759d60","impliedFormat":99},{"version":"24d8c68f8bc95ddbef68bc3dbc659876fc18a15c34859c4c21e002483c0d22e0","impliedFormat":99},{"version":"c9646244df7474905bdadac34ffabeca395850d5415e13bec0cff6884a3a1bd0","impliedFormat":99},{"version":"91bfd8b9c1596dc378f4a21ae2915dfe7f7b99cc4b48042e91d6f473259a50f6","impliedFormat":99},{"version":"40d8306d908ea3ac5af2e34093c8c589d22481d89e60b7ad6360e4ae418b42f6","impliedFormat":99},{"version":"96aadc567cda6bfe77accf7107137390f03b3f3e2968b06f881beebe8efd81d6","impliedFormat":99},{"version":"1231da3d8a3ab8b836808da1363b33f1064eacdce90f6ce1d8dad2a378f2946e","impliedFormat":99},{"version":"c3edc858676c0fb57372e7fb39a8367636c7cfbd532b9882a722b27bc901d0e9","impliedFormat":99},{"version":"2018b6f0e2061de5d0127ea5ea55594657870b0551d6763c47b5947add29c7ae","impliedFormat":99},{"version":"98feee5573bce5dcfeb11863da1463b0e5e4c088ff76d0010676cc297399aa33","impliedFormat":99},{"version":"9c7e6ee655cd3e2c3db15050e51cf28350e020ce2b5ca030f885165bac4afdfe","impliedFormat":99},{"version":"ea7570613b458b3b741acfb597f2ef75c646fc5b8de31f24f3e11e9ecc3e2626","impliedFormat":99},{"version":"6a8bb162513430ef154922525aea4d9c9e0e300d33b7e6c7dfdc21f1267e285e","impliedFormat":99},{"version":"5e756b34bf673b63c88b52377d9341a86e5b85bc15edcfd25926f18ab7562fca","impliedFormat":99},{"version":"8e978604cbc7a505aea54c40a6a824035d019de1a9ea073a8d89056d6e75d004","impliedFormat":99},{"version":"88a6d094de07d3bedda43a507010a4e496f83f898c8b30d51d1d66147317d389","impliedFormat":99},{"version":"0247e5036245f425410b138acb7eca8eff452e90b60ce15a6d14a79254dca3be","impliedFormat":99},{"version":"a917a6578721f88084be79236f3c3c6c8d0626d27b9d29eb8c7b40336d24a665","impliedFormat":99},{"version":"7d4d7cda987e76d83992b3099170e72f1238abde39b07086f2ce1c047fa5305c","impliedFormat":99},{"version":"7a6e19fb0b63f9a7f010e254376097051865a6970c698e0c4700840ffd88e646","impliedFormat":99},{"version":"c1214f047a9cc3b256a6babdaa062b1cc112f9e11c3276a585607b4c034177bf","impliedFormat":99},{"version":"645cd29e13ee8b379c92b7e016f80f020d596c0e329122fb89bbe857596b7fb5","impliedFormat":99},{"version":"85aa829c49948a7745bc9fe8349bdb83bba19763c3a8d6b30129498714c4a81e","impliedFormat":99},{"version":"0883b415bc4b8364115b3a74c2c5d14bb6852a1a9810d051ff426c51b7865e3f","impliedFormat":99},{"version":"57bcaba92379c398572ceea17e80cb9be80b0915bf2022fe99f360875e3ebbb6","impliedFormat":99},{"version":"e19c31cd2a5d8e14a56d40d9f8bb4819f70ee3290c71fef47d753cedbe1b8403","impliedFormat":99},{"version":"7867d743e0710c8233955855dd1c8143c76bd169d31e1b81835dd849d53f78e9","impliedFormat":99},{"version":"d953795eecf54315d23cf13cfef3aaabd521a305b71a67d5939487d9f8f781d9","impliedFormat":99},{"version":"8632c3b1a88dea3b5c07133a969658ade0bc8b0add4a160ecd12b2e8ff73f147","impliedFormat":99},{"version":"bbb8f4a3cd8286ed8d4615d7f6c0cbf4753c761e1d3ec18a20a161d276b6f617","impliedFormat":99},{"version":"9643da030e1fa23447dacfec5a005dc51f0f7f651dfb0c063d4195eefc8a098c","impliedFormat":99},{"version":"f78f0984c1045c759eca17c87d9892519813d7ccb07cb9f10bab6239274ab3bd","impliedFormat":99},{"version":"8db95a03e8ba40f5fd72926a7df034c42c083b264fdf948382eb2281cc3aaa81","impliedFormat":99},{"version":"a004f52364ed07ec22dfdd34041dce156046b1fd1d9170ca99748e7ebfca1b9b","impliedFormat":99},{"version":"dbff933905f07dbe803238031f124892e8ada74efd4b284287bfc75d2409e3e9","impliedFormat":99},{"version":"0941bf71e7631df878e29025c83b35ecf2b4fc730f61b008590fb0f937aa63a4","impliedFormat":99},{"version":"3e8837d71c58e3ddce51b55e29ad7691eb15aed51dffec5fb7bc968898abf6b5","impliedFormat":99},{"version":"6e58caca3025448230faecb5efda0951424ba15b071ba13b00f3e7f1cc6bdd29","impliedFormat":99},{"version":"b3a3f55260b2e4384b00a7bfc5720e9866074ac18f715f3e505c7593aabce3f7","impliedFormat":99},{"version":"3f8c206a53bb5a8370f094ff77ea9e95ee8c3661e248a1b9c0334bb71b641907","impliedFormat":99},{"version":"99853f693260e3d76256576888c7cdc5515e50e399fd3111afc514f0ec9e16a4","impliedFormat":99},{"version":"480cc4f99dfc33fbe523f155018e66bed504bfce061031bc04f5c47b7dd2ce05","impliedFormat":99},{"version":"bb0cbdbfe7201cc8dbe28f10d4577316d63a633b746f9e9ea9ed16a0400a0347","impliedFormat":99},{"version":"6bc725824b4ba929f44f3973aba69c3bf4c9b8c667d6c5bd257524584bcf6112","impliedFormat":99},{"version":"1f2a20e400e1df467ced60364324445d04e36184a2fb9bd39f32f85058d72b50","impliedFormat":99},{"version":"83c5f92d12a9677cb376404aeeb89f910efa2ceec8fb200bbfe59c508a5c86a0","impliedFormat":99},{"version":"91236eb27c0c580c803b9581bddfe4dd45623093b8638c8cf39148d193b185dc","impliedFormat":99},{"version":"ba41d9506bcc55e2043d0a9290c38a4c1fb4ef82fb2562fc2039d55ee0a80a72","impliedFormat":99},{"version":"214c9af465ad68b1750670de18e62b8f3e7749257aae5ab0d7170b994e9c7e5e","impliedFormat":99},{"version":"62e8d8503a51a24c9f0947d88edb5837357f4149c5103fb304bd31f8b5d4f4f9","impliedFormat":99},{"version":"5dad46254873e5e5c4b8b80f6776dac8bcc96b059cf6ceafd9be54d24faa3dc8","impliedFormat":99},{"version":"88ece1d1c523d9eaa48c8c3e0c4cd359e9df8a542ba6eaab5dd3c523d5fa9826","impliedFormat":99},{"version":"109ad0355918e4fad1bb9d849f8b72a6894b83d20ca6ad72e73124fa380a6c64","impliedFormat":99},{"version":"70b68b3ac7d6b03ae36e80766d62a666c407f455476283ae6ff219e49f0eaee6","impliedFormat":99},{"version":"bb72571132bf5160ef9af164b82f13fc74c332d11c3325e4953ede73627bda0b","impliedFormat":99},{"version":"e4101333a36a93829170c6e0c66ed47c648571502b484d22e4629317197a6bc3","impliedFormat":99},{"version":"f0e5a05eebeae29b22dd7b3747fdd6503bcf7ad42df8ccb0a8129e98ea112408","impliedFormat":99},{"version":"658adb3d7d4017bf38ba1d1eded9dd36c5be121eaeaf469abc11c2494c13e12b","impliedFormat":99},{"version":"f0c39b43f5d2619c66118d2441976b4f1fcd100e55be80bd55d8279b2776a082","impliedFormat":99},{"version":"1c116a992389ab12f4612dd3cc9b4d888eef454eed3794c1f515cd76d3b17cab","impliedFormat":99},{"version":"0d9ba4647605be41ae36c09439b623c174e7227c8943d15b1aad049c5932c2f6","impliedFormat":99},{"version":"ea8f99323ac76484ca97cddd57ed916dacd4ff2805c446a69da9e728ca8c9b43","impliedFormat":99},{"version":"18e80e1593efac104d1fdf3fe5c02023be9f3dce93ce3a95df75ce1c17b98332","impliedFormat":99},{"version":"68a5340b5cb05677b52fe5032e5b8ffaa3d02a98baa991ee161e318e17d0e74d","impliedFormat":99},{"version":"7a1457e43136d0a005af395929abb59ce7ea3712b1b4091040fe700c4386ff6c","impliedFormat":99},{"version":"8aafd0a8486f554718b22d02cb662b9964fd6b6346ac7c46573182ed3102e9b0","impliedFormat":99},{"version":"ab0f538caa8913de60351b4d993393ba30d485461dfed416b11fce206820974a","impliedFormat":99},{"version":"7d703f987cf71538d9b4e0388c4744fe91db9b4bf5fc0b5ec2120e523ff036ec","impliedFormat":99},{"version":"779e6f8af772f3f63982b2d907c662ca37acb84e7ce1fad4de9c8417142504d1","impliedFormat":99},{"version":"e1ad3c3b80b5bf0ba59c3b33878afb16a906c09840f8d1b072af14e587360f6c","impliedFormat":99},{"version":"51cda3a044b658d58c693647e19ecf16c29f5ae67caa0f1c619c7c76b93f1af3","impliedFormat":99},{"version":"e6b27342fc513672b2fcc831ed5d48627817ae0b26b3a487643b61785c52cecf","impliedFormat":99},{"version":"582d84dd785b99a7180701aad4fdfbe3fcb26cc07fc693032361070ed0369759","impliedFormat":99},{"version":"eb5888375a2ecdd0d3502cec0f77e1e42eeb2169c76b79b61add3ea0bc95ba4a","impliedFormat":99},{"version":"ca5f212e869dedebaa7bf73bea3c6e8ee886ba2f0601d7e789ce38e2db189600","impliedFormat":99},{"version":"d21865e1111bd2be2ee973598343a324713e31f1aa2e6184286e1a8f077366fb","impliedFormat":99},{"version":"3df99ade0e19a8fe6c989688eb5743baf8b02b4cb678018dbdcdabe00fc42879","impliedFormat":99},{"version":"785eab4c4a6166680180ba4e9bf06e6c205862a3c83323fcdeac61e6a76adf8b","impliedFormat":99},{"version":"c1686f9d62d0b94c43faf794bffd8cc24ac1475fc6d536aec3b4480128660322","impliedFormat":99},{"version":"3e050baa6d458a74a769e3fb50ff1051723dcfccfa8a31b7ac9ed0c990d5dbd6","impliedFormat":99},{"version":"bbc4cd67ab91a186d1a4aa1f65a7971ac31bc2d8c63fd2bfdf02e031a85be7f3","impliedFormat":99},{"version":"6c09c2fd7f6c0512ce2ad6e17a0d9f9203f47afd3b83742d4cb4db2d3ea821d2","impliedFormat":99},{"version":"16e3734500cb5fbe0cf27f4b61cafd7954db45c3facff516d8819bd63482a8f9","impliedFormat":99},{"version":"352036160d597fb56e03591325761d86221f6b4facdb0c7df4b790c4891f4f18","impliedFormat":99},{"version":"f93375427ab3f9a598bc14d9bef5a2ebf68c4755bcf3a68b794e131dba637d8f","impliedFormat":99},{"version":"798b8feb569f4b4c521cbcdb9651d0fe799ffad794284dce8a4e0d9dbaea95ca","impliedFormat":99},{"version":"3deb173189f9a8232ce9fc11ce610c27435f5e6b86e507f9c04d5fa275974101","impliedFormat":99},{"version":"de8156baa4157a698a1bda8e6b4ba62b8bef4972808f7450698017219f539475","impliedFormat":99},{"version":"65c089d5dadc1d26c81c374f00a8e11b7178900040dd7db065d8cdc405c65e15","impliedFormat":99},{"version":"66003617c7bd48b01253f0f0ae9218f15adbfd66a04a74d04235f28f9ffb830d","impliedFormat":99},{"version":"8733c82576758e483eaa077a95d1b2d1715353ee0d8f148d06a5f979fb71f22d","impliedFormat":99},{"version":"0dd5e215565f19d57d88f9c32cd365fd451207bdef48519c47b14dd4ddb301df","impliedFormat":99},{"version":"8b86136891f48d0e82f8fdcd623268b036b2a8ddc22241a1202576792af0b63c","impliedFormat":99},{"version":"1a080e2c42f4a16b339bc45067ecce29ce25040cb5db37f5f3f19413aca07e09","impliedFormat":99},{"version":"55f8158fc3e7d4ba4ab62820d32f236a5723af9c721e63879cb9c184292a718f","impliedFormat":99},{"version":"170f5fbca91c30969467442f813678c86d2d38d10aa3ef8e5fb388bab833fba9","impliedFormat":99},{"version":"224347dccdcf08e3e862699dc9e35f6ae1f9388f56e14db2357bb79b75c1c814","impliedFormat":99},{"version":"c6426d9201d8e27551ba9a0b5f4d9103dbc2b2057801a621098116a042b03b92","impliedFormat":99},{"version":"af11c67bbdf99195a89aa5233d8641f6e1d2f7ff712e941c773e16f602e7f96e","impliedFormat":99},{"version":"1dcc9bb3542020790409937565639ac7c3bb8b239ff49c65ee20d5244ced1fce","impliedFormat":99},{"version":"64fbdfda1827a333790a0cbcd36e40bbdab461df37fccbf8a2c12eb0a6450b19","impliedFormat":99},{"version":"9f40ebfa8a6cca16447d13bc9265e7b463b1d4e7265336e287d2ed2867af60b9","impliedFormat":99},{"version":"cd77f74ca7acac465b9b6ffc244a574cafbf16ef35bbc834c98314d1603f6b52","impliedFormat":99},{"version":"6a3fb20abadf78d08de29baaea62889070d505f35ff5140642e8074db0f5b7e9","impliedFormat":99},{"version":"75b2e385d7de8d6d714ff9aa6635bc494a6f90a6f33d2c5c7710bc71ac19d9d1","impliedFormat":99},{"version":"f5e7ffabb3a1e6d3c0c5039912c8a4ea8d753722c9a1e602911c100b7ec155ac","impliedFormat":99},{"version":"09757c18f7d7af16d89b75b643ea4b3a0e195d67a63c03755a07dae726e1148c","impliedFormat":99},{"version":"3b2b48dbb70d48112b34b4a73280990bdf75147d75659cd5280e846513abc75e","impliedFormat":99},{"version":"e6f905bb9525a266c16e4350cc4d4020689a588f2eb6d6b64fcea6d037a05977","impliedFormat":99},{"version":"4a988db19eb645816e40e48b945226f76de53f1299131003aabd37a013c22590","impliedFormat":99},{"version":"9b7dce8b944c33e20355a272f156582fe0198bcfa395d7082e6a66eefbe9ce57","impliedFormat":99},{"version":"13aa72dbe072780b3f936dea5d1c3094af3b6c186c24bb30d404540ec447b3d5","impliedFormat":99},{"version":"a280303c82a34689c9cc178068e6c012c6f582f80cbb6a75530bf552313090fe","impliedFormat":99},{"version":"715ae11614ac2209e2b5f3efb19b270603f8dbf20db559b76b02a47d6f3fd77d","impliedFormat":99},{"version":"d337b0c28c6f35442c1f88ff8071bcf9ef2ff381aa37fc30d323c6ac25e23708","impliedFormat":99},{"version":"425e7209aad5daaeeb4df06a718f8f177db5f0322c5fa94573ec739aa7f02deb","impliedFormat":99},{"version":"cab30bc9301ed991270375c5a0a011c897307e0e2158578e96f93b497121a3c5","impliedFormat":99},{"version":"00e136dc2bf5a5dd51fa6c5e9c02a7aff6ce4bd111e74b0f0d59c87494af51dc","impliedFormat":99},{"version":"4933c98fcc77ebbf6166152f65916cfbfd8f494bd14e6d8eca45ee231f1f2926","impliedFormat":99},{"version":"55b7f086a4ce7f130df29adbac36981bd64051263686af3271c604cd9591f62a","impliedFormat":99},{"version":"7d8fa714edcff481c66fcbd1c29b0856edb5591d085096c98613e9b701f55676","impliedFormat":99},{"version":"879e9c1b88c533389580e7178497ea55a36c95876fe1d94c6bf73883b6d29fdf","impliedFormat":99},{"version":"7c22b12c9c17dcc2317d52b83d8662c538fb337adc8c1ce69ebcd8f8394e2064","impliedFormat":99},{"version":"199e6d1a80d19524ea70d143488d72ef5e8ffcf6bbfe82dbed7f03d603948d18","impliedFormat":99},{"version":"691ca73f83cfe3262081875b1096744b1314860e6e216de3d242b4c76f8c1972","impliedFormat":99},{"version":"e43105a7b869d2963215dbbad1da3ba79108c3d5361df4e11d6c1bd967890bd4","impliedFormat":99},{"version":"c3d4d0c0dc57d5fed5a7af5ec433a3fac319c640aeee9ebd648066535cf9ff51","impliedFormat":99},{"version":"6c7d9793b68ed844870b77a5d4e6c92dc5d3b10d3c09007bb54b4baae99eab62","impliedFormat":99},{"version":"98a99b89bfbc2d9b7c3df85ee85f5fee6e9eb7309dc0e8b08b77b34ccb85957e","impliedFormat":99},{"version":"026567c73d9d23ce133aa2f27da3ee815b724f52548bf5579746f41c60723fcf","impliedFormat":99},{"version":"df6f1df7a3ae263e071615559e198d94c6fec45d33140fc3403eda6f57c5f8e0","impliedFormat":99},{"version":"5208ac9b4aead07702be291e7b6b2aa5936ed9ab8b21de6227cf351292246f6a","impliedFormat":99},{"version":"8d04cf9b77d2bff28760b0126105d3d5a34a1a1e8cc2b4966a9e1859bc55ba71","impliedFormat":99},{"version":"e7925d61f7792610f43a39d58dd58b804bf43d6071a09439d44063e35e5ac244","impliedFormat":99},{"version":"4768ed15015abc1826477096caf3c770ff266bee8136ebec5cebb929d6b9cdaf","impliedFormat":99},{"version":"7ab6049d6f98dba00f3e379c0bb149eae6d2bd3f8ead7de9238bdf65bfd5d791","impliedFormat":99},{"version":"b50f3a41e80fe4e8218a7f6c70257ccac4c3f360d318da72779ae1dcd0e787c5","impliedFormat":99},{"version":"a7b7342414754d0fc6a9560c08e5add2eaccb9570cd39c53a5155f368e695f14","impliedFormat":99},{"version":"e574749d4ad0f5b989632484891d00a6ef6104b3b014e9eb9bfd1f3c811b4a3c","impliedFormat":99},{"version":"e5352486d0919edaf6b57caf289859980db09607e225f70d21ac9b06ac1ef04e","impliedFormat":99},{"version":"23c0a2c86cdcfcbdfc95f11c89611f04cce0f593b5140a2e64d20556af3f159b","impliedFormat":99},{"version":"3cda756ee1dfe94744ff3eb6a11b7e5d41f502d0f8c46aae5338bc82cb929602","impliedFormat":99},{"version":"5a2db3c589431fd671ed605ecf0725decd9acf0e239966bd6d91eb2cb115a0ef","impliedFormat":99},{"version":"eb63ec99b754ca1947a7fbae5d7f461f8179f647bd0f665f6008443b7fc8fa14","impliedFormat":99},{"version":"5d4e4b5cb1a17707244dc479b464e537237ea4139b4235d703ea83a4a9287840","impliedFormat":99},{"version":"97bf370e78bc695d758542bbd2ecde489a61db5d678c07c85bffa91459343c37","impliedFormat":99},{"version":"e7789e4369b2c26d9a8a1150141b146932565ead086e2b4e32ef145325a8894f","impliedFormat":99},{"version":"0cd7b61de09c45306ffca7ca80fe784e56623cbfc99e1fa4aeee7ec924353d47","impliedFormat":99},{"version":"9862a6e0ad83a3b3faa226fc86fb1940b3a9183c0c29f5bddaa9b2c5cb813a05","impliedFormat":99},{"version":"d85d1d741211343158b4f64b4fc7d0ab347a643a3bb8f5a7c80fee1cd71ddb91","impliedFormat":99},{"version":"b30ca3d58261de8bfe7b599e9806d710700b58d7f7c7c1ebec13838bb1c36d76","impliedFormat":99},{"version":"19c817e65f0d10c305c29e01d8fb6defa09291fd67bd13034223cfcccc14f8fd","impliedFormat":99},{"version":"d72ed5882015b367986f5ec7b1cd58bb50bd1c51e941ff06729b67ab66d4342e","impliedFormat":99},{"version":"8e395378f1ac56cf2bcc7a2533cfccd2f431e45b91340418148c99e16eb58c6f","impliedFormat":99},{"version":"cbc884601dbfb8d72c8f8877a11179ab41a9bfa0b8c8b345184b835d09a29354","impliedFormat":99},{"version":"b3be68eab5f7e270ac467749dd01ca3203c572cfc511da099e028a930e933416","impliedFormat":99},{"version":"ff1f6fda8fed4820f48b922caecbcb6f74b81ef984908ca213d09463f9f7f937","impliedFormat":99},{"version":"e8c989cbc67a55209e74d695ff91a888e229d67f0cbbd8b49544cc07382c877e","impliedFormat":99},{"version":"838930a4a3fb335a78cb1d3c4d5b825ee7ef1a7ee2136315ba38346aae1683a1","impliedFormat":99},{"version":"883985e0a6918e642e1f416c6afd0d0e6e8033f3b29ea5a49cd808c88874b7cd","impliedFormat":99},{"version":"f126c52fb0b87a45252fb6397c2326e3f8c3947d01370c2b4d9b1d4d9196f046","impliedFormat":99},{"version":"99e693a698fe25a9924783102421e87d02ed7204369d0c499b207f4fe872f168","impliedFormat":99},{"version":"a70e24984ec50f9c0e02dad3f4b5bac149f10165420d266ba199e50fe45662cc","impliedFormat":99},{"version":"633e63e5fee829801ddef23112e30073ea34d9caf5f464d478fddee93e627475","impliedFormat":99},{"version":"0bba6e8d47afded79ee770ec90c46fec57cbdf69767306b86aa9b32e6f4dab11","impliedFormat":99},{"version":"66b7c3956758bcbad1eb7c0d4fb0d779323a825b8adccdf35ea7946eab476b20","impliedFormat":99},{"version":"16a1a5d4825dbf2991c5a8a2090dd84d8290b19cf541e93a7c4995b8037a627a","impliedFormat":99},{"version":"35f172c558295a38b28a149069d619f658efbc69ba9b672e6c853a489ba23457","impliedFormat":99},{"version":"3b2112cd844c32dbf3dfdfdf05e14b396f46ba6e61bb15d82374cc8aea2f272d","impliedFormat":99},{"version":"17e59b4b305bd2ad32bfaf62350c305bb45fbacfe5a4b0547369259d17775e40","impliedFormat":99},{"version":"c11ab5f4ec79d50b9741fe66527fd19c09cddde5efc4cd39b5a60df69c38b604","impliedFormat":99},{"version":"5530eedb6ac3365bfe20f117de0db075971955dd763eed8b9757ecb5b5a3e57b","impliedFormat":99},{"version":"fe847d09c50c98d93de932ab711c1569cccf7100c0c7ea59e4feaadde30ea048","impliedFormat":99},{"version":"a691ed743148b3133ff53977947c447c5951382aa75ff41926502195592854c3","impliedFormat":99},{"version":"00387baba842e4e7cf442edb72abe67bf2dc4c48b02a44175ec2d82a88847e88","impliedFormat":99},{"version":"b9f3a62b5a7b9cc2308ff957f2f9400431a484d44dd02b7518abcb08b387528c","impliedFormat":99},{"version":"1fd1e1b625fe20a8a1a133bde76da9140312d4fd0da1c18b27d23002d4d62243","impliedFormat":99},{"version":"38d0d481e741ca7daa25fd987f3443ffe351cd0a9c0b4bacd5e008c51f24850a","impliedFormat":99},{"version":"c07fe65c3f45091dba97d40b4ac07c390ff625a0dd92295d76051f0998618d80","impliedFormat":99},{"version":"b27a05e307131d13c3ed496e07dea2b79331aa1ac9571524fbdf16cfb3ebb1f5","impliedFormat":99},{"version":"0dc1ba6bb0e4fd00bae62403c87047bc5be4ab5943ebfacd5348ecf74c9b0328","impliedFormat":99},{"version":"ef60c0d7cd61055e5f06a6b76cdfdfe317460d5ce787efb48f35b3618bb68608","impliedFormat":99},{"version":"8ec7a6d53714b3c8d7a5e1eeb36108ea511cfc7fa4d7dd08229dc906568548ea","impliedFormat":99},{"version":"f84ef63eb5e13857e20ead0977940fbc9cb65da0e3705c04f0f4fc4326937c1d","impliedFormat":99},{"version":"827f0625ef8bbfa8997f91b8832626a4be0742fe3c9c6309cdca802c6edda8bc","impliedFormat":99},{"version":"1a535ae8a6cad0882e7c7189b21f2986dcfd5c2efd9950b5b04cae15179b27ce","impliedFormat":99},{"version":"47d40b96045ac0f52537cc2076f9ec1404fc444861191043c348d8afa0c6bd73","impliedFormat":99},{"version":"58b86a43ef8dd9649a6b67dbeb512f01f934ad78518ac89683927afed03b1de3","impliedFormat":99},{"version":"47c563c296e93315d38204ff70b2a3cd6788fdab9ba388791fa1c82987794d42","impliedFormat":99},{"version":"69ba23cb263a0916bc35457477b88e05eb35809c58a17449974d174a2aac6059","impliedFormat":99},{"version":"b1e445ab4f7d5c6d5094c3d6560539f641987a9acf82a0784b320e01c803af90","impliedFormat":99},{"version":"9e4c32ab790364ad67b078d569d273225d28a3cc5ee1b683e096fafdd0372745","impliedFormat":99},{"version":"3b9cf1e4cae113f06b84bcdb48dfd99a04b8708e4e4f48712ab94b7e1d11c884","impliedFormat":99},{"version":"52fd16e0b3f6582c3b545957606e1e82e8e7f666745a2c0a077eb5b534246109","impliedFormat":99},{"version":"28782dad7cc918112ee88ba23ac97f11a56c7e8cdaffa0978ca83751ba49b741","impliedFormat":99},{"version":"d707632c2500c4d4c503dcff2794e15c9102624ce70c9ad1662e396169290a46","impliedFormat":99},{"version":"798d2a4e66eb3726f8e07b6d94341a008ee974b8f86b8b98550f79b3b0e43390","impliedFormat":99},{"version":"6e36459dc4168e7f2c37a9f9e0971c3aa8840e753519781d1410c659c1fc525f","impliedFormat":99},{"version":"686a3bb7e54b8b622ec5bedaf1ec597633a9e75dba1261e4017e2e8e8ae47314","impliedFormat":99},{"version":"220ffd58c0b38bddd8d79d16b48aa3c71be8a0e8d01d45ad65396b453f6f5ad8","impliedFormat":99},{"version":"9fb07cf95a99e80c1c0462eadfb2b5d9434dd20817ef143ff67def7742b9760a","impliedFormat":99},{"version":"c70bfcb0d36b6e0d59a413e23016e402ad1d5afe2e5a74c1da3b52d271e4d4b6","impliedFormat":99},{"version":"55712f6dbd9fe2bccb9cad293fa37c294dd8bd9067566bcfab17e5596aa77bca","impliedFormat":99},{"version":"50f6da711079ff42be35995fa2dd810a629acee19248c4476ffbe1bdb324b7d0","impliedFormat":99},{"version":"be3455c8c848814f4b4f8602a1088411fed507bdff9a6cc9ffd176b9092ac401","impliedFormat":99},{"version":"16cec8c852d6b1cc27c107593c3087c901ce3e1f949b3560729d9702e26da2a5","impliedFormat":99},{"version":"1f0351b420afd408a267f3c3f578c788245d29d71da0809221f2f9b3dce475d8","impliedFormat":99},{"version":"b79b1fe1036d51b099e6ac6b3fbfb4eb36562c783e473e40c6cd2ec65991ecb5","impliedFormat":99},{"version":"b6964ec91330fd27e71660b777624e9342a2919c08655a7a65833324dc9797cf","impliedFormat":99},{"version":"5bb8a642d5b1f469421f30108f24d5443a83b2d59a385191e93ca650a9dede54","impliedFormat":99},{"version":"b76f9204bb26386bedaa9e14a23b3d9b1c6fae28c86f7fc484df7e5b74ae3844","impliedFormat":99},{"version":"8c1eea73430f19085f52a279f92371409f9c3c1f1cbc6187e474d0ff158b6b98","impliedFormat":99},{"version":"b288dcb1d03d232ba58b61e94124feafceb0d411013f603ddb2068702e5a13f8","impliedFormat":99},{"version":"a9ef5666ecbff227e17ce51d833b7c8822189565799aa860a60efa8a4f02ff00","impliedFormat":99},{"version":"29670032e04e02ef8a2172d3d637ef989b2896718852dc0d6735d47b1d8dbea7","impliedFormat":99},{"version":"c6308f587ff90713ac3832efae5b7aac09d6b067c2bdfac9637583a668d470c5","impliedFormat":99},{"version":"45c053bd25f8463d059c805283437436b640af4a28f9e10811bf1cd122d03790","impliedFormat":99},{"version":"bd4186ec52ee4bb1e07d27a1534eaad966bc9ba6cd65a58a63ff6a45632348b2","impliedFormat":99},{"version":"742c77f3420abf97cf51c552d99d0d935808b33c612e530ab54d115c513692e2","impliedFormat":99},{"version":"1b9af93c3bb53108e109446a0127b8dd6b9ff7c5096319fb6c4d63b9dbc8ec75","impliedFormat":99},{"version":"5cd660965157b9211c61f131ecd876143129cdd528e49099fc4610da0e7c04d8","impliedFormat":99},{"version":"a5664583622519fd735a5b89bde8040e93d11015e1559270008ec3722f546188","impliedFormat":99},{"version":"290d2e6293e262efd3aee13536268f8e96f31794800a1b68e392f27cdad9bc13","impliedFormat":99},{"version":"6c5e7e9f0c8361569abb01b323d939288ab7d2a16ec74962dd0981496a3e7ba0","impliedFormat":99},{"version":"386cf4e0b737544dfd42761ebb6159bc987b0747faff23af0b73b9003e00ee27","impliedFormat":99},{"version":"d7c7b4ae623b52e885e1aa716cbc22bad409d36b7204dec4004ed9c55133933d","impliedFormat":99},{"version":"dd0645217ad10dc8360c7d83bd6c6f36750b7df44c48116c883024dc8e125329","impliedFormat":99},{"version":"516294ca25dddf7d44af05033e856e5bae0fa1cd7a9bf255e601fb77b5b9f597","impliedFormat":99},{"version":"8e4735e0c080d09cc800cd41f646de9d759167292ef7cbd76a0eb57f8c4a00d2","impliedFormat":99},{"version":"0cdf249a81c98a3c6d79dbd0c1a65ff377d6879658aa3aeb8704a1b2158f2230","impliedFormat":99},{"version":"fcac8bc31559ac91ae0a65ffbf43779dae8a9c585f0e3c188fee07eba5444ca2","impliedFormat":99},{"version":"b37597091430552997625ea0f386da6b0ad725704274564c17282a06d77eb585","impliedFormat":99},{"version":"3aed368622022abff04f6298dcb1236b389a26af3a380ee8f06256850a3d99f3","impliedFormat":99},{"version":"be4068251ea5a2a50fb0e075ebd0b2cfc773bbf3f974b97b110011736b50efee","impliedFormat":99},{"version":"9223c5f0109bcc0868650998613d670c302abebde0cd2fc806e42a4b0bb5c9e8","impliedFormat":99},{"version":"0571ff9f961401db433c7c248b16bb2bfe65c0f09dc68543745c16f2176bc8ff","impliedFormat":99},{"version":"05626dcdef717ded5ea6fec10e3c6a6c7acabe92bbf36573470212d5d858ae62","impliedFormat":99},{"version":"64b324dd74b6cd0073181992870983522182d33cf0b9b3c1b6d503e4ba854b79","impliedFormat":99},{"version":"9e6970fc8d80d18a58c0189f4900317554e328e624da3bc78a954ea08b166599","impliedFormat":99},{"version":"cc1dc46db20468ef6189aa7cf4a8f4240897709753b2895e5400d20b6e4cd554","impliedFormat":99},{"version":"30986d936b4fb9b78344bba08e72ce06e731d10862061c2848b019d74ddb7eee","impliedFormat":99},{"version":"12dba10fdfe2dafe7f69c960da3f694723d82f2856ccade9926f5d04ce16e975","impliedFormat":99},{"version":"06a1c3af923cd00e69142a78418edb82c211cba3e72a91be52ae12bd845550d7","impliedFormat":99},{"version":"13405921db76cd2ca52f50b8dc8819884bb0fdbec7feaa8b0e5256fff635b007","impliedFormat":99},{"version":"474fe5758f25fa31f93293163386bc120cb3a859628146216c27d20f9fb4e11b","impliedFormat":99},{"version":"797a5c56e7b68fa7ddaa736ea0f84903a0280b3e24f7edc81dc65d59397c1dbc","impliedFormat":99},{"version":"13144ada8c1a653103674ae05cc08441800d8db5ba8efa624158528eede15884","impliedFormat":99},{"version":"42bbc0317d36df458e7e5cac3fe51a5747aefab3abc5cb3f238a944d5e32a91f","impliedFormat":99},{"version":"678842cc3c2c69a8c2fae8353656a1e5337828124b7b53ddf41689876b70c57d","impliedFormat":99},{"version":"f0e3459d79ea7a69f7a334151c38ca830c4541a030204e44b680bb47cb357bdf","impliedFormat":99},{"version":"9ab46babfc3d98ac7a31ad70b4212562ad36311d9fa235871dada95e3379fd09","impliedFormat":99},{"version":"92e6d6a927ad3d48d91b7fd3f31dda6e078cf03880e0bbe9cf56abb5919efab7","impliedFormat":99},{"version":"5b61720af63c370f28be29a1e6b60eb3d3cb6df1c169e467ef4241f9abe68272","impliedFormat":99},{"version":"f622b88d462a91a84c41747a8c097347cb08cdc26250ee5c86bec180d4587ed1","impliedFormat":99},{"version":"b9688f18464d0182fce970cf89c623191997178076478d5bf5604724a73dbcf4","impliedFormat":99},{"version":"811068496ebe65d79774a99796223a0ba3efe04be587f6ad9fb2c71865d5fd75","impliedFormat":99},{"version":"1a42ecff9998af3ec2d7465bedcf10aa76420768fd31143fcf0808dc7a3630dc","impliedFormat":99},{"version":"0ff2355b4074c3eaee36f435bcbbfdbcba07ddfb4aee5a2c37a2eb077fd024fa","impliedFormat":99},{"version":"ee053e4fb378dc43221a892e05dd65c664cea6947e5c3819ae41742643d70743","impliedFormat":99},{"version":"eb1359bae2c2ff3dce1258094f234a8de89b7b5afe89bd73e725a46558fc6aa3","impliedFormat":99},{"version":"73c08893e5e403971e0b2dff8f14d9ce3543b1a821c0d2fb8e5ac807a5d14603","impliedFormat":99},{"version":"b886342183fcbfc5fa59216cf405970b992eda93bc8a89094e1d40906dcd399f","impliedFormat":99},{"version":"5170f383ccc3d8431414b298aa1b9fd8f88f9f6ad0f6b40e4f78b5ed98d0f12d","impliedFormat":99},{"version":"a3915cd0ac9bf5441edd5b60f3937d89dafddacd7c8d5f323a279f0647e95712","impliedFormat":99},{"version":"77fd5aedd61211bb0978c3e3763876b18c55b73df2da0d7e17e43a13b3a1a617","impliedFormat":99},{"version":"0138f7ea01346ce60e52fa324df54ddfd6c9c31495fe74ca071bc44674287f26","impliedFormat":99},{"version":"95299ea6874a7330630d89d2ccc3aee12945d0ad632c72a168513f491ad465b0","impliedFormat":99},{"version":"8e11782e3f5c78b28558dd617d0d2a94a524b5106e0e8a6826dd71964cf2b172","impliedFormat":99},{"version":"7238edd71a373627b2d4980472114a75a861fdc5a66efba81e6e61fcb40d3c3b","impliedFormat":99},{"version":"02c11188c7c7ce94d6dce4fdb002d717a9b672ea1293b59dfbf9b54345ea9dc6","impliedFormat":99},{"version":"3cc667806444c6727c1a8106fdaf37c897c25d6a0747fea8cfd817c275309494","impliedFormat":99},{"version":"ed78cb361de97e537cca46c234c5f0bcbbea8a77f88d3270142ed0993373c159","impliedFormat":99},{"version":"e15f2519cc04d4a30a0543a349ce4d4ed10a8cef76e8bb226a624d75b7b24a7a","impliedFormat":99},{"version":"a8de68a1e6c0c3405cff69a6a82dc596d576b967c9b925c494ba30f8782193f7","impliedFormat":99},{"version":"7c6055ad026fda59ef8ac871f9aebcb080cc4ee084af8feb3dab392e603fb9dd","impliedFormat":99},{"version":"3227e142b26a6ea9e1493c950c238446a8d0297b093c8c95261d4f3e63312237","impliedFormat":99},{"version":"8fa71f3316151cd87f46e0f50aecbf39ba424790904b6dfaaec6fb3ad9226c8c","impliedFormat":99},{"version":"f2bafaf9d83cefd7f55bc2a480d1b5ec302e7615b30642bb00bfbf58b567a61e","impliedFormat":99},{"version":"a605659c69bb74a59b480f20bfee716dca83d289a814ff696273f71aea4c4abc","impliedFormat":99},{"version":"5886ca453462c07e9a2a6a835172a5f2149bc2b713b618fe452a94a126eeb36b","impliedFormat":99},{"version":"9f1b4ed0bcf465a6390e7c3760e0bb339f3d61c28e78c95024a3afd3ed81fdcd","impliedFormat":99},{"version":"5ef95b011a154332189243cfcf622787a41f3c646b0e3efeb347006064b91197","impliedFormat":99},{"version":"e6eeef06ce02234a927acdd50fcf396b8dfd8e8ca34c082cccd253193b4102f9","impliedFormat":99},{"version":"a5c6769e0465426998fb1ccba6db4a008cd9fa4a477a8bbfecf5b130c4031734","impliedFormat":99},{"version":"d29f65daae852c87855413712c3e9e81abc8577579cd40aa28789910c149c8cc","impliedFormat":99},{"version":"d4c7c425aa0d74cb74b92a6ccfa740431b48d3240ac7a2219556720a107ded33","impliedFormat":99},{"version":"4de384bb706f41f1515883a6d30cd1eb5d149729a79b1524e1bfe86acd103d68","impliedFormat":99},{"version":"b8f2296cdc94fe0a235c1c73a2f97d394050ca3d74b6b45b07b563a0c2892dcc","impliedFormat":99},{"version":"0ff56c8f9f2749a7c23c130450f0045a19ebdeab9f94f9b6f051e2cad105a1b8","impliedFormat":99},{"version":"e5aeb2f888cf51e86a5dbf26f710608140d6f85847f935b5dd218fc9b865d820","impliedFormat":99},{"version":"f111b5055f9e48983310817bd14978fe8b44600383b2fe0f0f8f82c501034f96","impliedFormat":99},{"version":"d0b4fe6473d64b2505e3b842592c6fef0491b2ec29d3d43c74289428e5649997","impliedFormat":99},{"version":"83e50a8f0a133458f603dcfc3aded3d8e6da48ea71d3a1aa5383c71bd12dfa51","impliedFormat":99},{"version":"4ee9650128cc8493c1cf7d0923004c0c5e6ef374f838dfbbb5f510eb9eff21d5","impliedFormat":99},{"version":"9ffc8307fa2cf2c934da7979c96884df4a36b92b54ca97325494ab2be78fd8d9","impliedFormat":99},{"version":"6c71102ccf27db42765e5a9369c36c4aacd83a78883df87ee103d11be3d951c5","impliedFormat":99},{"version":"57ae79de0948c54c7a75ca0d72d0c360906c1dcf8e6c31f0da121e5a9d382717","impliedFormat":99},{"version":"55cc4ba04c0965890e2e46a092c7b76e92cc9812a12a10d510e75047e5c2edcc","impliedFormat":99},{"version":"977c1704104f936a37c41eb6b0d24cb413e20e62337f7e2be588d3ba55aeebba","impliedFormat":99},{"version":"c75bfc4a6eb3b39957d05a6b9535b424a98bff9f27518e39a7dd4830ceff4eb1","impliedFormat":99},{"version":"f8b79f53b95079b96f81ce308a927651d791c2c3e5f32c103e90fd61fd58d3c2","impliedFormat":99},{"version":"46f6e8a12611611d782904154ebda6e4b6596cd026b7c09e2c145e418fc8a2b0","impliedFormat":99},{"version":"a4560c1de64d671a8d837a6f0313669a7aac4ad7d0efa2be1dfba29f8d61c5f9","impliedFormat":99},{"version":"f193467f1cad8f2384912bbadceea4cdece962b9033488994a7405c29a623d8c","impliedFormat":99},{"version":"a846eff5c1eccb86a3ccda935896a80eaa2beab68c64e68c244908c86ed87906","impliedFormat":99},{"version":"a97588cd01375653ed51d1dd9bdfbd30467b130ad60bb5b6f981ab2170feed3d","impliedFormat":99},{"version":"584115df5500463c3fb9102a3a74bb4cc6a75a9bf6300701e1edf81da292ae57","impliedFormat":99},{"version":"4428ab19274a44693215f4f4195800c8221e1ac8a5a388ea534c4e405fac5f25","impliedFormat":99},{"version":"5ea28a5de3f28607387d210754f9d62d2cacc86684e89df9e74d412bac974348","impliedFormat":99},{"version":"a496ace7d44a315f014ae2711927ff48fd4e3583f4a1e42df356c37b2c890ba5","impliedFormat":99},{"version":"814ac5e9d9bc60c105e022e08d3a7590a5a76c93fce1572bdcc574f25e6dd68d","impliedFormat":99},{"version":"3eaaf422f4a8fd066e5c0717382129da90b5699a86f3326057a2ff4abee0c40a","impliedFormat":99},{"version":"8ec41716dce70226a7069735300ac20691513226c614b03b1b1aa7bf4ba3b2df","impliedFormat":99},{"version":"1ce271cba70878af1e987d3a93fa539c9e35facd9b70580f738242059416c010","impliedFormat":99},{"version":"9be30526f3d636f4b421d3e06983744be0b4fe245025887ba5a032cac68c2e37","impliedFormat":99},{"version":"990e4693b03502adc4038f04ec6a3db2458fa1a998edb9edac75141fe6e8f9d4","impliedFormat":99},{"version":"59507e67ece04e5a994907e68042d704641ae844108f8af9edd82350fc52dc02","impliedFormat":99},{"version":"a36d1226856f16d243adff16b2f5603ea245f505af5a85d5485ab275ca946b1d","impliedFormat":99},{"version":"0baefe65a675b833470b248451ae56f35697581018bdcd2880e4031cdc0cd6e0","impliedFormat":99},{"version":"2ab5a9dab4e1c3780ee8c7ba280aceae11c699371178c253cf5a36341a864af2","impliedFormat":99},{"version":"a80518638f84ea8a15c155eb5841341688ec6dc8a04c5693281632dfe555c3ef","impliedFormat":99},{"version":"27ba80c04731d0ea2f37d6b6f5020e45e60396f5c2f4666e57b03238ef0a676c","impliedFormat":99},{"version":"26ad6385782d8b51674e22f236866e7e08119f50a131cf5a091c8c576202f9e6","impliedFormat":99},{"version":"b8ebaa198a03eea15385326815ff31a01cb63c08bf69517e3ba4e1c8296fd14a","impliedFormat":99},{"version":"42e3bbbb7255cc27270fa12d31aca9f11abb56e985d30c1bde3f978dc01d15b5","impliedFormat":99},{"version":"5cd09527c5bd0e6a9df18ee2a8ca39e1a1af60658e50adfa3b2155c09f731a13","impliedFormat":99},{"version":"1e470d86477091005bc41203654b0f310ca7d45bc0c7952f8f93edc6d0613397","impliedFormat":99},{"version":"d09c441b12602fffde7cc009f9e815eda2e8c85a7ae1fb9dc9add975a935a4f8","impliedFormat":99},{"version":"f3590539d69e5d3c824a3dbb93015c58fdc8d6b90abe7496c770a32f20c559e6","impliedFormat":99},{"version":"4fae33c695361a6438082ebde9374a3779df07ea561b1ce08ae4f514bc6520f5","impliedFormat":99},{"version":"88a1b15f496a6c998663e6486b78ebba1c2abda9e220fd8e377c11dff3e4e836","impliedFormat":99},{"version":"654c457deece232e59f6d73733254cb78bf6b043d9d3c43abf0fcccdb245838f","impliedFormat":99},{"version":"45a99b02acd1f5580b93e48b5c610a60ebbbcb075928cf80cdc9316cce1bf840","impliedFormat":99},{"version":"ddad6719d075ea89cf7434c7a318b69d9bf26c330465ad1315c8ac4d059f7270","impliedFormat":99},{"version":"8c867515406eda8230bc6c852eff20073178ceb2ce94d6b9d0f5678ed13c0b8a","impliedFormat":99},{"version":"b430de30fb510ddbcead861351793cc62ea9d97ca3093dd202d349ee96ced493","impliedFormat":99},{"version":"f4080510cd7e9c65ceccc843f98f68a31fa74443ab7154515308c104c8433a14","impliedFormat":99},{"version":"535919c7119b5e3da3211594c3542946e1b9e5102d78614359cc5d90a78c6a06","impliedFormat":99},{"version":"ef7e1df5f97be321ccccbc8d0cad38c301bf55be0e5528d20d4e4479ec7021ad","impliedFormat":99},{"version":"994bb550d4082528038ce02c9c9e63ea278a31094dd526d91e559990b3ad17cb","impliedFormat":99},{"version":"df03f5b8e177d1c904928545a74d3831c90bbf653fbfdc29241b3ae20a5be486","impliedFormat":99},{"version":"a7d3f59a29ca31f40ea37349c44a7441eb10f1bed068a011760f1204dadd7a23","impliedFormat":99},{"version":"1e7d696c473c1e054caf2a8cd2759d09f442fac69473293ab665d6f8c714b774","impliedFormat":99},{"version":"df9f8f3d97d404cc47e4407b1a5c21c223b30fb6f1f74af05da01effa0cb56d5","impliedFormat":99},{"version":"5e923de3cf0d71e5d9a5ad4e90d4643490c5ac5b3f58ae66bb95f21dc04d6831","impliedFormat":99},{"version":"b486e2e5a4b514d179bb1503178694bf9560ef6b7660a027328b9eed5e73a627","impliedFormat":99},{"version":"d53dfd573a2464c977c298816c36231a62ae3dc27553e27a3c71f6665b269baa","impliedFormat":99},{"version":"6fd3ac9e256753431d1dda6b26de5e0822aa85eec20bc6c14436a177f4279be8","impliedFormat":99},{"version":"a6817b104cff6630183738048e9e2e5ab6c5bbed9d46bb528f80258b0c4ff757","impliedFormat":99},{"version":"40319f136f28e2db970e46b7e72b7bfd5a772a0df98eec5405c47bd6c9917c9f","impliedFormat":99},{"version":"21baccbad8315ef36c9d7f50048abd08ba81a3afa0712ef258fa6c061110e0e6","impliedFormat":99},{"version":"a4705cf973f4d37ce92c052828e2513fa023ccb452f94544931ba2f917b95f56","impliedFormat":99},{"version":"02cb56c5871a710fbb3f0080324577411609df19bc17200156e46a44fdf8d27a","impliedFormat":99},{"version":"57dba29ff984d0c81bb177bdfa07ab0e57dda6dcaea83067ad911eda34caf8b4","impliedFormat":99},{"version":"e576d9071ca12c411674ca573eb789590fb5604ae8037a2572886cfc293282d3","impliedFormat":99},{"version":"4d818da244b409a389c73e3eb75824fc0381b6e16ddaf80a8d15870c597a0195","impliedFormat":99},{"version":"f66b41b72c893a7d90bb8c6399a737e3965292488394040e509496a366edd1a4","impliedFormat":99},{"version":"e1b15aaae981aabbe7a8df2826f4c6f31aa0afdde1e23d15434e2b221a851887","impliedFormat":99},{"version":"10e6b4bb2c98b99ad66f1be41b2597076ada2dca513547292346b211caa31f4f","impliedFormat":99},{"version":"93abea381fd60a80fd3fa282b931d3522bd1e4caabd7dba49152f8f365106d82","impliedFormat":99},{"version":"b771ec60b06611363dd783f8e323f87d9c9432aca465f3cd3d39a6a4bcd79ade","impliedFormat":99},{"version":"baee3182e5624f4bbe9bbdb3512544bd77d07a08e803f36b898c292cc4650de6","impliedFormat":99},{"version":"028de137173a15dd541f16c8fe03c3da3f11a27d0fa1c58c336be102c440db89","impliedFormat":99},{"version":"be06143be469d3d0d5ce6a2ef95e368ae8454bd498d0c0b0be43aa4a6ecc88de","impliedFormat":99},{"version":"6b39082cbf46f9c0e56e73efefea459d85e9431ada4122755e51c4fdb35688cd","impliedFormat":99},{"version":"35ffeb76a74f42b609dba599cc110ff974c04d7382fc86d903fb5eb08787cab2","impliedFormat":99},{"version":"71ebf7cfd3b6d1e628450c875307a3c2b69aa9e94687237bb775a57f25cf1361","impliedFormat":99},{"version":"c0bb78950621e72b06d7e288280b6109885c5f7be8b583659d65e49d9af011fc","impliedFormat":99},{"version":"c6fea1f34d6a312a51ead56d2d9dd487cfe2ed243885b2da89cf51c1f4a3d811","impliedFormat":99},{"version":"9789a429c5349144946f1aeb7578a9cf517555e7bd2a5f8fa6889e8d58a8847f","impliedFormat":99},{"version":"e8388203c62a6cc3e00a4497fdf292b96954c99aed9621197aa7ad3a9845e3b8","impliedFormat":99},{"version":"0e7e5b08261dfe5ccdc9d9cf8b2aea22fa5345f96c317f03f5a0977acbb99e32","impliedFormat":99},{"version":"db4157be7d269a6daab5481918c15fbb5136765c3b8bb49489d3ee9626491b4c","impliedFormat":99},{"version":"b3fbadc1bc66794f535b892f5523ff3ae360d65ad72806bd24ef4602ba81c2f6","impliedFormat":99},{"version":"1f24cff6fb79847abba14795c5778f4cb1f137efb4e353e0a3fb1945e725290a","impliedFormat":99},{"version":"1056e3bf6369fe009b40647744c8402df6d0ba1f2d3a462a2aeffc0c83027934","impliedFormat":99},{"version":"b0da0c17082d1fcd42ba9a147e0002ce2f8e7fe8d7d1e2e9810b14331ca10cf7","impliedFormat":99},{"version":"f8a0dada5366f74f29291c9e6eeea48d81bf3b6dd7fed6aba386e60ecfaf2057","impliedFormat":99},{"version":"c8e93478ba85d6a953b7950a530a3228fd2b0663e76a42551fcf29599e5ae269","impliedFormat":99},{"version":"4e4a901be180e8174ebd0115037bee49eb8d0a5123e81f3a57b1b230f2fd7bb0","impliedFormat":99},{"version":"f4fe5a6412f5c4c2ce54119325f77804a5ff523d5e5d935e8a3c34c2d55e3a6f","impliedFormat":99},{"version":"da16b5d7e423e5e0096fce83b5703fd052ecadbd39573985baeae406a3ff406f","impliedFormat":99},{"version":"fd214d4558f949dc7cd55d2657db975a9272ad99a0a02f31af2100df72b0099f","impliedFormat":99},{"version":"ed2438ea4315e3db2767e79082e0066845c46ba040724ecb49b82d0597057987","impliedFormat":99},{"version":"e086358dcd0e44204dd616f050029b80e85d3df4e454d772ffc1580d2c42205c","impliedFormat":99},{"version":"b992f4b97e7284088fe8cd40efae6ceac1271fb8644173ff6015fd970d916818","impliedFormat":99},{"version":"cdcd99a7a4fcb0aa03586e374cc03c29edf192d79210d46ef2bd97a4e2ef6bcd","impliedFormat":99},{"version":"dd1a8059730f96916cceb5d6c734f59239cdc7aa6f5b0ee70fddbdcde89b1da8","impliedFormat":99},{"version":"82d3606bbaa51acea77cdfdc2e3199c4686ec2c6f71927972ea30ad6fc7b4a62","impliedFormat":99},{"version":"7588bf02783961f2b6871ecc6e9c60363ac79a0ea63ddfe4fffdbdf98a628a8b","impliedFormat":99},{"version":"f4ec160ed91e9bc5654099b8c9fa3c549e731c7a3f0de8d2cf14982dd126d1eb","impliedFormat":99},{"version":"330874360097f09e8cc4ab9c19d0c29ceb099ae500ab5d6461c9297b8c3cf0f7","impliedFormat":99},{"version":"1c4da43c55f396f8fba432a8827eee9fac8acf6c1494a57ef48177b57795abe0","impliedFormat":99},{"version":"1ce9891c6838a553f39b2ddc6f83075f3850a512475af31beef974f6f7cb292e","impliedFormat":99},{"version":"7911c13a1992969635e30dbc94eee146269dcfdd3e07e0e265daf5caadbd5f0a","impliedFormat":99},{"version":"6701c151b37be87bf02126312f18e65b3d91a2c20a7377833d33d919fd6df9c6","impliedFormat":99},{"version":"2a2283d33979c27b027ebc07e65a8f09d2f307f7a9efd1e07f0b29d88f787aa9","impliedFormat":99},{"version":"3c97002cb68bd560118a6c8cbba6ff2509bd73d87dd713edb3acd83e66bf4e35","impliedFormat":99},{"version":"664417e248d43d27f8a04b9ae5e1f3ddeab5aa6d9b41284ffd366705f18f4885","impliedFormat":99},{"version":"019210cf7ccaf627a430edae894d25b312db658fc0f2c282c36d04920b3f5ae1","impliedFormat":99},{"version":"a2ffa2e737e5b56c638654d4cf91fa6724b1b95c7be50a82b7478dea04ea8a7d","impliedFormat":99},{"version":"09abbe2cca1bd3d538def48c19a2fe62293bdf1f97504a52e26d40a3898144c3","impliedFormat":99},{"version":"3649547b460e5c107b12f6f29be60d9a81a59fb3034ef197979a694c687e77f1","impliedFormat":99},{"version":"f15d3906d25d611062b5c89d3145dd4a2ac60bf68f6f5e7f4d48d1189e474f46","impliedFormat":99},{"version":"6661f2e5e07a92e4267191cc689ad3930ebcc316cf44221ae8eb951e3d4612de","impliedFormat":99},{"version":"076c0467d216ed21904db5ac256da6a4fc1115cb870857be9905446da9e81341","impliedFormat":99},{"version":"1f659ae61eb9ef9148eafa4ec4171525da55c35a298b720f215ab0a89c045b8e","impliedFormat":99},{"version":"91753dc66ca2ba7e922513c6d73c9dbd3d6336ae1ba310e2d0520c99b93b99cf","impliedFormat":99},{"version":"ba087ea4f7c6bd22a19cb28e433a592ade91ebdfaf0d37f053051b81b3fd194c","impliedFormat":99},{"version":"490f6527eb1504768f00b4026ece4d21edd1949d5eca11bb76132330b5a8a55c","impliedFormat":99},{"version":"30c18c79997adc72842a146fef84249191bd8ee28f3456605c014b1e316dd7b3","impliedFormat":99},{"version":"fbd2d2f4374aad855367713ffce22d19c87e9483da420533084c0c734df1523b","impliedFormat":99},{"version":"b0e8320ce37a2a4f1cabd3d9c3e7c72065b847fba43562fa7680e436bdebea3e","impliedFormat":99},{"version":"9a6cd826888b63061bda7c34e6bdb871e7c0b01c344f15fcc3330cf5c0f99594","impliedFormat":99},{"version":"36101c7781871da39fcd8939e8faf083c606a896553fdcd7eb4b2f7c6ec47a43","impliedFormat":99},{"version":"ec266fef2d1fe9b7c49ae608b3ac60c4713d8d1cd2073c92f5db1a73a39556cd","impliedFormat":99},{"version":"34bc73c461e18692931c22a7afb38d308f7deaad9f21de13ea54dba0ca4336fd","impliedFormat":99},{"version":"574f30981211d1f1fdce83fad7c9b093ce986124480ce09ca871633ba6a17489","impliedFormat":99},{"version":"2083cc805a15babbf2d0abdf9d702fa6f75711590bae3fceb699ac26a4c76621","impliedFormat":99},{"version":"c51825d37dab66cb975c5442706fe6451b9213e098e3ff5f846e5a77539a7421","impliedFormat":99},{"version":"9068ab90549e1a5c0f165b9798650d60bd38e7f8615c05862edcc7d6dd10d5df","impliedFormat":99},{"version":"9def60b8c5edd45e81d01f89c7717c92584cb821b0fb14fbd025aa10b0ab3bf2","impliedFormat":99},{"version":"c7a0c488560b1dc8baa8c7f399cc474a221f7cc7b7380d0fe6014cb35f807840","impliedFormat":99},{"version":"81393d54e070aa48685972683871a707f160f408622954ede5d2a2f34ad332df","impliedFormat":99},{"version":"238e9e20c91fc4ccc94d52291ecef44762188d515f649095ed0ac99d59ff4fc4","impliedFormat":99},{"version":"77da76559d39664d9f60942ae9c12082b4fe475a67773bdae73073fd11b3888d","impliedFormat":99},{"version":"cf1189bc5bf2286b630f33be24872477888ba5d270b763e8f240a20e0fc5957d","impliedFormat":99},{"version":"f8081297cbe7510cabcd2142cc09294c076194a782ea5422c1fa5aa02bb662b6","impliedFormat":99},{"version":"1426f595ea21e6da826b69bd69c046e7443a77bf7a4434a9183297f46aabe509","impliedFormat":99},{"version":"79a74245aa7b853b8b5b0c249243529340426691807c11697106ed49d8c96381","impliedFormat":99},{"version":"e849d81b6db10ba5f8b89b25ebe91fbc005fb531b8cb417b6dda49b5263bf485","impliedFormat":99},{"version":"b19fe3a95a628557bca5b5ba33377870602b9c24bfb4eef0085fa0f04bd8f7cb","impliedFormat":99},{"version":"ebc64c6ca9a2f7942c0aaa51bac5d44d5d714e562693415a158dd409c2959de4","impliedFormat":99},{"version":"fdc0a465ffda280fc5c52ef5862816082f737f07f24086d1899b3b90ee03581a","impliedFormat":99},{"version":"e15e7ff9e28153e0c1c0589f35d82929e7918cbd60436c837c5604288f1572aa","impliedFormat":99},{"version":"1466a57f95e268e08d94cf92a030e469d4e6f8c180951d9c0fa453ab0a38e1da","impliedFormat":99},{"version":"d234d63170c7f6c82460feb9e8e7df14e38a694e90bb780f5054081847e8a971","impliedFormat":99},{"version":"5a353f55cd755eacd87196e6d14e8e8cfaaed147b08b19fe7e8721e3cff7e4e6","impliedFormat":99},{"version":"ef65b08cb958fa2bd14dac358334d8f500a3583fdb0f5d0d36eff66c6d2926b2","impliedFormat":99},{"version":"bc23a8c1d5732b76eee7581bb2757a98cf02048426f3a6f94632ad25547848a7","impliedFormat":99},{"version":"37820ebe50ca6e567034fba5022c1b2b35fc2dd748ffdab1f6a267696e041729","impliedFormat":99},{"version":"b7f56968a5aca1b41d82fd99d0859c366bb1e24c21610573ac69717c6c69cfcf","impliedFormat":99},{"version":"90889a47a83550ef4d6bb79f61a46f790318a5810874848df91ab1ce83bfa5e1","impliedFormat":99},{"version":"09ac02d7d979b4bfdd333f1133c60731f8e02f77ede93a58eee7c2e2894f8ed0","impliedFormat":99},{"version":"3a9e9b5c7ccef9aa106e8d38d624fc1ef7b61023f1cc99e49e0d34d806717aa2","impliedFormat":99},{"version":"280ca67f0276359a04f91526e7904723c634b8307f36e88b8ed1d55174d65646","impliedFormat":99},{"version":"56380192c5502ce3cbdaab811c03293ae0a3477dfec5ad999e0561c7a55cc049","impliedFormat":99},{"version":"a75dbe057fa7121e02effb45fab47e3c12d43063707c633be18c92b0a863a37f","impliedFormat":99},{"version":"813ecfd69412e80b3dd7bbf45217ed1f76d70a939457bc4213a38db216bb6e4c","impliedFormat":99},{"version":"8f4accfb1ff30808a2aec06f15c493e88683a576c5ef4871388ce7cf0dc80da8","impliedFormat":99},{"version":"1cfb8467bd39e186a94ead6da6a9549de18583c998a3d6ca7a7bf92922b7cfe5","impliedFormat":99},{"version":"095a8f6c16d323c4f1899434f895179bf3a318302fbbde8e25a0caed37832c41","impliedFormat":99},{"version":"e68ac7298d9feab31b040212b8c81d47146c89e2f0b044078413ab84dfabe024","impliedFormat":99},{"version":"669aa01bd96f92b0fa1f9f5dfdd7b974b8823e876d690d2aec1c480b70d2d178","impliedFormat":99},{"version":"1ad942280beb8fd7cb99508d5ec9ba3c0034523bd9ac8cb6059cbe9545194977","impliedFormat":99},{"version":"4429304845afb8cb868836562673380e3926380bd4600fa7a0a3ca8ef546cb7b","impliedFormat":99},{"version":"55d10877f0755054ab010faa6273fd064165339b9ff24e9f26a9a57b68a24c3e","impliedFormat":99},{"version":"510ec596cd8c9dc087ba99465428d24b027f504df7d399d97fc971d0aeb961c8","impliedFormat":99},{"version":"59cd0cdf1fa93bed733d6d84b0d5147501d87bfae8ce4668b4b093c22e16c563","impliedFormat":99},{"version":"854a4ed2d2ce9680a275cefab3ff654fd76627503ee311d19c0e2a25069627f8","impliedFormat":99},{"version":"edc84564b1feb469da3da5b7e1efcd6f48fa2e04f006f07aeef4c6a5659c03e0","impliedFormat":99},{"version":"b076daa179dc64180ee9538fafa4fa6e5796a3a81539eb3349737ed08d57e2c6","impliedFormat":99},{"version":"80c166471e9529f02214e75d8bb590870f822fba82a3f32cae59795e26d660a0","impliedFormat":99},{"version":"25aa189f1265f4674b7fcaa43004d7c7cbc46b6e1f3460fc413f3cac61c77da1","impliedFormat":99},{"version":"97b2bc30456543136698a248328f804aeeae10ff3211f5368e96bb9ddf532272","impliedFormat":99},{"version":"c9f27874d3107b39c845a4380551ffcdd721246db1c82bcf43cad86d66ea1249","impliedFormat":99},{"version":"451418c8195f8533477ee9f08c4642f7a8ffc9813d73a747d77fa70eeb66a79b","impliedFormat":99},{"version":"e768ad0e15d014a0a09e47dfb4c3d6c9d5247b186ec2887f07e984eceeeadeed","impliedFormat":99},{"version":"77d3636c84e22e5692cea961ba0957d687aecd776ca48cfa5f1c38f3fef6ce26","impliedFormat":99},{"version":"119c30d96ef567d5eb44ecf9caeeccdfe4b4af9321fe687d10b238f64a1f16c7","impliedFormat":99},{"version":"a4191368ca3082ba97bc919f8de837e29cedd508f11c20d559421ed90f5648c3","impliedFormat":99},{"version":"7424fdb26b9156029a3d0282191a16576bb1656f7e77c1da961b9e44e9fb502f","impliedFormat":99},{"version":"94e8a85d1d6ca2a4fe056f231dca96691acbefb9b0273ee551109bf69ce53f11","impliedFormat":99},{"version":"b78d4a5e2cbc7f6b244155da9fbd30d6016d225739abe750b47d443ad0057289","impliedFormat":99},{"version":"289db195422725ce6e021fcd4ef250c4702248355515ca1648227723a98d618c","impliedFormat":99},{"version":"41f3b546b4f1810ad4ae11e8a32664e70ec2b118d72e7d6cf04c85ae29d8a43a","impliedFormat":99},{"version":"ede452edea60a6ba0214f4201f5b959d57e7c26a743a5936a05f182a4bc55930","impliedFormat":99},{"version":"0a52552297c1d0af3470ecaa85e504b3f87af7fb327da36b5ef63ba06f135e20","impliedFormat":99},{"version":"ea7dbb28b1ebb092c8f974336a33caee5b29145b699ac1e8ce765979790bbb35","impliedFormat":99},{"version":"acc64e8177b7e4fe1a2545bbdf2875a89852ca5b042471fb05901c5e2a6d8fca","impliedFormat":99},{"version":"eea6ac739630bfb73b2e59bdd90d6f278a7ef0228f09525fd6adaf09d90f7280","impliedFormat":99},{"version":"d60137c38ec70f4d3495e2cf71ed73bf75f405f78c5c8644165bdfb0d9eef528","impliedFormat":99},{"version":"b437a4e1ad5b26d9c0d740ce71f956181c74bb283abb0d2510360ff065b0ff3c","impliedFormat":99},{"version":"7639369a2b3ad9cb82a464144871c8f90990443f92320c1fd3fd777c2ff5bdc3","impliedFormat":99},{"version":"c058887683e283e99658518eafc8ec08754f5b16792662b09a638d5a317a7719","impliedFormat":99},{"version":"1dee7843ead6b1135839649de81ec125fa95b5c7c80c52b8a08d8fb7c40ece9e","impliedFormat":99},{"version":"e3d7986bd6b3e8949cf921f9a50fd955da64d399dbd817f5394f772f7d52b6d6","impliedFormat":99},{"version":"4115d25268a762ed4b047dd9d5e29f46687ba84049cff9fb4c68b18d60e910d9","impliedFormat":99},{"version":"1ed9ce51e9bcac81f9d2c4d27ac2704c0a2267917fbd3dc6d207413cd7a6b272","impliedFormat":99},{"version":"f13cd5883699bdc863db77c15ba94050548e23e98fa5a5b26455fa105fd15549","impliedFormat":99},{"version":"008f7e03cecd8c3a12015053fb8714b8d738d35a35ed1bfe4c4b02be0116b89e","impliedFormat":99},{"version":"d86f6bbbc83f4d8944ffba72ebf5b486afa462fd26b9e04fbb199b4d2dd920c2","impliedFormat":99},{"version":"4bd9c7cecc7b653a34f633729cc1a5a954f95320f2dae63c48e8416a625b36de","impliedFormat":99},{"version":"765f3ebb07e1f7568ef6ca14d2e7f80900ac5dbab6dd25872f16e23fe5ef4d34","impliedFormat":99},{"version":"6cc361c360c6fe2723b28810e8725b894d9c88e4c4cbc197a8a6ae304452ceb1","impliedFormat":99},{"version":"386d783e5c235fc03eb9d3be33a41ed3a3744708c6f184fe9618a25b6caf9a58","impliedFormat":99},{"version":"fae4c4fcfb20aa2860f880aa7807d8d8102abda026c539c261e4da53997d9659","impliedFormat":99},{"version":"a4a256ff9f03798c49a001a0d4bc708e96a20be188d80d3f4d6f45e1d2480b91","impliedFormat":99},{"version":"80cc57ed8b2f7b94ac5d6b61ffa975eac13e39cc55ed9d32f2a4c03b07c9ce2c","impliedFormat":99},{"version":"6a1c4cb49ef0a86d4985496e9250de9fb38f286fa7bba5c2556855398e24d98c","impliedFormat":99},{"version":"94613cc30b4c60bc7b5e41f0014f5afc1f70238a9f3112400e2ed802b9edc2e5","impliedFormat":99},{"version":"81b5b7d4670aab0d1999b8480d483e3b0d12a8ef6a47238737bdbca5cc5b3af4","impliedFormat":99},{"version":"0a75c44f704965ede175c60995e6ef37943ace985c82b37ec004a02a23d6f54f","impliedFormat":99},{"version":"dc6fb79c45f464f758e0e2b6a09c34e0e88c980e3573c04333639d1061479dff","impliedFormat":99},{"version":"1d49ef9a4c8784be6dd7d91afb621b78c0fc8017769b9dd846ca63232e905467","impliedFormat":99},{"version":"db18fc8ed779ede2de97ba0e9bc61d13d1e971711bb85fa085d2be02376203dc","impliedFormat":99},{"version":"d92de62f96a0cf43b1625552f278468e06061453105a5b60ff78c6f09c30ad8b","impliedFormat":99},{"version":"4a0b017378d2a33ab5c70d2d885b9db3c10846e1f0f9b685e3be5edb59ea3eac","impliedFormat":99},{"version":"1296c3ac64a45bb17d8c90629bd2241c5942ec6ec51a4013ba540f2aab85eb07","impliedFormat":99},{"version":"7ace98c6830758e1f8e22def1defa1b77ccdebfd07d3508a1fc455cf098a62a8","impliedFormat":99},{"version":"e7f716b3a0ff08e8d289109d49b76e90644247d65a80cbaae2f476d3ca954bf3","impliedFormat":99},{"version":"5f5f47b68b781eb477da7e844badb481b34489a6e8686c1ba155bbcf2e7a133b","impliedFormat":99},{"version":"d863e0158ab6d1a5e8f315f879c255c7c64a4e45e5a980c2739c73c8ef9b1813","impliedFormat":99},{"version":"bffcf294ab82808982b50b77c7ed624a48b51b362e5108c98e3dacdd6b96605f","impliedFormat":99},{"version":"9ead7db9d2f12cb8ba534475698fa8d5cf6422e6ea0e77584b31e59ecf25eb63","impliedFormat":99},{"version":"1e2c2fc640fe117bc83f6320e5fd181f6e51a86a7eee232d510ed0c8b2909091","impliedFormat":99},{"version":"fe6441d144419863eae6218ec1ab1fb52af6c4f2f2c9993980038501ddb392e7","impliedFormat":99},{"version":"f928aeb71d76a99ebad7fc109e96135e497989ddb0c0ee16465e35de24f0cfc1","impliedFormat":99},{"version":"c883c7b209cf3e51a693023de37186e199d7e2342cd41f1f0b8ab9ca34851cf6","impliedFormat":99},{"version":"0fbe381899728959b9f565371a95921f76e4255dc9813ca95a423c7a81cf157b","impliedFormat":99},{"version":"b1dffe769906190c61aa2de29ba423c0d9764f05aea7ebbeea97f8323f6bab95","impliedFormat":99},{"version":"5f697a3ca1e2ce84e63f5c6fefd55ee00f4608a450aa765c923abc907f0032fb","impliedFormat":99},{"version":"6c8bedda58d1dcd5cade3495643fd5a1d1e6af75f50b961922b4214003c34daf","impliedFormat":99},{"version":"3d8ca129295708074c35d06f4e9b40e11e9ef3233df0dca6ae1aae5007bc1235","impliedFormat":99},{"version":"310790a0b20dca6d4fa1884837f48775ac1a7b711dab75bb25e203544c82a46f","impliedFormat":99},{"version":"03889f2863e44d756e96e03ec365dc1a299653897025ea1ded75e99f4183026d","impliedFormat":99},{"version":"66d50cba2ce96e5ff08affeb25d1ac3e6206887585a650ebf6cd712935c7bdfb","impliedFormat":99},{"version":"f72510666ed2d0ac50c3999c90e6a628ccfddeb123783759aedf8b64afc472c7","impliedFormat":99},{"version":"82d2796cae92487b15c2ce8bbc220d9d6ce8e6dbbe521b08f1d52eeddd83627d","impliedFormat":99},{"version":"66b417c8ef1aad8f6f4c2185673d0a3702aae2bee03885edd91a8df598d087ce","impliedFormat":99},{"version":"a7b40cfee10f0cde9d2d0d368e143a879af2fb8eda8dd86d722010b37d67fc22","impliedFormat":99},{"version":"584c9a3408fa4608c3f267c88f9942b23f3ff65a14daadf3eb1d24e1109a34e7","impliedFormat":99},{"version":"3933b9179508da74e2466ed5af41790695e53bef479533eef408ab34c08c3805","impliedFormat":99},{"version":"50aa52ceb6b51e337f65deb87aa418a7c24794fc0f9eabe41357461fa3106dd5","impliedFormat":99},{"version":"f6867a201454c8b60a78672c53367fa9dbc1aba247db27156c3e5a2fb3cbfb55","impliedFormat":99},{"version":"2bb7ed42f0bb306c77fa17ee006759f2f3cc6b6817fc4337d729ce010e7f8fa9","impliedFormat":99},{"version":"05c7c9e1d12cf2e7ec37dd86fcca61e4b1b4a2fff3fdf3648ac7210b5c09d944","impliedFormat":99},{"version":"8acc21ead62f0be0b6f6415e6dcf89ca2089d262a5d9da84f4688c9a37a9397b","impliedFormat":99},{"version":"e6b5b68af5046e3d64bbff45d3b41e0fa5da06f85efda33a07aef32c2fa4b54e","impliedFormat":99},{"version":"bb3b6049fb550829d304ff7024150cd4acf3650c11d9230e52d194caab19393b","impliedFormat":99},{"version":"bb119081db62488bf65a9563919a80d270201a1aca45d8ebd25a6eda85af1b78","impliedFormat":99},{"version":"abb85df073721c70450b1af3713cef5ab1025a36cc1566c31cf39475c512e842","impliedFormat":99},{"version":"8407d00275c737789e3e526a2f63542d43240d39cc813e0f391d0791e796751e","impliedFormat":99},{"version":"678b7873a38230d98a4ef8879f1b84b658b01506519e05a0291e887b914a4924","impliedFormat":99},{"version":"f474bc9e506a7690f97b073c4c93f16a67fe1859a7610cb08b54513b62f076e9","impliedFormat":99},{"version":"ce2b604e7fedfa087e8fc68f7d850a2208f2b0e5393db59a4fc7e21ebf04fa60","impliedFormat":99},{"version":"286b74c5381464962234a6e6fb7e41b56434a0466c0b8072f7762d102941e4e8","impliedFormat":99},{"version":"781a8c3f0fd5a7bb5c73ef1a804a09167ee6e72031be68af06d09a85d38003e5","impliedFormat":99},{"version":"d514bd8b93d377b46f889857af29d5a56246697136b3640514b376e15c88d9d5","impliedFormat":99},{"version":"e8e5d7cd52764357a339835c34327ebb8634c5cb932d2709f93c152f2abc3129","impliedFormat":99},{"version":"79583eda9e4c9b3807c71fac738a25625b11fa7ed587b2d1b5be69341219365d","impliedFormat":99},{"version":"e839194cdc481038bd67ed3c1566f5d29390c2af98f6b580115e7515ff3120e0","impliedFormat":99},{"version":"f347639fa958762371b8408e36bb5342a58affa28768131a834797c2a9ed07ad","impliedFormat":99},{"version":"053994ec3f7884ddb63bec643bf2276a4da93210bd12926ce4ea51cdd2f1af00","impliedFormat":99},{"version":"38bcc4a561e59c0a0e6304fcf9ed28df9a18471a06f7d25d4d839aa0f273b4e3","impliedFormat":99},{"version":"fa83e95a170c109f9e52f892a28a59973a8454979a2cc4161c765173e67f86ab","impliedFormat":99},{"version":"08a5be336a00293970580fdaf71e1197054ef2c9cd87769b19f7461cb3c617a5","impliedFormat":99},{"version":"ce1ec8f1a46ac13b31b36828b903ddc8b5f0a4f1cd59d6a1b122221fd0da4164","impliedFormat":99},{"version":"361fdf24d40b6d7f3250dac622b9d37becb257e4f855f85f0a30dd6558d50e99","impliedFormat":99},{"version":"1758b1f8898f121a176226b62e39461eef854bd33ed0552991c64cfcfd237826","impliedFormat":99},{"version":"7ef114c434371bf0bb373d8afb5e7cfdc5e1b2678683b92b89588a359bc01d46","impliedFormat":99},{"version":"56ec6bb075d116568daeb44a00ad710e9645b9e383fb61d83078a9e0d59ddac2","impliedFormat":99},{"version":"4d62e78223979185bf0fcea9a50e9eeb81e20e4df1f924511efaba42333d39da","impliedFormat":99},{"version":"170475025a9322720723114bb937bfacc4e5096666073e99d7f1df897cfaa46f","impliedFormat":99},{"version":"9dc2f4660f991088c1df79af054f451e7763b592b83911b2f55865282e5ffeea","impliedFormat":99},{"version":"3efbb5dbfabf41264c36e3e8c154ca2ec5a28dbc3dae87eb299ce826cec00eb5","impliedFormat":99},{"version":"ce0d485a733403bf3c9a552234e183b2f6b4f2a9f392ce8593ee52d09a7b4227","impliedFormat":99},{"version":"21d7d9fe27f34102a480656861392bd4fd37ca27837f9dca3542854bab35ed92","impliedFormat":99},{"version":"c0561d3440cc84a4e1a289b392dbde82e307d89eea71449c3a41b609daa9715a","impliedFormat":99},{"version":"ba519b143a14fee5242e9e8e549c5944bc6e6361af1162363c8336bb83289708","impliedFormat":99},{"version":"ddba8d73eee6f39c8a1458c244b9bf5cc54749f9d96b3e4c020bc27587afc05a","impliedFormat":99},{"version":"abf16ed784c74fd46c656cac5f5f7128a8fc9ce72bde1377d83333b32e160824","impliedFormat":99},{"version":"297f7c98f8447d6d8171c2c830a5769ac5d815ffb1a41b31ceba551b1869f91d","impliedFormat":99},{"version":"73bbe80f02235433340e10426a1a30e4862f302d8a4773d391544c47f6c44a59","impliedFormat":99},{"version":"091f41ba847167078b84a31ad0ff781613e22926b1aeb2bb9c5a5b81829e950a","impliedFormat":99},{"version":"fd8c79b9432d4c75dc3a776ca5cfa6c0cb01579667604ab7d3b564adfe02a1cb","impliedFormat":99},{"version":"a474af2d3c46c4ffb9ac67c35794d3188c99d0a9e29ebb851c925e966376d867","impliedFormat":99},{"version":"43f3124b51d65427823ac7ac1a77ddcf9d1aaffcafa199307c8359f96579d956","impliedFormat":99},{"version":"8b5edc0c16524d7ce61e374614f7be58eaf72aa31779e7995d75a48d8d321153","impliedFormat":99},{"version":"833d94d5f9ba3c0a0e0a9d98c79f4ee0ee099ef6f1aa6c594f98e9a503eb20bd","impliedFormat":99},{"version":"1073447c7d241622e16b03261a5a35e1ab9511a726fe2e9d9472a98531e08ebe","impliedFormat":99},{"version":"678498cea62d90c7b6f6fe6d77d6913a498207292fc5ac085c3ca6673e1ac390","impliedFormat":99},{"version":"07ce6472819a84a9553d35477e08e843adc1c4e06f706c32aaa6bf660573abe8","impliedFormat":99},{"version":"b49acee84c7c1e58e404d674f3fe8a72a73bea8f21a3384739ffac376dbc4d9d","impliedFormat":99},{"version":"e05f80fd2fd9766d62e58ca19f342980594df24b67d046b2407bdbbc642c20fc","impliedFormat":99},{"version":"31150f240226c796b061e96be00586546e8691604f5e8e074b6eaa1cf7ccbda8","impliedFormat":99},{"version":"bc4051bf149154816a313aeaafccd606224c7dcb5797faaf1461c7eb7b9dd32a","impliedFormat":99},{"version":"8384d125cdbde45b18b143511eecb4cb1a630393b20e0f384da7ce53ad500200","impliedFormat":99},{"version":"bd1bf0a339f399d23c42b3c71077411fa0516521cf650f2a979b6b3ba1bd5776","impliedFormat":99},{"version":"7382c0c8f0db3389d4c2deb5555a6a13510cb2f3b1299a5be434c78959065adb","impliedFormat":99},{"version":"28d190c786629e69bd4663183c7f6cdc6f6d95661abb33c4acf8e864bb17d6c2","impliedFormat":99},{"version":"f7dc29a70b2c22eb0ee37c58fa13b92c56a03cb8d07e517e50745fb8f3180fd0","impliedFormat":99},{"version":"2f7ff4d8be24993c494c9174d6d0d567a2f777d47ae605bdcd1f369561d3b1b9","impliedFormat":99},{"version":"3c5011b14981547a762a9a4b23494d3bc56000e016c61cf288cc282cab4565a9","impliedFormat":99},{"version":"31d6f6e05ea3be94b6d0958fe20585c624e37139efb2714c6d4716ee2e83e39c","impliedFormat":99},{"version":"46380cbf7064fd9f0c68902243e97e6c50d044c1c2075c728f9ec710b3d4764a","impliedFormat":99},{"version":"ad56ff5c498ae19096bea2f2e48a483496a69e7f882378ec702d0dab3431adbc","impliedFormat":99},{"version":"33ac964aaec5d03a7f2bdf85235133d8f8a542ee8cd53cdb6370cf12a5fbca1d","impliedFormat":99},{"version":"cb525b94462aaffa1953f40ca64f30af02c056ddf190a005c9372140edfb1520","impliedFormat":99},{"version":"f9c4cad499989bc9e52fb20fb79ddf145e74f0bbf658c11e632ff27784df8d67","impliedFormat":99},{"version":"8b2416a382ec4e4d165f2eb918be0656e116328a89517376a344e155f1f20574","impliedFormat":99},{"version":"c2b0b8ab0839ebf444fb041be8baa98073cf24e7d34e9d7b025283bcc19e4f6f","impliedFormat":99},{"version":"6b83038103737e8a2a62c32a504cb51ec0ae8c290245392b1ed3ae6422dd92ad","impliedFormat":99},{"version":"b8c56895889493917c83e010b52a20503a577fb619bd2dbf93dfc96194d1507e","impliedFormat":99},{"version":"a346e366f5b0bc3b7ed1246b37970ed4cf285ae542a9322fa570461ecab1ce50","impliedFormat":99},{"version":"ac08e7ce22f77cb1c3e394319bf6c5e91765260cfc1592fe4ec98d7b6dfce063","impliedFormat":99},{"version":"936087f474e826b32d78e7207349207d340d684a888366348c4f21e588b80af1","impliedFormat":99},{"version":"7a62127857134b78a06fa3eaf7c5cb640aaeb4c5f68b286dde92530b0d91439a","impliedFormat":99},{"version":"d68366828b46684cdec1e5167d55a37eb12ba413a8dfa7a9f3bcba9b0eedda79","impliedFormat":99},{"version":"1e29f487dba82fe1e01581eaa5452778335c8e8035dc7bc897a08438349e015a","impliedFormat":99},{"version":"436e04e198949a9de4d12437e9a0e22a635cd572f6a1e7569c69367aebd564a1","impliedFormat":99},{"version":"2de8f43486975db9345e8671658af70b6732714b921338efeee75332fc9d2b9d","impliedFormat":99},{"version":"a6042f5361a7d639267a950bfe7b8071c8fac0a22c3460516aa57132e5e8d783","impliedFormat":99},{"version":"994706f0de0e7cdc500fe2df830df39e8673b95e21052b860833bafa47e3b3e9","impliedFormat":99},{"version":"eceeb11208e29be4ee959d8601d44a160e5891acfaa2890ad3381d9ca4552a3a","impliedFormat":99},{"version":"1d4ca5e0f18a48b4c9bb13d191acf780c72d6364f53bdd5cccaae3da8dc75ec9","impliedFormat":99},{"version":"9ce599bdf3ac0d33b85546886643ef3826894f290a8fdccc09f34673a773d585","impliedFormat":99},{"version":"1aded17d799608c0ba976d9c017b71b552428923ae175c4ce2d03c6c251fc293","impliedFormat":99},{"version":"110a028c72500ec8975f8fe8e9c58bc3c53ad1b8cecedfdcc9b13d01c82f9fc6","impliedFormat":99},{"version":"0f28079d1c3a5b121158ca2992b65c9b39ea578d10f9a660edf19f0a3df74d72","impliedFormat":99},{"version":"6c543b666c02de0cb90fba1b177ad2e0f38c024b2d35baf7866d88a772335013","impliedFormat":99},{"version":"22ec74740daba4895b82f71ee7a81e8d84ebf8ca87d750a9cb7eebfb53715cb5","impliedFormat":99},{"version":"a99162f184a2eacda687014590c6cdb127b309361ca893876df04e6f42899933","impliedFormat":99},{"version":"a2e7959cfc8d0b1e2a8fea12946fdcee9d3d39ec83f9f69fd4fd3019dd729b1c","impliedFormat":99},{"version":"f75cefd211302ed3f67f21cca8fcec37a261f61aa74c8506818a594d63987c50","impliedFormat":99},{"version":"0bc6eaa27ee3b289d58fd31f9597fb3786fc5109a6fd247f2d18025cd8bdbfdd","impliedFormat":99},{"version":"2c0c57f8d531e350d0bee50b45fd4979073ad3e6770fcc82ee5d9434d33f01be","impliedFormat":99},{"version":"657ac562004aa622ba5adf699048ec590ad1b4c0c47c77ad86b8d3f491c068ba","impliedFormat":99},{"version":"b95426b6a6ef360b38ca32ed9a16d827079eb9f9c2e018be263a6f9a6d4dc62e","impliedFormat":99},{"version":"9c7501860d6585a6fb5d1021c57a33bb953398da637c5dec0effcd7877a52bfd","impliedFormat":99},{"version":"618c2d571eeea05cb19350eb01fd25e3362faf19fba3465eda421d8ee393bdf3","impliedFormat":99},{"version":"55430d53d77e54aeb57496d12b469c03d1970776db91e2ffebb9746f67f49db3","impliedFormat":99},{"version":"7aeb98df985447d55fe2c79d236a8ac5d9298828f5f64eed23943e28c91aeb1c","impliedFormat":99},{"version":"724c015bd6ca5e48308f10bee2f034ab5e0a5b13452e36bf05fb5f455a1e9ef9","impliedFormat":99},{"version":"8637d71c11793ae5cdd3171517ab0453ddaa84a916f6069cc7d21756f01c9a13","impliedFormat":99},{"version":"7b564c8ffe25811dd0300e0fa16d9e1bdb57307300a569aa4442a39c794b38da","impliedFormat":99},{"version":"b9aca9e3eae5ace3397362e7093655ed2f2000a3299003a0be3ba63fcd8a1cf7","impliedFormat":99},{"version":"1e06d6137283a011ec689e6ec5e76df3dd46abd938745d363a260adb54b0b6e3","impliedFormat":99},{"version":"9e0bc78a0cf6afc736a142062ad0186d496b8ef9dd72b5a3d2847508ac9281f2","impliedFormat":99},{"version":"12f6a1fc30cc21367b7bd024a1f234f1c87c2694e5123791c4c1336008a97ae0","impliedFormat":99},{"version":"62a82040362407115b58a948e8f2cfd6a8c277a2d549c18fc06d21de2c3931fa","impliedFormat":99},{"version":"1d5cd62166fb45b821f6292a748f772620525688c4844ed81ddca13253ffe99f","impliedFormat":99},{"version":"b6007d32fa2029a0a1aaa3c405900ede8b00d3ab717af0a1ce3e681bbdd7cbd2","impliedFormat":99},{"version":"d430e252cb220478232cf6783dac71b221875110f508ed3ffb4dd58a93cd3bcd","impliedFormat":99},{"version":"0ad363f8072fbe9e4f6a5b90a870ff30f962eee48c5fdb543445f709675250bf","impliedFormat":99},{"version":"5dd81b737d6f72e731aaac4678c763af0ea6897d7fd8196f04e5477757db3e16","impliedFormat":99},{"version":"c9de554ffd41dc88473b51f8a726f8e6752ba4f8afa12100fe24b9f18ce2794c","impliedFormat":99},{"version":"f7f004f03df753e513625d261669f87c49d92bc2ffff624b809d5c8c8068806b","impliedFormat":99},{"version":"651cbda75a78fdf3da54f3a7eb5de87652390a44a34140303fecc2de9983040f","impliedFormat":99},{"version":"200120eafe59da37d180ceea37b342957849377c7215d2efa9294edf537e745c","impliedFormat":99},{"version":"54b641ae9f41f8d05b6141e6a02acda77b0743e1c3b0bef93e689f890981b1eb","impliedFormat":99},{"version":"b231fa6be4cec8a6b90f98071335cb22a6196b64724af56b1e851851fce48bed","impliedFormat":99},{"version":"f9f8a5c8b56a99c29943ba345f03102f159330ee4984ac0c06a4b93f3f9d6c1a","impliedFormat":99},{"version":"2cfe431d866a5dc7a23dcb1cd896aeb995dd1416a668591c79d7c0548b2bc8e4","impliedFormat":99},{"version":"8def9a68149edf17ea5df2669bde45acd5cb3f5d28ad79042a428e830a24af1f","impliedFormat":99},{"version":"a00a6157f232a7d36c78bdd7c85d34c8c9e8dd7bd11cf2540748f5fb9030330e","impliedFormat":99},{"version":"2b08bc4a87ef16bf7f587f074bb9194c112b69462fda07d5410476c8513f9a7f","impliedFormat":99},{"version":"615607b6a29e3f697222ea2b0109a513d0c7a961969eaf9498917d36db55ebbc","impliedFormat":99},{"version":"343987d6dae8f8ec43e2eb17aabb2bc5a15be8e7c797d37c74792e4bd43ca839","impliedFormat":99},{"version":"a80b7bb7f5c675ece7f1e73bbf4f4f9632ea8e919c1170ed9bae7cc6fb707e77","impliedFormat":99},{"version":"c27b0fbf9267c8beebf4ce5b1696934e7bc08847f28b2c9249659441a94b9da3","impliedFormat":99},{"version":"24ae591b2f12be72a4a9fe993b92135d6bb3e5e1bdb13cc8097625651404c4be","impliedFormat":99},{"version":"0a1a4813f7ecee0d0c39fcd50583dac3b63d408a95072905ba16d469919985ae","impliedFormat":99},{"version":"7dfa35a959b024bf2fa0483f2488ea6ed893ba1277ccc075e018a1730270d544","impliedFormat":99},{"version":"c0e98150595af80066480e9583f89e6370693cd0827fc89b68ecabfa5cdf4a72","impliedFormat":99},{"version":"10134fdfdf255c70e3e1838ad9ef47047677383aa6978883c15eb0f5c17c3563","impliedFormat":99},{"version":"39056f5033d13f6d66c2980c3e0e20bf84ceb64d51567156451695773148ab1b","impliedFormat":99},{"version":"4d79b57384b19b721f29cc120ccd4c8aee6c2351d8626a6bc3c43e1e7dd555fd","impliedFormat":99},{"version":"ec110162f200873cca9707c8be48a53fb509377db99e916b839b1a36332a655e","impliedFormat":99},{"version":"77f67d567cba48115ebc24484cbd0ae510dcd1e41f9508ee1f5bedfc0a925520","impliedFormat":99},{"version":"45c5df088d0b2fd4aaebabae4ac3e69f05ead369aa0e146067dc7f553b0a8279","impliedFormat":99},{"version":"6428679bceeae2bc835cf15366c8219a9f4a3baf9133cd2004dac51a87ebde8c","impliedFormat":99},{"version":"163555f7f6c6ec766b30dd054fab8fa69f5184d82698c4f5e7457b4f22710fab","impliedFormat":99},{"version":"8a92621a4d723e1c2f868b18f4ffba76739a0c93e8437850f99f5009499b8848","impliedFormat":99},{"version":"2bbde324e159d9f59ab56a4c77128737958704e9c4331caf646b075c93d58ea8","impliedFormat":99},{"version":"b7c30808b1e0801ebc2929e8112cbc6471473f227d25fa3c338ddf8b4f2cc972","impliedFormat":99},{"version":"a5e0dbdd3c5ebcc3561c493df20fb1c35094444b67e5cdf894d5ca83455f0dc4","impliedFormat":99},{"version":"bd0ba36a4dc087b698b83e8f210c57b5b8cb29c4e7cf09f52b9c53cfc8527508","impliedFormat":99},{"version":"40b46bcc69563ffbeaad0e7a9be85f7ece978902c7e17481874d3e40b6f30baa","impliedFormat":99},{"version":"24fec85fbf17a72b387916cb73ac60273753eb2f75bdba89fd9d8373d2f8e823","impliedFormat":99},{"version":"0fda2c7354705f8e3c7e88495fc14af255e1e4fc6ab5fd2510b0ded969550128","impliedFormat":99},{"version":"8364b396dccb914a0fdd1a43ac72eab807e2285c023aabdceb4a90175ea1fcaf","impliedFormat":99},{"version":"267fce2a9d770603e7497ff4d2ebabd4ccf6b6c40b379574fab6e7bf16df3635","impliedFormat":99},{"version":"6a92542761d442813efe8c992d14ecdb9f0f88d29128ababad72d5372d377a41","impliedFormat":99},{"version":"a913e963331c6b05ee3b833b505855f555b1632bb7784fe97933f01569b64221","impliedFormat":99},{"version":"a96a6cc809d8f09952880373df55104b7869b601c45b58be80ea50ad46bbb0db","impliedFormat":99},{"version":"6f13100cf876318d0f1e8f3600beafb1622b52c8ecad388f82d5c8a7ec5c3982","impliedFormat":99},{"version":"12d6a24ab50e258055b79b5620e1ab58fe1d5e53f816d7d24bf63b2bd41399af","impliedFormat":99},{"version":"4d71c3f2950cceeea5a198a49f9548d4c1744b2e08e799026b2c01e26c30ad84","impliedFormat":99},{"version":"56e5330e3c63f36f57cf87db4674dc0da3e84c9745431e3ee4eea014372e6ae3","impliedFormat":99},{"version":"06454590fb020d26332487e466caa15be9d4ab476cd1f2b502d041df6e73ace4","impliedFormat":99},{"version":"0af1d04da71458d3b24e9d4fccebd327335f147e0b822871151c6fd74d8a945d","impliedFormat":99},{"version":"0a65f24484673f5ff0ffbf09d8970f509a4c7140cf4a92fb94f4ea8be759b150","impliedFormat":99},{"version":"c3ef829e6159fc25cbb3ffb2392cdc65fa0bee0de45d1905eef39ce72a30b223","impliedFormat":99},{"version":"1a2dd1a4760aed2479369e605adde150d3425e1081be79753e3a1e4238bbd4d3","impliedFormat":99},{"version":"8a580ca7dfe72e5393d3e68f966e9a6770a0a67ed31a06783a0fc2d5f1bf41aa","impliedFormat":99},{"version":"dc5c180e2c2ecfb91687fa245c0ef789abb2f258925c53a46c40b5be5cc06d7b","impliedFormat":99},{"version":"2e03d78647e74bdec38e689f32471393ec37f477b75305ca299fe7afb378a900","impliedFormat":99},{"version":"d7d10dd270b953176d3633315fdacf90cf2c3fdefc7980b948e7d242e1f83d20","impliedFormat":99},{"version":"5e2880e69e0b2f69713a7b5c8595d08353d8129e62f14fbeac5e3a7a2f350111","impliedFormat":99},{"version":"26541ee839fa2672a9aee147d1bd77c92304c07c9c794f4e70d98fca4ac4d5d8","impliedFormat":99},{"version":"e6e906f2106464f4ff8ff9d653ab434dd57d14f1a86ac364f0d764633cbd4fc1","impliedFormat":99},{"version":"2dfff976aaa4c0efbfb0694bc9a0115b3d745473b9ce80949fa02f249df466c1","impliedFormat":99},{"version":"4eb1912637486f1b4a360658f7b1899aafc2b07761ed99f60617d11f596ea58f","impliedFormat":99},{"version":"be1378176c12ced525f5dfb672356dcb44c3ed826b76f1ed7c2e14c17e698690","impliedFormat":99},{"version":"6bf5d30cc1060528895d2db6fc2039c65606d2871c575667913cc67f28a8e2c4","impliedFormat":99},{"version":"a6cdbac42ca8870acf70615c430f50664236917dbadda4829b2ca85e22f65dc2","impliedFormat":99},{"version":"1a35594e8294b60a08ca90d0712772b851d3bd3343fc3f40c517e8f91118ae81","impliedFormat":99},{"version":"bc992f34d8a1ea9b6bf76ce99de74c427f5c578c36d40f0f47627d7edefd1396","impliedFormat":99},{"version":"239c64046bfc03eaddf462ca3bbf31f3f629f47c42292681eb8174ab4a1bf080","impliedFormat":99},{"version":"e9922a0bd88d88458bd8e1b59629942989a4606cdc73104549b1d975e11c716d","impliedFormat":99},{"version":"fa4277a5b715291925ee253e3d5f4ace6858e30527cfce7c981dc3d910b282f6","impliedFormat":99},{"version":"32e62dcf57f473fe959c8e55c05b85fc6f2c41bcede76f192462688214ac52af","impliedFormat":99},{"version":"fb4d45a4a5a2d6bd3e55a9bac0c06fdfcfc437364122611b4eba92331b550764","impliedFormat":99},{"version":"fdd44bceea44c6b6bbfdcc60b21a749ece00c3c9ca39effd76728769abdbd3e7","impliedFormat":99},{"version":"5c3e3cb494f815df8289e573e275f5af22b90f65ea4f84ccc02db71cfda44c10","impliedFormat":99},{"version":"6156ec87a3211fab0e201ac49e23bfa7b305e4e144bd3096e6fe134d7b89b608","impliedFormat":99},{"version":"bd18b9e00a08f52ed006cecd5278e09636c9a8f554a53cab6980436f6ddb930e","impliedFormat":99},{"version":"76f6f91c693dddac2959f67e0dd1694cafe5a60d648308ba37a8b553157e08cd","impliedFormat":99},{"version":"dc9283c141dbfa236f0cc44a80455f03127d43f9f099a347d577499131bbf0d1","impliedFormat":99},{"version":"2408d8663c9b5598c32539ce5a2e63e61d3a9c783981441be2ba91c59a214bfd","impliedFormat":99},{"version":"c54b3671ac0947ccb76b538e9ad5c2c7bba2d6b4e2facfe90ed3883e5fc870c4","impliedFormat":99},{"version":"acb187dcbf37d298e9aa2188723266fd52bc691c21368f7ffad1f5cf5efc8f23","impliedFormat":99},{"version":"f844308a48c568900f8e3618cf117fc82ed6a6436b0ac3d2965af03d14c6b096","impliedFormat":99},{"version":"b0f5047634786f8ca1ed076ef456eecfb357a7bae8696f02608d996ad8afc52a","impliedFormat":99},{"version":"4dbd3281bbe8fa74971a802b8c1e1a0747d2e70762b9a4814f7ff442d3244230","impliedFormat":99},{"version":"c364b9476c21321d77f63075ede02113e60943fbbde14b86cc910d7891a5218a","impliedFormat":99},{"version":"653fdee1ae608312fce98873672eab205ac9c27b23de1b2791031a0f6d2b98a7","impliedFormat":99},{"version":"e4fd76ab9cce4eb4f61d7288e26bb8a5cafffc80954c3ff18fb27b89204d3442","impliedFormat":99},{"version":"ecd553fb6353dc1c393f596ef37cc74ef38fc0be23e07e780ff49a39bd448ed2","impliedFormat":99},{"version":"0e06ba8ad31c92c97ff790a2bf54d322d8b926d7a7a086ee0982e955a7399bf0","impliedFormat":99},{"version":"1a9e58fb9dba9bd5f06eeefec5c2f8d5292781b06972fb9c9fb1bbd8f15ad5f9","impliedFormat":99},{"version":"1dc595c305bbf9c102977fb7970e0374d1912357e17b3a95148580c6ab46b5cd","impliedFormat":99},{"version":"af4ded8d2788d0228df353955c9a0ad6c24ef0cef8d63b589629cebdf3e5c0dc","impliedFormat":99},{"version":"d64e5c26af45311b8642ff27d05c49d8b94b9828d353540ec1c0084b92e2963b","impliedFormat":99},{"version":"ea52f5796671ac35eb62145531a9d9b33e18624c7ea907b2ba054425865ef23a","impliedFormat":99},{"version":"63ddc0222be4dfbe4edab975080455c6753b18fde348d060a6d3ed93e0097cb0","impliedFormat":99},{"version":"79cd6112cda96ee5a62a7c4ff9f7b08651af15fe45205751655cd4b4c2d1b58e","impliedFormat":99},{"version":"2fafc95d1afc4b30891cfae6447b0547fb21a4eaef4e7b6f47b789eb07698cf4","impliedFormat":99},{"version":"cecd28c55bb2e5f66e02fa64c5eea751f9273c8515918820ca2409cbb90c802a","impliedFormat":99},{"version":"4e0d0aa02da405ab61975064bfff8a927b493239e7c2694b091ce3a87512b802","impliedFormat":99},{"version":"0bce1056c3ad5ae75512bffd21985a8107b6c7520bf073e4d1fac19ef1970c4b","impliedFormat":99},{"version":"dea629ac95fc8d6abde0a2486299c3f3c7e6ddfe7c14c3ce1302ec17c025a021","impliedFormat":99},{"version":"e6ccd8091551b224ce7989079751cbeff03207986bc77aee9e1d50c0d4eaa6e7","impliedFormat":99},{"version":"7d209a3d160e8412a320568c8343f4aa5e2a216e019f1e382f3d4c6b1069295e","impliedFormat":99},{"version":"aecc37bca58dc791685b01ce18238f8d2a03fdeffd4ec2f316e37f5b7307334c","impliedFormat":99},{"version":"7b8bfb125f9de5a4ba30f5d81b5f671772cf76987d880147f786e33fa878a5b3","impliedFormat":99},{"version":"d57d9fbf2673c403c6bfcc61ab70c16c4984a6880e647dc46c51333b1f812833","impliedFormat":99},{"version":"f3354264b00ad46c7f3949220e00ff70955bb67889e0a23e0d055153966b6294","impliedFormat":99},{"version":"7fa9b40a7f202ccd3c9a22a5777f02e50d0c009f633d23c8d8cc7877e95c89d0","impliedFormat":99},{"version":"1186f9e84db92d299247dbb5a283311055f82de1f8808dfb048566d1207fdb82","impliedFormat":99},{"version":"c30f1892b9de9bd083512b9fb5858cb5787481ecdad8920d7ff75586cc88d432","impliedFormat":99},{"version":"e43ce675a68782c430c60de48267f8268bdd682697cc38e54eb9d03f53b7791a","impliedFormat":99},{"version":"df1d3cca4a0197138ca725503c85ae59e06cc21e1dba64dcae94b8e8c2b1fffa","impliedFormat":99},{"version":"6d74fdf0a7ce758f316a2b2c8960ffc50235320727326a35f580612c0d593a4a","impliedFormat":99},{"version":"ff77c6d5963b87d427ccfe6509756ef01de1aedd685d186e177603ed741c93c4","impliedFormat":99},{"version":"62c3d6cc07f01164ab6b94b1daff72afbdea44a42efa750673e7c65739b85816","impliedFormat":99},{"version":"f9aa7dd2db630e9ad51214d9d0e14da672a82322684d908e361d1bae4bf64e01","impliedFormat":99},{"version":"eca2962342ba258932717a7431741e432d275beae879d6e655eea3cddd69ca91","impliedFormat":99},{"version":"807091b6bb063571171ba50d3d7da2daf4e718de25a32b4d168d9b2854b16979","impliedFormat":99},{"version":"2256e03055dc0cfcb237da2751a763b4c695f6c6c82003cf0d36851cfe77384f","impliedFormat":99},{"version":"af7cc8267b8302c517bd0a7367642bb1cfe2e516b00bd855ef85f2bfdba59c60","impliedFormat":99},{"version":"fc490848f97ceb21a52dd6afe32a9b98cb1cb2e6320806b5fb9c24757a1ca769","impliedFormat":99},{"version":"c06313963ee23643e32bb4a0deab3d69453115fc1aa59eb2e9699516fd9883bb","impliedFormat":99},{"version":"31ed9dbe091c55243ca0d95c7d3796448367091cb1e958033c3830cb58d873dd","impliedFormat":99},{"version":"ef436a3aff282493c3ca803abc5dcb5f678be8c2345cacb3a0356a47c6bad708","impliedFormat":99},{"version":"75c0e4d715175affceaf29516d4f46316bd757d2e30945ccce722aed9aa91ec4","impliedFormat":99},{"version":"cc7ff6ad94cae8f1b6b73cb0f3694e1bfaaaeb7db708efb3aa63d657d453cd05","impliedFormat":99},{"version":"4dea994dff6f8f13a75aac0d5fd62f867790c72af9c49f2153b48f1fbe94a545","impliedFormat":99},{"version":"c119857e4642cdd774d2a84d9547ea6daecc225b2273cc6e57035bd422f5ae1e","impliedFormat":99},{"version":"04127fb55c6a3db066777ec0efa8196f461e779ae5f25be1460c5b77bc6fffe9","impliedFormat":99},{"version":"0c11f88d9828efad354c4cee7152ee7eaec74fe59ae13ae29eaa68c1e65521a0","impliedFormat":99},{"version":"df3e033a605c7e31ac7a6e7517c422dbba4a940c8aaefbb8a64730bbcdbbd199","impliedFormat":99},{"version":"738ca31cb07d20afdee107364e270214da052479973e0b08a9a886f8f90d2803","impliedFormat":99},{"version":"bd034d73f41f90d3cfce994b51a759b887632bded1cb36f1dbe2773973a1ddf5","impliedFormat":99},{"version":"35a17e3fc0e53518707795ed24bd428633c45a4e8fcd4657eea2963b42366eb2","impliedFormat":99},{"version":"b3fd4b12bc1b4f73f1a110ccd99ffea459864afb8e682768d641de1be3508bc7","impliedFormat":99},{"version":"d1dcfea8733aec727e00c133901efd6974a9f7601c4c19ccf9c2e99616efc104","impliedFormat":99},{"version":"eb0b20ca60a2a2ee1daba9b78bfe8ae99487de57223aa6f0f6b99252b0cf9363","impliedFormat":99},{"version":"8c868f22cbfa1cd301a05eeef770dbc4453dc42f065fce267945674b4d82f3c0","impliedFormat":99},{"version":"e6ef6469aa806b0907c02b67207a86bdfc8a9291c5e63f3ee0a72437df56c055","impliedFormat":99},{"version":"85c677f795a09a5134768fc909906713afa46536fb7c178a58a2dbc8f981ee41","impliedFormat":99},{"version":"97ba6fc4a6c7975f53b16bc72ff0ca05d41104ef2a605f07f219ff23ba6643fc","impliedFormat":99},{"version":"59e5e42bd98ddb731aee619aa16b1d7403e598e54bf75d2daff25c57495a13d6","impliedFormat":99},{"version":"54e79ba20e85bc05dbf39eb24f94c0bf485ff76b096654420eb32dbd7a616c53","impliedFormat":99},{"version":"80a960337c67f64bddaea4ac2fd437ff09dcd64c50ebef0ad0e5abed981c862f","impliedFormat":99},{"version":"98590ff94f0c45c9da700bf9c972714e4367cee30842aa130ab501f463574bbe","impliedFormat":99},{"version":"7d4539a233361ed71d335793db13085aeafbc1e28920a9784b01a8b2d461d354","impliedFormat":99},{"version":"a00c3e3e9475eb832c523f41734a7ebef312b1ac1d6ecb68d22df37798e18c02","impliedFormat":99},{"version":"275d39d59a9a5c7ad2d13757bb35bd190f487cf5fe221b8a556be2117ce6c2a3","impliedFormat":99},{"version":"072c8a4438ecf5be5f093ca727bd03874fdcf7ea8b810a5f3d5b5e9a1a158646","impliedFormat":99},{"version":"6808d7d909f594bc5e0695b09e9f663f79b08573e3d95b837c1f15294481516f","impliedFormat":99},{"version":"1c699544448121e8a6521399e2b9e709c9561b768bdc3249b035be0ff5a458ed","impliedFormat":99},{"version":"c0f9d148738c7f952a6e0075e7f7528e67fe813e5473741b6ae2d7bd1c701727","impliedFormat":99},{"version":"9594f39327c3776aa4083e472770ceffc14d45e669998536b60b5de5b1006143","impliedFormat":99},{"version":"4830175a6be86164822a5b2f7fef7a97fee781b33a52453ac8d8b50e3ec91830","impliedFormat":99},{"version":"d82b5aaf44272f7e153a3a469db46739c306b5c173e0382cebea70bf469d3a52","impliedFormat":99},{"version":"65d1b54d95e8712f8bfec843cfdffcbaaf6d92d0a69abb7345ba55f3f5712017","impliedFormat":99},{"version":"80464d99a301e3db960da2a7b010ada29874dd6dbc5f24731473a2641cee217c","impliedFormat":99},{"version":"3834d1fff358eaf5b52078c27881964e31e54fd1b5be8cb097a2ca75c2d97b7c","impliedFormat":99},{"version":"3fb4902f08c9b5a77956e9a6856505e00a7fe476a586e832571c3d4f83ddba31","impliedFormat":99},{"version":"93c8e1d9bdc1cfd856abb2ddb43c31da1adb5bb23a3172422ac3426d80687f97","impliedFormat":99},{"version":"82dc653116ac560acfa8623cd3fb7de40ac5f2e3d65991ce36070364b4f1bb07","impliedFormat":99},{"version":"ed6c38e10a8ef5ff290aa185177153ad1e0a85c7d867047532d33a6b1bbf2c4c","impliedFormat":99},{"version":"bab1312d15063c1685b8aa51252669c1c5843f3f7c3568674dce1a342cd1f7a3","impliedFormat":99},{"version":"023a397566c9c9ba724750a017783ce78006b04d52f96322f42ec104daa1f776","impliedFormat":99},{"version":"10fc62a7f653cff767df362868a121e6d5fe9d872fa4c0fe3de8b1acd4324f0a","impliedFormat":99},{"version":"05346e4aa851fc01eef2d4ccd77b5c8e5ef306ad044ecbc8594aeb7d753e1d33","impliedFormat":99},{"version":"05e97bc0985fe092ec1fa0204a4ba209902419231941be9d7109e759992af3a4","impliedFormat":99},{"version":"d7baa0aaffbdb80eb363c76f5c062841441281757037ed9a740d2e8ab9cdc391","impliedFormat":99},{"version":"cfc54c8c0b1974dab7994739dc901c13fc8cef6ed11cc0ca2777dc0eb1751f40","impliedFormat":99},{"version":"9815136c848202c8b0705c8e56a6546d2052cd0abee889fc0a14a08c8a13dc0b","impliedFormat":99},{"version":"4b77dd1d6d600b721c6b475b9479455233e7a189c1e8db14a53a8e34726d2f55","impliedFormat":99},{"version":"132c03d02eced90fa294ae08f814348ab056e1bb6e041a4802aab72deb61db78","impliedFormat":99},{"version":"3bbe7954dae9f4355f7da44a99dddcabe92ec11239e38a0c3c89da998156d9f4","impliedFormat":99},{"version":"3531151069b2a08dee7b82bf7c3437f707048c682395003a49fd630968f5ce15","impliedFormat":99},{"version":"51a2a1e0465147c66d3b3ba8b6776caa67e473db9aa5ba771a7fd5f661d994f9","impliedFormat":99},{"version":"616fc8cf7d4a11d695eb0e309522f1facee105a1c740e74fee5f710dc745bb40","impliedFormat":99},{"version":"c14a02a41bbdce485e6b9fb0a70c91a354a2044b55e2563e6731d51a3b3ead5e","impliedFormat":99},{"version":"bcdc75c3befba305fa771f51762f600ef3c7f60e12ff9b0ed6eddb3b11da3792","impliedFormat":99},{"version":"9b681badef747116e902328e3ca5bcfcdebb16259a8b8419a7e95132977e9e60","impliedFormat":99},{"version":"fbb328355422560a1393277f37065465dc36c25c5afb716c6dbbb4107f2c3adf","impliedFormat":99},{"version":"f9c5a5df867ff31c79e8dc2e7bcaafdca06d8a8adc28a099d8d05a1a4efae2b3","impliedFormat":99},{"version":"2e3ae086adbdd8ee059a46739badcbc7f46ce88b16dbc2afe2b669b9130a1fb2","impliedFormat":99},{"version":"fddbed7f42a61dd91910d468774ab7aece510e4bfc8acc8d6dee2f0b2cc3024a","impliedFormat":99},{"version":"d0c25a9039e1b56414c154cc2c3e22e84c8be8855c9780f4c1ff793f9e551589","impliedFormat":99},{"version":"1f700fe107041685043548d72a01cd21bd31d658a5a4d6d230367840ce65b389","impliedFormat":99},{"version":"c08f35285d1267946e19663f7c121f6d3e16cc03004a76506abc9f9f8bdc8b6c","impliedFormat":99},{"version":"021902fc642e31e811caed40c5ad07e0b105d7e269f609226b6447fcfbf62733","impliedFormat":99},{"version":"0a45d152162e8a7803ad6b0644e9f2098e28281f6e5ac08f21f14587564f568b","impliedFormat":99},{"version":"5b85b4c8d575c506088c9e77dce0607988506e12ad1a011f7068c9a35e5abdf4","impliedFormat":99},{"version":"d64b38d73752d637905e2ee9f39f7f82e2102dc96171adba9ec6454e396e0319","impliedFormat":99},{"version":"119acee61f07781e3ac933bf590854365e8117c31c896bf4bf1b9e3f399ef492","impliedFormat":99},{"version":"95be4119f87bdda94d4302cc6e6c1e2d1b849d170c5f187815224eadf7b1bcfd","impliedFormat":99},{"version":"ddcec9094b6fe3c92c6758cd338292672416b7d58ee807483dc8ddf629d699d4","impliedFormat":99},{"version":"283daf40cfa9417647d1b581cb734366581a803620bd20c60d92bdb7bb807beb","impliedFormat":99},{"version":"94d19f12b5d52237ee39b35048a1b7aca7c0b8fbfda6457c2dfd83414a6ecb89","impliedFormat":99},{"version":"7b14ae6939622c3fea912933aac786f88fb39c20f115eb7496ef890eadaab2f0","impliedFormat":99},{"version":"1023d9d72029b02a767d59537342fa1be3f3a33d901b52d7e0d47d42269f0215","impliedFormat":99},{"version":"a594453ed8c004df6b340bda0226e7cfe94ee67698d1c0dc1b9207d1e20e0ff6","impliedFormat":99},{"version":"4c3c179bd19a0c96c14749c9095714c9b150fc6ff2506c81b877d25644045630","impliedFormat":99},{"version":"21d8b3b7300ed19f41050d3fd6317f5b7c65dec299e1698502fb6767ac30d839","impliedFormat":99},{"version":"a6a5c59f2cf45d5ab475340ae96ebeca495094d15d72102b7bff05b7f0bf7383","impliedFormat":99},{"version":"eb7628d3567bcd483094fc2688ceb5305ece0563463787cca7035efce9f43c2f","impliedFormat":99},{"version":"6d79ded4df43562c8062badc829bace67925c70d18a7b72c09b7d23797cd0891","impliedFormat":99},{"version":"d25e9a88e761b13dd58628d2d41641195ad79ae927bb268387082656673ebe0e","impliedFormat":99},{"version":"b161e27190a5249c6e5660288e014bcdcb1e5bcc2733f848351b027e0d772ff4","impliedFormat":99},{"version":"ef547825b57ce9440a18bbd170125b89cc5bb8ce7504dfa6cd528a3dc4489e76","impliedFormat":99},{"version":"e74cf49b55f4550883d9d46173dc10ba5fd38d55f69f8617ca147b6d39c8a811","impliedFormat":99},{"version":"339e68b5bb386ddf9129bb3370d01f22f8919cd0e40ca71e06f881f69e02af66","impliedFormat":99},{"version":"8c65681a3636770c9b9b2b52d14ee4d59b48092175ae3124c044a2637fc84153","impliedFormat":99},{"version":"c6bd0b212d354956e2c5edc8e0249adb3c0e9c942e7e1794e23fe9f0dc721828","impliedFormat":99},{"version":"5e3b5745889dc9efc2e6f5515ae5bcb8135f2a8412d4a1facf374e09176f7cb0","impliedFormat":99},{"version":"f0576bc1e933fdc38f0f182775c24efab54f2a7a9b4a1953abe77a620346b9de","impliedFormat":99},{"version":"f061956ca6ce7bd52bf726d2e87e620aa928807f3ae526d3d432742d1da6e2e5","impliedFormat":99},{"version":"39b7dda3a4bd1ebfe126c67d3df5ec230681097b54a4f5a7abbd841cb321c003","impliedFormat":99},{"version":"532cdefa9aa613089dbd91804d4d9de824ed6e9f44980db04a14465e19a9e4e7","impliedFormat":99},{"version":"d0682d488ea0a7deeefc809d65c42e09f339dde50dec54f901104ba35c70f5d5","impliedFormat":99},{"version":"d4bf07baf86b8e6fbefb578d8a630db479bd8feae8a9932175c4d412c21f27f7","impliedFormat":99},{"version":"f76ccf2e0dc0bdbd29cde0b9054b0d510719730e6455db17e98f2b10646eaa59","impliedFormat":99},{"version":"f8dc3be04418be55ab97a46357ad0f6102a118b63e756f0fad0de94be2094d3f","impliedFormat":99},{"version":"3c0ebcc673f72499ae0ed99b0fddd52a60fcda51d50b11d1adc046e5985916a2","impliedFormat":99},{"version":"5395a87aca9a065a768051b7a6c66d3cef55e097ac2ad6ab917e27c3c92c4e49","impliedFormat":99},{"version":"79bcdbfebd96644cd9e93ca0b53ad0dfd398e0382c03e58f3135474ece45ca13","impliedFormat":99},{"version":"a134f53667b920e6ec7095c1c3cf0bcaa95008cd43000d3bdab1e570ac70cafc","impliedFormat":99},{"version":"116808caf097e8bf31ce1cccf1bf4060608cb7d91bc426b92abd97f7287de047","impliedFormat":99},{"version":"bf7b4bbe862d400269c378a2f744a5a94df5467cd18c805ee3a5ad741766ca40","impliedFormat":99},{"version":"2bb9ec2ead61f07ae6d8277af9318e37205b49abc9271604597844346ed3959c","impliedFormat":99},{"version":"d75af40a0b511d0633157556a35141a834d084094b01a8908d78dec2dd80fc64","impliedFormat":99},{"version":"ec2ca88bd0a13a87374a11d5bfacadc96b55810aae7a008cc44279b7757839b1","impliedFormat":99},{"version":"83551e90c20424cc93773db3ed94f8c43b39635830bf5548455d0836deb96393","impliedFormat":99},{"version":"945b49df2cc8667365e20dd3c1f8de64447e0b32f1d91c23ad06b07b0aae44ea","impliedFormat":99},{"version":"d6dcc687d0baf6635c1808c740aba4db6e37a3b5526f2655f0c291accec0decc","impliedFormat":99},{"version":"1dde80f3943e6bb56df526b77bc7a41dbc234d4393f1b8f19cc6672171ead575","impliedFormat":99},{"version":"7811c9c49dcddf09a7b307acaeb6f3c9b4d1d1b97961d87cc94670c46ea0362e","impliedFormat":99},{"version":"25c965403446cdcc5417d13db187a7f0394f7106cd1a45e1b9cd3a2c08aec4b2","impliedFormat":99},{"version":"1dc95a1964076bc1aeb6731a17ae5f7163846f248f60db0e0961ced43db22c6c","impliedFormat":99},{"version":"63482e1d5b8a0f3a669807bfa5355d3a9790856b120f512b4bc40a0dc65c78b6","impliedFormat":99},{"version":"7755b72611ef60e8cb35ce5e7ad393577e7389c485d4683570751e88177b1bc6","impliedFormat":99},{"version":"5445c1b45a4b24cb793cea4f1f8678912f2a7eda127afa72c48614aceb3059db","impliedFormat":99},{"version":"b600d5e621219860f2f6f9e1aa25d777c6b6c469a9048839b0bc256f16e65cb8","impliedFormat":99},{"version":"ed89af0e95cfc3c3aa05ab7f10f39252e304864be96702d3002e3a0b5314b058","impliedFormat":99},{"version":"6329a32dffec51eeb23377ccaead107d1ae30efe84e019e70d92a529df7e48bd","impliedFormat":99},{"version":"05650f29c7b38c2cf78950caa7386acbc7936a7cb08711bd1975cd4818c5cb09","impliedFormat":99},{"version":"12ccebd398ecb9ff4452279fc72aa34c70ff770926652dbdc7d34ad21777f466","impliedFormat":99},{"version":"1c4b858e912db3388433ea26a3b99b2c0ef72048da6946614463596b00298c5b","impliedFormat":99},{"version":"dd531d326e40577dadb38f2a931264139865f767bdd762af1e81f316b0c03a45","impliedFormat":99},{"version":"e8d0fd2600751d9b4abcb81e3067f1a6823763a25322401cc8a0578747692bf5","impliedFormat":99},{"version":"63964d043dc01122a1db457afec463743918a5c71c4beed6e2906f509e015a4f","impliedFormat":99},{"version":"4247f6bd3d537c2f7ee7a31067c7e95b70327bfd55a379fd96a42e9f2aa6d46f","impliedFormat":99},{"version":"e2ebacb3dd831a48a1beb9915bff463005845a398d999214a0bfba620602b741","impliedFormat":99},{"version":"5bf7ca566fde86fd0469836409db26264d98380002545a6b8ad048de46f47489","impliedFormat":99},{"version":"e860b60d342ee8666f83bf61339fa74fe7613f2ac7ed599f0f802ef3d85778a0","impliedFormat":99},{"version":"130de79e5e21aa52d0ac67eaa51182d90dc0354eac7c35a542a56426e93062cc","impliedFormat":99},{"version":"d590eeb146c65796d33043d08e48449c205b37033ac4157af8984bf1019928d6","impliedFormat":99},{"version":"68b3f5d095283a725fed296ee8cd36c1373a2476aec8cd9d8c953de792161248","impliedFormat":99},{"version":"55804c2d006cf28fa308eb3b608e5673ee4ef0e3d9999e532efcb9d703ceb522","impliedFormat":99},{"version":"e1dae322a476b97875adaa6cff6aeca7b3273e2a0fad1e175e329c4ce3d02542","impliedFormat":99},{"version":"36334b90aee2fe5d6f2971f05af58a15c68059aaa806e9a94652b324ea630e18","impliedFormat":99},{"version":"9e5ea08cabcd47ef88501f69a76fd8bcaae5149e6f92251e612e544af3f58a00","impliedFormat":99},{"version":"390b90167c23bdcaaa56041c620b04d9e85957f79d103a0b8a4f40f1159a486c","impliedFormat":99},{"version":"b97b7f29234eac44f2395e36457abffc93f7b709d599821df52bef118009a959","impliedFormat":99},{"version":"d8a29ab9ec8a59aa4f7a46217dd76da3fe9b98cea738f0a3031595e7e3633106","impliedFormat":99},{"version":"1efa9583067ef597f832bca0ebdb5a1e2c940d4b67fcbbbf4679528843e38834","impliedFormat":99},{"version":"ac9e0128567c5676b83e5364abc4230a6610fb9bbc698392b5ed197b99d95278","impliedFormat":99},{"version":"d8250e45341b1e98b42e3008bae572623c2690e331950ba0a7b6c0ce6cd59d50","impliedFormat":99},{"version":"7ff384e556272b468d2710a54ddc17eea161d98dd44485d0c0d4161ac7e7da85","impliedFormat":99},{"version":"1388631fa8cdd249a9812333f0c2ce35490055ad8013972eb920e123bc5eb8e2","impliedFormat":99},{"version":"ee6b73c727bb809d7f5e6e22b4a668f466a5ab65ee1b8f214f8ccc3e311a2451","impliedFormat":99},{"version":"35fb1d611af3bdd39f5d5402847167af43f150716a477de3796c7c4ed071f004","impliedFormat":99},{"version":"708088bfcef2939f193d1ec862b1e576a9f345638600a9e82e398ea6f2e006d6","impliedFormat":99},{"version":"e66d27a7e54d9cd5e3bf7acb28838419aed6cf78c8ef567b9ec1427fac774f56","impliedFormat":99},{"version":"d19cd315889739ab3e1956053ded5eb062348ba8cb9891a661d0186b91770f21","impliedFormat":99},{"version":"a0eab628c5edf5340163c01613f55ac9fdfd5186c52d96372528c576152f3795","impliedFormat":99},{"version":"619c675e7f67b1343992c6f35c4ec9a52fda78bb025ff72519089a6da62d1a47","impliedFormat":99},{"version":"e190626bea4b1e20d7b3e830606a43aadc25815806fa1527d527e961a35e8bcb","impliedFormat":99},{"version":"94df71ff0eea0e304a2ef7722ab413d75adc8a98e0c741a0107643276560c7dc","impliedFormat":99},{"version":"1a6e320abcdea25b8489b6e39cb4a4d8c90d0f0ba9f597231e95420c723b9dd3","impliedFormat":99},{"version":"2f838725d0b114c8aba336f110037c0b00b6248958227a74d80fb62946ee5b18","impliedFormat":99},{"version":"c9dcdbf46d97fb2df29586c68cd5ae6748c081a0c53fc37a6132ffa27f6955fb","impliedFormat":99},{"version":"8f430a46820c2649bc2d07c6febccbec75be9bfaddf35bf264590f8191e94373","impliedFormat":99},{"version":"9b110210c3df13bd4e53db3576f4416c4f56142d50b986f46e39450c88c6b780","impliedFormat":99},{"version":"c2d231b90096f0d5dfce494e54bf81666758316e087781f14bb6da1aff9e8700","impliedFormat":99},{"version":"1e1dade75405e843690120e917bf189f5a1a8dcbe60caa2815d949dfa4b5d5be","impliedFormat":99},{"version":"e7c7954921e22f5f8945230f89cb777668b059a7354b350e0e357987aa0417a0","impliedFormat":99},{"version":"3267519b49ae97bae8a7bcc12936ebb116209c6aecdc9f15234b0160ae66300f","impliedFormat":99},{"version":"8d5680d71ff8394d96be70eb3e03ed4332e1b7e5191502aff1576900c120ca16","impliedFormat":99},{"version":"60aa61caeda93417fe0d1c238def5ac44869ad867bc779a82be7c9a1c06d4734","impliedFormat":99},{"version":"2709edab013c18a69f6bd497d4abddf8368a14e0baf1bb30ad5d0df59bf42693","impliedFormat":99},{"version":"c5c079a529300647828ea61619a49261b416ae65f46f968511519c5f8a79489c","impliedFormat":99},{"version":"a746fb3152e96412346ce01d24d5ed2817daed4ea188016d4aad0eb486d193f8","impliedFormat":99},{"version":"9b5dec24186dbc05171cb3f9e4f1e191154d3d4b7f187dde95ed0b08bd38c32d","impliedFormat":99},{"version":"c21c62c5f7e9448de7e9b0fa9f240d5febe46ef3f170ba4f976b24b4d2270cc9","impliedFormat":99},{"version":"b0f482d75e79e076a84d8f83abe57cf80d75bfe4f64faf96e0a6c965ca6e2a6f","impliedFormat":99},{"version":"2d72fb98ce4a52674d73af8bb2afe471cc480afe5de81e4e1fa40fb576169bb7","impliedFormat":99},{"version":"5ad98fb3cfd67c6c38af8bd2761a605f9d911d6e03955fddb0c7fb7cf824c694","impliedFormat":99},{"version":"67f46739541c08dc52bdfdfc8b77534dd9443e7fb4bc31467a211acc699a02de","impliedFormat":99},{"version":"7d8515bfa58add7868c423b29c77598203e0c7696b7345d933f4dc8b04c5aec7","impliedFormat":99},{"version":"6bf2b0fdef53d2460e7f49b6e7272a25b74ea474c052275a9215f645b60b0f64","impliedFormat":99},{"version":"94b181dbd37f644090e1ef29852d9dd6368d8874cf2f4c0ae0a298feaf991eaf","impliedFormat":99},{"version":"2d74c63d20d7eca1904f26dddb4d8b8d8fca12529bebe4bda7d2d14d24bde067","impliedFormat":99},{"version":"eefefc2f2dfa29e2572a60421e45dcf8530a34b17c3a6d43c5799a5ef0550686","impliedFormat":99},{"version":"8e6551df9921ce8c6a3b4fb520d007e28353a0d3d8a1d06f6778c6f1486207f3","impliedFormat":99},{"version":"68954597f1d7f67b99005951e94ca60a3a62bf3913362f2cc7cc653de3f19496","impliedFormat":99},{"version":"4773ec8c467186c29116dc3c3cbb7927437de3311a09b052687c3dfe067c16d7","impliedFormat":99},{"version":"0c710c1c886c67727bb7deb8197d009a9146b1f5afaff62b7342acc76b73f279","impliedFormat":99},{"version":"4d62901366dbbd759da77f66c16897747f70a2d8dfdacecbf8606e668cc6c301","impliedFormat":99},{"version":"60b81bc5977f284d06c8c89cc04b8cddb45ade80758d1a0455c30161b34b437b","impliedFormat":99},{"version":"56e436146023b114fb661baf70bfce2041d5806721935746699df3d1331fc6af","impliedFormat":99},{"version":"7cd4bc26d96221eb7fe5fa0c1b503386377fbcc6757d26ec50014e176a61b7d8","impliedFormat":99},{"version":"bde451f81ffba547053111934cd0ae75bdde203ddefae30976b77033136de1f1","impliedFormat":99},{"version":"775d6a8112d08a9c5fa5d2cb8aa97881ca81cd2344f6e9e158dc81efab171b07","impliedFormat":99},{"version":"4cd295e5f329970c707fb48b9d88d15b32faa9f760fcc977ee10eb160f4e4a6f","impliedFormat":99},{"version":"fc4038d8a91384abffe294d3df8c3a06e3df7073bcc2251272fb31d912e6fe2c","impliedFormat":99},{"version":"22bf99ca0b3d239d88d6d8e6e2bf155afb30b4aca7fc09335087938467543a09","impliedFormat":99},{"version":"cd3682274f378c2462bc4fbb41ccb5300d71cc5c008c12fa9d35ccc54b34cbca","impliedFormat":99},{"version":"67dc58c0b812bd006ecabbd29e8f9d762c41b0549910fa6991411c16e95bbb59","impliedFormat":99},{"version":"2bc40ddafd68113f9625ab18be76b9d69d8622534674ba26e32123e74fc8667f","impliedFormat":99},{"version":"9c099d02e2e82d484012f3713a164cf55c7f63e5cd9c2fda4dc4076648ab862a","impliedFormat":99},{"version":"48b3074e05dbe986b0228044afa1ab2b130f562920e20ad6199c3219b2629d17","impliedFormat":99},{"version":"c9829c5ebf26778156e33b866eacf467eef9beda248f32cfa5042cd2ede8de50","impliedFormat":99},{"version":"658ee70a53b7a4f30af822940b5863208e367769715698fff58d0f38939ce858","impliedFormat":99},{"version":"ca1bca6d1b00d79b032c2dbb48ff021fa7eaed6b0aacc796ef5d0873933891ce","impliedFormat":99},{"version":"bdde7f266379e321a38646d74c7da791308d1de89073299a9d98e356634f7c08","impliedFormat":99},{"version":"dc3b92eb315796d2568e17de9dbbcf25796565d440e63805642fda5cb68b9baf","impliedFormat":99},{"version":"d486d1fb1ab86280b3f8d730a0770d5843cbaee267a13748c210a49d5d8701e7","impliedFormat":99},{"version":"6a1edff3f864b17879bd05bdf8dbff682d14d2b701892256b74da0693616e06e","impliedFormat":99},{"version":"1c3a5421df2a1a0f71e86ff5aabc804934ae48ab0495acb257324b713d2e5c3a","impliedFormat":99},{"version":"6d604fdd569f7e30ff284c132db9b5185c8f9c80fe405d23e2b07c0e663f6278","impliedFormat":99},{"version":"91e3013fbc9ab4ae9415de1dfabd24b20eb1a448a8aa1227f8a1206975626585","impliedFormat":99},{"version":"e811d3bfe878fb14b09ee7746624751e7f861369ab6301d6e3b340f6c5209873","impliedFormat":99},{"version":"c181f4ed01c4d578e7cbfc15121db9d1e53d3292e0e674e89b8def9afc33f2de","impliedFormat":99},{"version":"3aaa8b4924cfba7d4dcec41271cd848ea987093376caf1da8cba263b1a8f604c","impliedFormat":99},{"version":"5c28b10c7829e6f8961a9a03a600e3037ad4d9e005ba3a177ce88c76cad419c1","impliedFormat":99},{"version":"c93bbc1a968312df22be64d3fa283d9a8794e1619f5413b134d7543884a26481","impliedFormat":99},{"version":"3367c741cb20d44594a33c81189e92fb6312ef1ad80e731e8c3782ea7cdc3b50","impliedFormat":99},{"version":"57da5fcf4facb95e01fdebff97414e51417d12d61948324cb1a504959055efe7","impliedFormat":99},{"version":"b9171a4741909269fe324049b8a2c560aa829a95c1d1770be18c9d137f820304","impliedFormat":99},{"version":"f05eaa4c4151749a5994f68172bae0b621e4000e6763eda011b802f5c675871a","impliedFormat":99},{"version":"d59306afb4025cddcefbb888fe9b5e530f36c13dae04f045d783591db4a09af1","impliedFormat":99},{"version":"a5cefa0fba77455d5ae3fe0b49edaeebee2ab7a02eb1fc3f2c1d1ef0f89e80a0","impliedFormat":99},{"version":"06dce6f582073c6fd71ea95e87fc30681c363f0136aa26aaf01b5072f8a747ee","impliedFormat":99},{"version":"7f39fb5640073a2c8292f58c332b8ea1dc40e6771e82485353ff5c01de3004e2","impliedFormat":99},{"version":"eec3d5956e9cdbf258c3052d1da1be6a5414ce9b925adadfbc69772a0f560ea5","impliedFormat":99},{"version":"ca904be0ecc01a5c6db3cd367b323989c367ec5a1b8fc8b290d566030350af63","impliedFormat":99},{"version":"71be24f0406ad4969187a22b62d5a5bca07a9e69fd4695332b823ba50b7221f7","impliedFormat":99},{"version":"dba595f82dfed85bbf4e39b50e27b9c9f52c20a48c72036dfc2ad8658f835bc2","impliedFormat":99},{"version":"81d42dccaf051e8d599b1d70281c410a8a501ac6ba6462dd95b66e06bc2086fd","impliedFormat":99},{"version":"2ce213f1fa2a9d38a4320272de78bb70e7ed1285090783732457d42064a66ed5","impliedFormat":99},{"version":"bba6e7d4458c3bc24b69f68a5152963eb6e9dd72d38618f83b3698153f635d45","impliedFormat":99},{"version":"0e7650ed5439e7d872df4e6bef9d75606da9c73a0682782f52c700936088eaf4","impliedFormat":99},{"version":"f101801894b206a26d61f2b84d1bd5587441e47557bd946f7d651fe0c6ee1f94","impliedFormat":99},{"version":"f2c1d36ed7cb9f429874c7865ae1d193bb27bf0669829e50bccf4bcb2c6ef300","impliedFormat":99},{"version":"d90c065d25c7043fd9b8d3d44cbc3f082b048a641d491ce27812b97eb5545a29","impliedFormat":99},{"version":"6a81e1ba677caa156e6624eb2ef502cf5b3e87cc259656ce649ec39e9ee97606","impliedFormat":99},{"version":"dce3724933c25a776d212550af0a1da5655035339daddc4ff9d4e3eb22ef13a1","impliedFormat":99},{"version":"7fab59a9579956454a09e5ac8001fd78365e650db43364f6aec5be52ae8711e5","impliedFormat":99},{"version":"b2fccbb91f9a6c85e91da363c4304c76f8a7b26db84f7f1bf8dbe5ffb2d49e50","impliedFormat":99},{"version":"bf890587b717c5562425aa385b021ba18900eaa0f176e3e57f0d1016b647d32f","impliedFormat":99},{"version":"3428eb55e4db8b21337d9bbee986cb297e3bd788e2405f02adcb37ad371811b8","impliedFormat":99},{"version":"abd06f3aee8945b159cf67d05a92ca23d2b35a1163773eef91f7cc2618915ee0","impliedFormat":99},{"version":"c6ec8754b8ce679004c28bf2cb0f2eaeaa8de0acff94ba5082fe16ce642ba92c","impliedFormat":99},{"version":"a3005d619de5f9e5d0208596083fc7110aec861b8dfa18f681395c216cc95fd5","impliedFormat":99},{"version":"5c3ba312c2cef70f54aff90e59fba9b61101e1e941c2b3970c1353c453f25155","impliedFormat":99},{"version":"cc8d78d412e3ae3997d0075078dd2787d842b6023bed13a6d9f3e07bb1e7d073","impliedFormat":99},{"version":"eefb675743146703d1c5a1b3e8171b4b95ae65c41120425312846f90c0132017","impliedFormat":99},{"version":"5928755ef02a97a3831fe4c85f9b32aa157d499acc21b3724c911b7733804c89","impliedFormat":99},{"version":"8627e628c30fc555b119326c656a5101e92548b05ceccbf38d6b5916062fb976","impliedFormat":99},{"version":"184c2412e3d88d7acfe70bddecff9004f4610afc87eebe042272f2fb7e0d3c89","impliedFormat":99},{"version":"6f9b9e4b4f44fef1d0b98eb1bb602345f0e22e0a137ef40a615503d4e8b35bd2","impliedFormat":99},{"version":"cacf616e3fb3ea7c2880cf4d1feef47a3dbed29b0bf248b0bdd47fcd8c8b5945","impliedFormat":99},{"version":"48eb53c96dcd4745c4c8bee4d1104ecaa6a877eefb8a5b57af0f7ddded949a4d","impliedFormat":99},{"version":"e0e35716709f99220dee1be057e8f7f08e1d8134c94ad414129b79cdd9b41f8e","impliedFormat":99},{"version":"ea544bfc5e4ff015c70ef62acaf356dfcbf1f39f10f07ca3f414278094e56b56","impliedFormat":99},{"version":"7d439735b74cc5a3de2a8855fa941341dff0628d1fb2b5f2dadc10a5bb6d9fad","impliedFormat":99},{"version":"e12142b93d24c7c67fca041df35d357f21d3751eae8b2afc5684d49630d12d37","impliedFormat":99},{"version":"b1bb63b43bc374799ca0e916542b0e71a7c8bb06d6c8e5f94848a8969536c933","impliedFormat":99},{"version":"d714669ac93d063f5ac5a0dc8136e796545c6b1358554ab423d07fdcca0bc7b2","impliedFormat":99},{"version":"208dfef5e3aa4e176a5112b91b5d1ca082c28d2eb33bceb82c0261ebdeee74bc","impliedFormat":99},{"version":"8361e095def6c5f9fe7303b341c4b3249e82354f78f8ac9602a076fc5a04e3db","impliedFormat":99},{"version":"e047505265ecef24ac538e166898c74fe4ddde9a208446b298ea9805a1337ebc","impliedFormat":99},{"version":"13a9d63fe4bcf62c0115da6fce6ff28caed592d7cf3718bdf2852616a9318304","impliedFormat":99},{"version":"5dbc1632a49ae9232d2006a9283086c23676a72c2869348fab07a92ad25894fa","impliedFormat":99},{"version":"3b24a42d6ff7eb9fbdaeb74d38c2bae20b455d9ecea76759c3bed2c9a70d3b9f","impliedFormat":99},{"version":"ab15374738d77b8203d5054b306e883bc12ae980f3de59b9bb2ab3c299eee15f","impliedFormat":99},{"version":"7fd8a28b8373c1ae0a9690ea4ad8ee96da351b2ddf9fff8f0fc33ba81bd49615","impliedFormat":99},{"version":"0dbf9eb173a832b643337fab8eb3153f2f82d40648cd9d0b654f37e469dde44b","impliedFormat":99},{"version":"a69420221cdbd10d0e9298b3b94d53a633606634ad0357da5cc541ff814d8c5f","impliedFormat":99},{"version":"e706e16bc6c83d4a60da03d7a423048f244423ed55064134d0db9a0a5d1a31ef","impliedFormat":99},{"version":"b11860ace9980039b9a876c8aa763228cd2eb7dc1ff1d4e4b6791fefa8270271","impliedFormat":99},{"version":"a5f1e4c5671abc235aac21bd99603fd151232360064d2622f6d21b592fde4cc0","impliedFormat":99},{"version":"a026df81b303bd8a8256705e4ba7f7845096a35a20885403143ecc25482fb5c6","impliedFormat":99},{"version":"3ad573ea6506a5b47d89fb43f6e7b3e24c9af3dad66408349bfdbea4d0ec9762","impliedFormat":99},{"version":"f08d5516ded9072f6ef4a9bd95d4301b7f168afb484dfcdc3f9fa0032fff1406","impliedFormat":99},{"version":"3532ffeb99fb16886cad07caf0feb76a19cc1fa14a648ce6e6558b04ba91bdcb","impliedFormat":99},{"version":"825a2b8b6cac11eb3228d4cf75487a3c4ed7daad1dcc1fb01c2f67dc75fa6e2c","impliedFormat":99},{"version":"f6976b6c4bfa0dd945409221c74e8adb9ac21110e43e6de6e5564d71a4faa010","impliedFormat":99},{"version":"3ba19528d73345e791808428a0ac7f251f3a03075f73b89393d5b7df828001bf","impliedFormat":99},{"version":"ed4fe0fdd2dd66de8ce064556270713b73bb71126c016db3a755a1b9ddb2b079","impliedFormat":99},{"version":"94535dcbd25f61962f2b2c9a36098001f0529186fcd9aaefd804bc7c3008e0e6","impliedFormat":99},{"version":"a3394d25f46ed8a6ede20cf2ebb07cad72e0928987c437dbe0a80799177268a2","impliedFormat":99},{"version":"979b1767dec45cc88bf49235cc88b3bf84f9072e0cadcaccabc3f51f37dfb4ba","impliedFormat":99},{"version":"53538d2fe5ed0d17a8fb3ed894b35590af1478a2864273f64ea153dc931f5137","impliedFormat":99},{"version":"ceed655ca8647e687782de09d86aa52aaeb71a04db53f7317b294afd98e745ec","impliedFormat":99},{"version":"a19f58bc593383ee611214b5aff06db6ec0c9abff34fbde4e937d56db230fe2d","impliedFormat":99},{"version":"12f07c1ca64c24a16c6b863a1b39053ba466391a42fb48fcdc731b5fbac19c2f","impliedFormat":99},{"version":"8fa98834033c5bd66d016570ad710aaeae12014b87799f57d8c107931308da75","impliedFormat":99},{"version":"5540adb5ed9f7b2a3b93cbae6ba4cc15c9c5aa0230f0b2c4ea65df9fb2d35ade","impliedFormat":99},{"version":"237549157534b24eb1486ad6338a5b11ad017da0b2a433597ca0d62c3978c166","impliedFormat":99},{"version":"14f449fd97d14b30b7d7e74ccb5dc52065397f5a159f109a81de462cbc2ee735","impliedFormat":99},{"version":"1da1c5e42d173cdb28539057403afbdcdba7ba70d1ca88131d478a3461146631","impliedFormat":99},{"version":"84e2defcc0264038c29dc40ffcb934623cf2906e721546276a11749645d650a9","impliedFormat":99},{"version":"981a6d13fdb1a917808b2c0021adf15df76b77d15ce32942f2567dacaa636818","impliedFormat":99},{"version":"8f2f419e0fefd82149f1bee26ef5962d27a2bd1c0ef5652f4558d603debd0cd0","impliedFormat":99},{"version":"20d839e1a5bd43ca94fc3855594dddfb4b04051ca87a6c0f61dee0d2135afed4","impliedFormat":99},{"version":"1327881b3a0726661b36d1d011b4722b301f6dd1d428b82d78f1bf94d7c5a1be","impliedFormat":99},{"version":"3776840bb69fac8d6715b70c236bd06b99ecdd2c0784a2cf0be7899fb7be13a3","impliedFormat":99},{"version":"ff224ec5ccda457a52bdf0747d643ad6029bc7ac8cf898f94047361fde72291c","impliedFormat":99},{"version":"b58c384659075652971d0c6cd25ad07ae6f193ee37820bd8daee6e3436a8aaa2","impliedFormat":99},{"version":"daecca2dbb88b1b1d759e2172245e8187180d777c2066296762811a33703db67","impliedFormat":99},{"version":"caf1e76335a50ba27f655209a1089e342e44c10410c834436308266cae1a0ed5","impliedFormat":99},{"version":"ce73f579eac2b41a8f1539d870ab294efc1b4e0a5680b57e027d2d69464bc5d4","impliedFormat":99},"4839ce7cb0b549e2b79beb82cbee5a549945bba77f9c523607f2b595bb20f553",{"version":"a4f6390e8692c6f38254c55185426fc4ad6e6957ec2926369ae5a916dbd5a005","signature":"53555e30c30798ce187c92aa8ab0c384c364b004f98ef27e827f927affc9d4c4"},{"version":"eddabe5960db89abd0ff9da48fcc90423904aaa5b6217612bb38abccbefce09d","signature":"ebc450cf4888573aef03e1d85e34f8587789f7436e4c80a3f1417a66429a9a29"},{"version":"0c4e98e79b5b8863bc295949f8fb4a7888ebab13c04cf44f94e34510725e5027","impliedFormat":99},{"version":"168bf3629d87ea4d9c947b4454b6b47c6ae646e697676f194e55cfe9fea29deb","impliedFormat":99},{"version":"445749eb9eaba85286f2329f7b043fbbaef2567f49cb89dab7d1ad941c506d8d","impliedFormat":99},{"version":"909e4aba623c453e3effe342f0d79c5fe0b4c0653004a8db316b3a3f53a6b7d3","impliedFormat":99},{"version":"0b2d6655ae1c1b1f8f825eb397d758a0b24b8a556dd03d3ad001e884f2171488","impliedFormat":99},{"version":"5105bbffcc9bf47e1de2380df13cd334b36fc3a582427826f40c8f627320ae16","impliedFormat":99},{"version":"adfda365e7a7b3f6b6c4909d8dd3a00c133bb67ad925204c358702b32e2351ee","impliedFormat":99},{"version":"c2ba6dea9b641828281fad5dbcca7ed9ea263b2fae1d90ab98304f9582bb589e","impliedFormat":99},{"version":"622c3617ec1da0036d7a68ac30c01fa401791a320898ea866c29ae49a293f4db","impliedFormat":99},{"version":"6cc0caaae7b01ddd72c9c36ab12827b6a1e4ed722e4433d7d3f8903ed31645ae","impliedFormat":99},{"version":"bd98c5555dd88b9f9686e5711cd3b42380a1958eeb4d5aee8249c4b84393471e","impliedFormat":99},{"version":"f431f0000f3ff27dd8824931dbf17c8bc68ee2551eec691f2812bae39bb3ce45","impliedFormat":99},{"version":"4e1deafdca057e04cc798f8c6bdecec1b624d1c2df1591848663939daffbd675","impliedFormat":99},{"version":"1fc64c6bda09d1f250436c0690262eeab55f803d1abc2b7e0620ef8cc1500087","impliedFormat":99},{"version":"0a76797de90647d13ddae055b94bebb9aaa24f96fdfb3c99556677652e98167b","impliedFormat":99},{"version":"a0cc536a85dca0de44d25c2c704e4e4009f6b6eabe3ae1e965c886529a6f8341","impliedFormat":99},{"version":"06d6a979c4cac9fe223b5fb768fc6c7c31586308a1360a6edf5971afe129faab","impliedFormat":99},{"version":"432904aba95c74008e841a949b01d39e84915e5b9fc58a075894d802fa60dace","impliedFormat":99},{"version":"0cacc9158e69c16f24cf7f4d7497ddc0bb641b39c085bd5d93963ac218a185fc","impliedFormat":99},{"version":"19005244c31b94d94c872d226b684c11143f554f772e36b09f4aa9159192be74","impliedFormat":99},{"version":"94a003d5819de12cbd240a778c4b8543c8f693d85468223a2cc6878be18cf81f","impliedFormat":99},{"version":"c098ecfc6f67903ac2a51b9aae5b64cd04672fa7f194032e44efdff4e435ca2c","impliedFormat":99},{"version":"8d56dc11df16222ec8f72002d31b40bef472a96a73d713d760c4b3927787f8c6","impliedFormat":99},{"version":"4d46e4bc399668902131af2af99959f735e5d664687b05a8b85ce6684b1a81e5","impliedFormat":99},{"version":"77d7913479c9ce4ea7a4a31eb4d82a7f5059bed39ba4b05386784bbe96da88c4","impliedFormat":99},{"version":"888d7eb20ac16f2d61ebca87a6c6366c9be236356cf207d25814f60b141ca822","impliedFormat":99},{"version":"a7775e809c78f34e318f3b556f6b48986fcacae382363de053d22531315019b9","impliedFormat":99},{"version":"e7b0aaf53c4aeb8777bf07cc717341b8c248243e4eac281fbff9b1b9b5b30609","impliedFormat":99},{"version":"9aee2824983bbea0aed365194e957a4e7e596927a05a7a0a189ed259baa0b615","impliedFormat":99},{"version":"723c8d96de2952aa6400859bc69a54995bea3cfcf1cde9ea273a491e055d76e0","impliedFormat":99},{"version":"416f76fcfa73d480084832cce6748a0bab9aaac25b13b8d941efe8922c26165d","impliedFormat":99},{"version":"9b1e0b3ef3bfd4dd83832298a45d21f36363f789ec62ceeda33b6174bdd0ed76","impliedFormat":99},{"version":"629a5b84340b49c75877efe93e5411f8e0d30457e2e30d441d6eaeccf7bf48c5","impliedFormat":99},{"version":"15762c497de33e524bb6785239bfbb88b9c0b86b00d306a7b48d88b93def28a6","impliedFormat":99},{"version":"a756f43fd580c04ed22c6a17f994da1ac776ae11f1b0ad354fa7a22737e6491a","impliedFormat":99},{"version":"7285083af8d20661af5df4f6b3a5ff8c406ab4e271eae9cc29cb822227f0def0","signature":"f3ebb5e5e3eb92ed6c2d86a786c6a79929003b2c6b4df787adf6faf63d0ddbb3"},{"version":"f88099409b07bb6756425e40a266cf5f3399ba9d6284ec0eb2798780171f739d","signature":"baf781c12aa7291705f20801275fc2e818bd26a254e9ad8a51d7f74a9cbe8a96"},"eec4f1971533031eb841bc17d8a93207a6b57b8cb337bf67852619eb41f04c84",{"version":"c1d0f0f67c564267ae1f3fc16d54a38a61799962eadc074ffc8d77464323de0c","signature":"ccfef0898e782249205295151b32dfbb9cbafa7eab9443d55b73143983a73775"},{"version":"8658354b90861a76abc7b3c04ece2124295c7da0cc4c4d31c2c78d8607188d03","impliedFormat":1},"ccd5f7963d8c2b34cc1a0177a4f61c8c12baf3d7a8946b5927f48a5a736b8dbd",{"version":"b62b463ec076bbbc96a5b561aa71c14a6273fa2d82f58add5eb2bf7e6d989bf8","impliedFormat":99},{"version":"0c99736eab237838305321a605605bc3eeb3c35839b65fd5f0e5d8d70df45809","impliedFormat":99},{"version":"a9373d52584b48809ffd61d74f5b3dfd127da846e3c4ee3c415560386df3994b","impliedFormat":99},{"version":"caf4af98bf464ad3e10c46cf7d340556f89197aab0f87f032c7b84eb8ddb24d9","impliedFormat":99},{"version":"68b6a7501a56babd7bcd840e0d638ee7ec582f1e70b3c36ebf32e5e5836913c8","impliedFormat":99},{"version":"7a14bf21ae8a29d64c42173c08f026928daf418bed1b97b37ac4bb2aa197b89b","impliedFormat":99},{"version":"ecb63945343b2d3c0ab28356d42b0c67b5a3e4018e5eb3729cbab733b85269b4","impliedFormat":99},{"version":"cc43d27c71ecb4b9f37edf5d7d7e5a6514fb3ff6296bd4a2059e62792f53067c","impliedFormat":99},{"version":"cfaef7ae1c921cfb29b1edab8f3374d757186fabcffb4165b2d8021243e15d62","impliedFormat":99},{"version":"f4e9ab8532f0f95e71bb60752503e0ad1eb4b9d2f258943ec1bb83a5b0d8c77c","impliedFormat":99},{"version":"0b7eb8afdaa3e7ff2ddbd9630ddff4baf9f4e7f3fde86794a41d230455c0d893","impliedFormat":99},{"version":"1c346e7d145fddb858fa0fbdc81b49ca1431c52eb54c88f605a3dc91f9ad1cfc","impliedFormat":99},{"version":"846d65880ee8a56e26b286eb5445d117317a347118181e4c3dc7d4aa95baba4c","signature":"165ef1272a23e87c93a470fff8672daa881dbd40548749b1e4b76c5f9f15415b"},{"version":"5bd483bc1e50b14f8552ea10ced88526acc14832ea4559bf4c2d7ec09be75e34","signature":"7d78eadc676cdc9aab54c5bdf9960300f1081784245561fe1a528c79578b214b"},{"version":"0943a6e4e026d0de8a4969ee975a7283e0627bf41aa4635d8502f6f24365ac9b","impliedFormat":99},{"version":"1461efc4aefd3e999244f238f59c9b9753a7e3dfede923ebe2b4a11d6e13a0d0","impliedFormat":99},{"version":"429771b1696015ee35641de21b318d29fe2789b24cca3af38a88ffbb0b293077","impliedFormat":99},{"version":"e1bd4b7457afa3053ac678573b6927e00d7e20556d6fe9a1a662bfeafd1622e5","impliedFormat":99},{"version":"96951030146671c2dc7b60360e4e6fad4d59ffbc3dda5f5c6cb0b77de00d2d29","impliedFormat":99},{"version":"751e2cad40a84a258f5fe151d8c563544ff1911fd90b30fabee0319a06f13e4a","signature":"8ea9a78367fed70b11f8eddbefbd27fad0e7ab5e49aec663318fe9d66e921976"},{"version":"5ab3c5a0d1320110ed38b6e023c7868dfb447925348ec1a8177a7f8688aa17c2","impliedFormat":99},{"version":"21a71137bf72a9adeb9b433e932dc343197a84b79767a335b1f8e74c4055949e","impliedFormat":99},{"version":"421ffe85b2c7cdaae336fa63c9de2c83e1b9dffb6e3c0c90c33a895ee65831cb","impliedFormat":99},{"version":"6c05d0fcee91437571513c404e62396ee798ff37a2d8bef2104accdc79deb9c0","impliedFormat":1},{"version":"8ca10ebfe5a09f5fb32469aeff2c5eba4332e0fdd947cd4a70d6d5826a0bfaac","impliedFormat":99},{"version":"54c95df9221956ff26117e0804c5e08a9b72af80c26990492f2dba8de0198caa","impliedFormat":99},{"version":"919a6bc7f25762983065b97abfb753a69acea56d00b1eaffc253ec52971f7659","impliedFormat":99},{"version":"c57b441e0c0a9cbdfa7d850dae1f8a387d6f81cbffbc3cd0465d530084c2417d","impliedFormat":99},{"version":"2fbe402f0ee5aa8ab55367f88030f79d46211c0a0f342becaa9f648bf8534e9d","impliedFormat":1},{"version":"b94258ef37e67474ac5522e9c519489a55dcb3d4a8f645e335fc68ea2215fe88","impliedFormat":1},{"version":"c435ab831345f6c3d7f826539c09fc10b4120fc3510574bf845fb77d04533d6c","impliedFormat":99},{"version":"21edd654f056d18819b1adcc87415b4a474983578ef34ff2f481ecdb1e7b7c11","impliedFormat":99},{"version":"fad1015e194a466be5fb58e0693529dec40ae530bf5fbb7dbbb152367e654197","impliedFormat":99},{"version":"0552198e151acafe39bb50811185a6a2ee7f2bcd5a688c3a5fc1bc4dabacc66e","impliedFormat":99},{"version":"de16c21a712e700b913da3cd3b82d697c3e664b8adfc8aac8cbdd0292bd8509d","impliedFormat":99},{"version":"c967deea59e658210d03adc3b4437c5801205c391eabdd9af17dd49626127fea","impliedFormat":99},{"version":"efef8898a5f225818c1e82559f779f7b48824c95e50f7421dd93d613d85589e0","impliedFormat":99},{"version":"ab0a111172bef51dac7c251951066f5490ab65c36b22feb6ee60c4b3b05b9417","impliedFormat":99},{"version":"919ee66e6adf87a446443072e14b152e50ef06d78c32ff99d8d017bfac99cb79","impliedFormat":99},{"version":"94b39ed86fe847e8af31ba920c0e597600a0a4db32d9ccd8da2069ccd7b99ff7","impliedFormat":99},{"version":"99d1a601593495371e798da1850b52877bf63d0678f15722d5f048e404f002e4","impliedFormat":99},{"version":"edaab529e9cc82de3cdea505467771f3a5cebc55c6f6ea92cbcfcc57707af653","impliedFormat":99},{"version":"cc711bab5172da42f7190827ca818ec1662e08e0522b3749737cdc26b49c6155","impliedFormat":99},{"version":"ff24e3d89010c1b8dcc57b2bd0f135490f985b2acddd14253da9e574c291fcee","impliedFormat":99},{"version":"822b1053e08681dbdbaa3e461d23988b36fb0320b2338ac3227a41bdcd475d05","impliedFormat":99},{"version":"56c56602fc8e29b8a171f98e54e2194a719695744bfd7f5ec31e2dafca300519","impliedFormat":99},{"version":"77f47f65c2acaddce49978131e4330a12b3596cb64fdc33fe0aac2a09d4b8596","impliedFormat":99},{"version":"f62330823c7836e29c07995299d35afc90a888f81566b2b48807f5dca80fd24b","impliedFormat":99},{"version":"19a4c68487041176a03a570c6389538dc30585161e88d26eb482ac18dc4fa744","impliedFormat":99},{"version":"7bcf911d9867d432fd5a01e3a27251b3616f8f9736f9ed26f2c83f62337d8559","signature":"7237060c590b200df3597909a9a0d2bacbb1ad68dc62cd4338c9357a08826967"},{"version":"a2c83a4850633905d4d1c63f913ead38841253c5e50822a6846ad642cbbd59db","impliedFormat":99},{"version":"494566cf3ffff7ab496c240fc512253f6e84cfeb58ecc3b1d111d8c0acd7720a","impliedFormat":99},{"version":"30a1cfd1bf8e695ca18657e4666743a0b5576a9ccf9c88fe5f4aa965f34a7c92","signature":"0ef3bada4d456b048d0fb648488b06ab89ba1707255c6fb63049865353f4bbb5"},{"version":"c34f93fe11d5b7be9da47dd474499741d201c61f408df11b78c94a58c7689418","signature":"7e1036db1a2b3cc3cdf8a8de22a31f2acac0ea0eb7513992506f1871395dd719"},{"version":"c5ea79a331b24ec205ede27ad9ad6375ea86fff691d8a8018249975acf991cbb","impliedFormat":99},{"version":"0b018b64fe7c3f2f7823e1dbe4698642310782a7ec257b00763ac6ac108db48a","impliedFormat":99},{"version":"d1c44c309f5e14253d73bc9bfdabaadf4e6a03b94a49cfc8fd7fe6c69d7b4488","impliedFormat":99},{"version":"b7e9937889154383fe0369f22ff3b507c71603f5b576d62495077381d07885a8","impliedFormat":99},{"version":"24e30a7f1d7ef54f33cba1aff4eeaf468ea2f4bf7dba7d5dcc6f68fea027f1c5","impliedFormat":99},{"version":"7ec047b73f621c526468517fea779fec2007dd05baa880989def59126c98ef79","impliedFormat":99},{"version":"8dd450de6d756cee0761f277c6dc58b0b5a66b8c274b980949318b8cad26d712","impliedFormat":99},{"version":"6b5f886fe41e2e767168e491fe6048398ed6439d44e006d9f51cc31265f08978","impliedFormat":99},{"version":"56a87e37f91f5625eb7d5f8394904f3f1e2a90fb08f347161dc94f1ae586bdd0","impliedFormat":99},{"version":"6b863463764ae572b9ada405bf77aac37b5e5089a3ab420d0862e4471051393b","impliedFormat":99},{"version":"904d6ad970b6bd825449480488a73d9b98432357ab38cf8d31ffd651ae376ff5","impliedFormat":99},{"version":"2535fc1a5fe64892783ff8f61321b181c24f824e688a4a05ae738da33466605b","impliedFormat":99},{"version":"5010cde4bbbf8b1528f143d44a7b82d3bf5e46e3e7704b624c3d59140637cdcf","impliedFormat":99},{"version":"63ba60a47fed77820fb575dce84436bd245185635b4d1b6caccdc96a01750405","impliedFormat":99},{"version":"c74d76410dbbd13755b99b3426af9f4d99c6d1538178afeefcdd7ca6730cdaad","impliedFormat":99},{"version":"d8c2a4e5148f12589cb7f1d832c33bfbe7b69bcb0e8b76c38ed193d690c0049f","impliedFormat":99},{"version":"633ce6a96aa7d4257ebf52a33774b33bad4a97a83832e5b059bf317086367901","signature":"f5c8fc442ddce61f5e9b1a5dd836c48bf30d1fb3a037a065adeb9e24d4dc6818"},{"version":"5bb39804b24ef289b6da8531fe73c9784cd3d7f7b9e4a12d283d901d80eb2230","impliedFormat":1},{"version":"f8503038dcd9201494752bac5f20576ea1cb97e45470ec8a5072d17389deae49","impliedFormat":1},{"version":"7d8a6dec4f588a35012d6332c1c892d2e00073d49f027202c88e7cdc8a2d2dd5","impliedFormat":1},"783e73e46aeb3e1f9d63c731d7813f2ac2da03a5af9dc3c5676d2815102bc06f","88fcf2317b5f49bb215f09aaf12bf38145c0bfb7fef374615d8b2c2343c4d3d3",{"version":"9a2ad0ea0ecfcdffc553cc855b917a316a88d1eebba7c768f32e4b2068fa0f62","impliedFormat":99},{"version":"6eeadcb44ce04569e8238694cff479343d41f86939351f5e25c64c85a8f66596","signature":"b3c5e6c1e59d3416d68dcb9d0dfa42b0562aeca5955923b31e8673e19779f9a9"},{"version":"f9de39cf4120ff91727e9ddf5b068203753df7d9ac54e61f8f4354023e9ec9df","signature":"8405ce754b7c4bca810dd902d7c104e67e77d54297beccc5ac19a4306f09a759"},{"version":"18e4e04026491e2249aaa28a72d8039b0fbfa15052e4bbc80ddae9be85938a76","signature":"095a9fa87583c755c43734e171c87f3a5d899ddd45ad3a647914b927b33e6730"},{"version":"6a328ea91614a549d3ad11272c0a11a7f4df71001b68480e774973b337c01eba","signature":"bfa1ff9f7df9e7340f215a7ba9353d60f101259a78c7c3a3ed64f9d05ca08c79"},{"version":"d135c01436d86cd07ccf7758657c127ef44f479816e7c25f776fde9d9b10c2d3","impliedFormat":99},{"version":"f77bcc9219086419ae381df164b060ac3fcf8294b0c8e211c1e17c6586148459","impliedFormat":99},{"version":"f0a2546d972203c452bb540b2475cccb93f342eabf3ea20536e7dfa8e80ae330","signature":"64656a610bcff6cfa2a7becb0d1c815c6f750144a97593e0e85d72175c31c1ee"},{"version":"ba2c8511ea62f54d78948fa83db3655ccacf4e7d074a27eb4caaaf45ea29c8f2","impliedFormat":99},"ff15e542ede91e2735f07e523795be409b450f814c0163c59ed1f6baee4da8cf",{"version":"150653478b80c3bb651df96105e3a93c457147e12b16da350284edc36c8d7636","affectsGlobalScope":true},{"version":"afb79c74b7e009d3c34b5cbbf8183363fc9b6c9bcb6ed4e509638a9a33943916","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},"d1986184a09a52db8228cb2bb2a61a8c05c9354e5b93cec8e2628d8579c892d7",{"version":"16c868e0106d0a5a7cc9bf94bde4b1e326290603e28bccb45e35d3c1e9445a47","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fb893a0dfc3c9fb0f9ca93d0648694dd95f33cbad2c0f2c629f842981dfd4e2e","impliedFormat":1},{"version":"3eb11dbf3489064a47a2e1cf9d261b1f100ef0b3b50ffca6c44dd99d6dd81ac1","impliedFormat":1},{"version":"8e0733c50eaac49b4e84954106acc144ec1a8019922d6afcde3762523a3634af","impliedFormat":1}],"root":[552,553,818,821,[3853,3855],[3891,3894],3896,3909,3910,3916,3946,3949,3950,3967,3971,3972,[3974,3977],3980,[3982,3986]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":4,"module":99,"skipLibCheck":true,"strict":true,"target":4},"referencedMap":[[3985,1],[552,2],[3986,3],[3983,2],[3984,4],[3980,5],[3982,6],[821,7],[3977,8],[3891,9],[3949,10],[3946,11],[3950,12],[3893,13],[3894,14],[3855,15],[3910,16],[3967,17],[3971,18],[3853,11],[3972,19],[3976,20],[3909,21],[3974,22],[3975,23],[3896,24],[3892,25],[3854,26],[3916,27],[553,28],[806,29],[799,30],[797,31],[804,32],[798,2],[805,33],[803,34],[396,2],[3885,35],[3884,36],[3872,37],[3866,38],[3859,39],[3886,2],[3875,40],[3867,37],[3870,41],[3868,2],[3858,2],[3888,42],[3887,43],[3869,39],[3873,44],[3874,39],[3857,39],[3876,45],[3877,37],[3879,46],[3878,37],[3880,47],[3881,39],[3882,48],[3861,39],[3863,37],[3864,2],[3862,37],[3860,40],[3865,40],[3871,49],[3883,39],[2340,50],[2341,50],[2342,50],[2344,50],[2345,50],[2346,50],[2347,50],[2348,50],[2349,50],[2350,50],[2343,50],[2351,50],[2352,50],[2353,50],[2354,50],[2355,50],[2356,50],[2357,50],[2358,50],[2359,50],[2360,50],[2361,50],[2362,50],[2363,50],[2364,50],[2365,50],[2366,50],[2367,50],[2368,50],[2369,50],[2370,50],[2371,50],[2372,50],[2375,50],[2376,50],[2377,50],[2373,50],[2374,50],[2378,50],[2379,50],[2380,50],[2381,50],[2382,50],[2383,50],[2384,50],[2385,50],[2386,50],[2387,50],[2388,50],[2389,50],[2390,50],[2391,50],[2392,50],[2393,50],[2394,50],[2395,50],[2396,50],[2397,50],[2398,50],[2399,50],[2400,50],[2401,50],[2402,50],[2403,50],[2404,50],[2405,50],[2406,50],[2407,50],[2408,50],[2409,50],[2410,50],[2411,50],[2412,50],[2413,50],[2414,50],[2415,50],[2416,50],[2417,50],[2418,50],[2419,50],[2421,50],[2422,50],[2423,50],[2424,50],[2420,50],[2425,50],[2426,50],[2427,50],[2428,50],[2429,50],[2430,50],[2431,50],[2432,50],[2433,50],[2434,50],[2435,50],[2436,50],[2458,50],[2459,50],[2460,50],[2461,50],[2462,50],[2463,50],[2464,50],[2465,50],[2466,50],[2467,50],[2468,50],[2469,50],[2470,50],[2471,50],[2472,50],[2473,50],[2437,50],[2438,50],[2439,50],[2440,50],[2441,50],[2442,50],[2443,50],[2444,50],[2445,50],[2446,50],[2474,50],[2475,50],[2447,50],[2448,50],[2449,50],[2450,50],[2455,50],[2456,50],[2457,50],[2451,50],[2452,50],[2453,50],[2454,50],[2476,50],[2477,50],[2478,50],[2479,50],[2480,50],[2481,50],[2482,50],[2483,50],[2484,50],[2485,50],[2486,50],[2487,50],[2488,50],[2489,50],[2490,50],[2491,50],[2492,50],[2493,50],[2494,50],[2495,50],[2496,50],[2497,50],[2498,50],[2499,50],[2500,50],[2501,50],[2502,50],[2503,50],[2504,50],[2505,50],[2506,50],[2507,50],[2508,50],[2509,50],[2510,50],[2511,50],[2512,50],[2513,50],[2514,50],[2515,50],[2516,50],[2517,50],[2518,50],[2519,50],[2520,50],[2521,50],[2522,50],[2523,50],[2524,50],[2525,50],[2526,50],[2527,50],[2528,50],[2529,50],[2530,50],[2531,50],[2532,50],[2533,50],[2534,50],[2535,50],[2536,50],[2537,50],[2538,50],[2539,50],[2540,50],[2541,50],[2542,50],[2543,50],[2544,50],[2545,50],[2546,50],[2547,50],[2548,50],[2549,50],[2550,50],[2551,50],[2555,50],[2557,50],[2556,50],[2558,50],[2552,50],[2553,50],[2554,50],[2559,50],[2560,50],[2561,50],[2562,50],[2563,50],[2565,50],[2564,50],[2566,50],[2567,50],[2568,50],[2569,50],[2570,50],[2571,50],[2572,50],[2573,50],[2574,50],[2575,50],[2576,50],[2577,50],[2578,50],[2579,50],[2580,50],[2581,50],[2582,50],[2584,50],[2583,50],[2585,50],[2587,50],[2586,50],[2588,50],[2589,50],[2590,50],[2591,50],[2592,50],[2593,50],[2594,50],[2595,50],[2596,50],[2598,50],[2597,50],[2599,50],[2600,50],[2601,50],[2602,50],[2603,50],[2604,50],[2605,50],[2606,50],[2607,50],[2608,50],[2609,50],[2610,50],[2611,50],[2612,50],[2613,50],[2615,50],[2614,50],[2618,50],[2619,50],[2620,50],[2621,50],[2622,50],[2623,50],[2624,50],[2625,50],[2626,50],[2627,50],[2628,50],[2629,50],[2630,50],[2631,50],[2632,50],[2633,50],[2634,50],[2635,50],[2636,50],[2637,50],[2638,50],[2639,50],[2640,50],[2641,50],[2642,50],[2616,50],[2643,50],[2617,50],[2644,50],[2645,50],[2646,50],[2647,50],[2648,50],[2649,50],[2650,50],[2651,50],[2652,50],[2653,50],[2654,50],[2655,50],[2656,50],[2657,50],[2658,50],[2659,50],[2660,50],[2661,50],[2662,50],[2663,50],[2664,50],[2665,50],[2666,50],[2667,50],[2668,50],[2669,50],[2670,50],[2671,50],[2672,50],[2673,50],[2674,50],[2675,50],[2676,50],[2677,50],[2678,50],[2679,50],[2680,50],[2681,50],[2682,50],[2689,50],[2690,50],[2683,50],[2691,50],[2684,50],[2685,50],[2686,50],[2687,50],[2688,50],[2692,50],[2693,50],[2697,50],[2694,50],[2698,50],[2695,50],[2696,50],[2699,50],[2700,50],[2701,50],[2702,50],[2703,50],[2704,50],[2705,50],[2706,50],[2707,50],[2708,50],[2709,50],[2710,50],[2711,50],[2712,50],[2713,50],[2714,50],[2715,50],[2716,50],[2717,50],[2719,50],[2718,50],[2720,50],[2721,50],[2722,50],[2723,50],[2724,50],[2727,50],[2725,50],[2726,50],[2728,50],[2729,50],[2730,50],[2731,50],[2732,50],[2733,50],[2734,50],[2735,50],[2736,50],[2737,50],[2738,50],[2739,50],[2740,50],[2741,50],[2743,50],[2742,50],[2745,50],[2746,50],[2744,50],[2748,50],[2747,50],[2749,50],[2751,50],[2750,50],[2752,50],[2753,50],[2754,50],[2755,50],[2756,50],[2757,50],[2758,50],[2759,50],[2760,50],[2761,50],[2762,50],[2763,50],[2764,50],[2765,50],[2767,50],[2768,50],[2766,50],[2769,50],[2770,50],[2771,50],[2772,50],[2773,50],[2774,50],[2775,50],[2776,50],[2777,50],[2778,50],[2779,50],[2780,50],[2781,50],[2782,50],[2783,50],[2784,50],[2785,50],[2786,50],[2787,50],[2788,50],[2789,50],[2790,50],[2791,50],[2792,50],[2793,50],[2794,50],[2795,50],[2796,50],[2797,50],[2798,50],[2799,50],[2800,50],[2801,50],[2802,50],[2803,50],[2804,50],[2805,50],[2806,50],[2807,50],[2808,50],[2809,50],[2810,50],[2811,50],[2812,50],[2814,50],[2815,50],[2816,50],[2817,50],[2818,50],[2822,50],[2819,50],[2820,50],[2821,50],[2813,50],[2823,50],[2824,50],[2825,50],[2826,50],[2827,50],[2828,50],[2829,50],[2830,50],[2831,50],[2832,50],[2833,50],[2834,50],[2835,50],[2836,50],[2837,50],[2838,50],[2839,50],[2840,50],[2841,50],[2842,50],[2843,50],[2844,50],[2845,50],[2846,50],[2847,50],[2848,50],[2849,50],[2850,50],[2851,50],[2852,50],[2853,50],[2854,50],[2855,50],[2856,50],[2861,50],[2857,50],[2858,50],[2859,50],[2860,50],[2862,50],[2863,50],[2864,50],[2865,50],[2866,50],[2867,50],[2868,50],[2869,50],[2870,50],[2871,50],[2872,50],[2873,50],[2874,50],[2875,50],[2876,50],[2877,50],[2878,50],[2879,50],[2880,50],[2881,50],[2882,50],[2883,50],[2884,50],[2885,50],[2886,50],[2888,50],[2889,50],[2890,50],[2891,50],[2887,50],[2893,50],[2892,50],[2894,50],[2895,50],[2896,50],[2897,50],[2898,50],[2899,50],[2900,50],[2901,50],[2902,50],[2903,50],[2904,50],[2909,50],[2905,50],[2906,50],[2907,50],[2908,50],[2910,50],[2912,50],[2913,50],[2914,50],[2911,50],[2915,50],[2916,50],[2917,50],[2918,50],[2919,50],[2920,50],[2921,50],[2922,50],[2923,50],[2924,50],[2925,50],[2926,50],[2927,50],[2928,50],[2929,50],[2930,50],[2931,50],[2932,50],[2933,50],[2934,50],[2946,50],[2935,50],[2936,50],[2937,50],[2938,50],[2939,50],[2940,50],[2941,50],[2942,50],[2943,50],[2944,50],[2945,50],[2947,50],[2948,50],[2949,50],[2950,50],[2951,50],[2952,50],[2953,50],[2954,50],[2955,50],[2956,50],[2957,50],[2958,50],[2959,50],[2960,50],[2961,50],[2964,50],[2962,50],[2963,50],[2965,50],[2966,50],[2967,50],[2968,50],[2969,50],[2970,50],[2971,50],[2973,50],[2972,50],[2974,50],[2975,50],[2976,50],[2977,50],[2978,50],[2979,50],[2980,50],[2981,50],[2982,50],[2983,50],[2984,50],[2994,50],[2985,50],[2986,50],[2987,50],[2988,50],[2989,50],[2990,50],[2991,50],[2992,50],[2993,50],[2995,50],[2996,50],[2997,50],[2998,50],[2999,50],[3000,50],[3001,50],[3002,50],[3003,50],[3004,50],[3005,50],[3006,50],[3007,50],[3008,50],[3009,50],[3010,50],[3011,50],[3012,50],[3013,50],[3014,50],[3015,50],[3016,50],[3017,50],[3018,50],[3019,50],[3020,50],[3021,50],[3022,50],[3023,50],[3024,50],[3025,50],[3026,50],[3027,50],[3028,50],[3031,50],[3032,50],[3033,50],[3029,50],[3030,50],[3034,50],[3035,50],[3036,50],[3037,50],[3038,50],[3039,50],[3040,50],[3041,50],[3042,50],[3043,50],[3044,50],[3045,50],[3046,50],[3047,50],[3048,50],[3049,50],[3050,50],[3051,50],[3052,50],[3053,50],[3054,50],[3055,50],[3056,50],[3057,50],[3058,50],[3059,50],[3060,50],[3061,50],[3062,50],[3063,50],[3064,50],[3065,50],[3066,50],[3067,50],[3070,50],[3068,50],[3069,50],[3086,50],[3087,50],[3071,50],[3072,50],[3073,50],[3074,50],[3075,50],[3076,50],[3077,50],[3078,50],[3079,50],[3088,50],[3090,50],[3080,50],[3089,50],[3081,50],[3082,50],[3083,50],[3084,50],[3085,50],[3091,50],[3092,50],[3093,50],[3094,50],[3095,50],[3096,50],[3097,50],[3098,50],[3099,50],[3100,50],[3105,50],[3101,50],[3102,50],[3103,50],[3104,50],[3106,50],[3107,50],[3108,50],[3109,50],[3110,50],[3111,50],[3112,50],[3113,50],[3114,50],[3115,50],[3116,50],[3117,50],[3118,50],[3119,50],[3120,50],[3121,50],[3122,50],[3123,50],[3124,50],[3125,50],[3126,50],[3127,50],[3128,50],[3129,50],[3130,50],[3131,50],[3133,50],[3132,50],[3134,50],[3135,50],[3136,50],[3137,50],[3138,50],[3141,50],[3139,50],[3140,50],[3142,50],[3143,50],[3144,50],[3145,50],[3146,50],[3147,50],[3148,50],[3149,50],[3151,50],[3152,50],[3150,50],[3153,50],[3154,50],[3155,50],[3156,50],[3157,50],[3158,50],[3159,50],[3160,50],[3161,50],[3162,50],[3163,50],[3164,50],[3165,50],[3166,50],[3167,50],[3168,50],[3169,50],[3170,50],[3171,50],[3172,50],[3173,50],[3174,50],[3176,50],[3175,50],[3177,50],[3178,50],[3179,50],[3180,50],[3181,50],[3182,50],[3187,50],[3183,50],[3184,50],[3185,50],[3186,50],[3188,50],[3189,50],[3190,50],[3191,50],[3192,50],[3193,50],[3194,50],[3195,50],[3196,50],[3197,50],[3198,50],[3199,50],[3207,50],[3200,50],[3201,50],[3202,50],[3203,50],[3204,50],[3205,50],[3206,50],[3208,50],[3209,50],[3210,50],[3211,50],[3212,50],[3213,50],[3214,50],[3215,50],[3217,50],[3216,50],[3218,50],[3219,50],[3221,50],[3220,50],[3222,50],[3223,50],[3224,50],[3225,50],[3226,50],[3227,50],[3228,50],[3229,50],[3230,50],[3231,50],[3232,50],[3233,50],[3234,50],[3235,50],[3236,50],[3237,50],[3238,50],[3239,50],[3240,50],[3241,50],[3242,50],[3243,50],[3244,50],[3245,50],[3246,50],[3247,50],[3248,50],[3249,50],[3250,50],[3251,50],[3252,50],[3253,50],[3254,50],[3255,50],[3256,50],[3258,50],[3257,50],[3259,50],[3260,50],[3261,50],[3262,50],[3263,50],[3264,50],[3265,50],[3266,50],[3267,50],[3268,50],[3269,50],[3270,50],[3271,50],[3272,50],[3273,50],[3275,50],[3274,50],[3276,50],[3277,50],[3278,50],[3279,50],[3280,50],[3281,50],[3282,50],[3283,50],[3284,50],[3285,50],[3290,50],[3292,50],[3291,50],[3294,50],[3295,50],[3293,50],[3286,50],[3296,50],[3297,50],[3287,50],[3288,50],[3289,50],[3298,50],[3299,50],[3300,50],[3301,50],[3302,50],[3303,50],[3304,50],[3305,50],[3306,50],[3307,50],[3308,50],[3309,50],[3310,50],[3311,50],[3312,50],[3313,50],[3314,50],[3315,50],[3316,50],[3317,50],[3318,50],[3319,50],[3320,50],[3321,50],[3322,50],[3323,50],[3324,50],[3325,50],[3326,50],[3327,50],[3328,50],[3329,50],[3330,50],[3331,50],[3332,50],[3333,50],[3334,50],[3335,50],[3336,50],[3337,50],[3338,50],[3339,50],[3340,50],[3341,50],[3342,50],[3343,50],[3344,50],[3345,50],[3346,50],[3347,50],[3348,50],[3352,50],[3353,50],[3349,50],[3350,50],[3351,50],[3354,50],[3355,50],[3356,50],[3357,50],[3358,50],[3359,50],[3360,50],[3361,50],[3362,50],[3363,50],[3364,50],[3365,50],[3366,50],[3369,50],[3370,50],[3371,50],[3372,50],[3373,50],[3374,50],[3375,50],[3376,50],[3367,50],[3368,50],[3377,50],[3378,50],[3379,50],[3380,50],[3382,50],[3381,50],[3384,50],[3383,50],[3385,50],[3386,50],[3387,50],[3388,50],[3389,50],[3390,50],[3391,50],[3392,50],[3393,50],[3394,50],[3395,50],[3396,50],[3397,50],[3398,50],[3399,50],[3400,50],[3401,50],[3402,50],[3403,50],[3404,50],[3405,50],[3406,50],[3407,50],[3408,50],[3409,50],[3410,50],[3411,50],[3412,50],[3413,50],[3415,50],[3414,50],[3416,50],[3417,50],[3418,50],[3419,50],[3420,50],[3421,50],[3422,50],[3423,50],[3424,50],[3425,50],[3427,50],[3426,50],[3428,50],[3429,50],[3430,50],[3431,50],[3432,50],[3433,50],[3434,50],[3435,50],[3436,50],[3437,50],[3438,50],[3439,50],[3440,50],[3441,50],[3442,50],[3443,50],[3444,50],[3445,50],[3446,50],[3447,50],[3448,50],[3449,50],[3450,50],[3451,50],[3452,50],[3453,50],[3454,50],[3455,50],[3456,50],[3457,50],[3458,50],[3459,50],[3460,50],[3461,50],[3462,50],[3463,50],[3464,50],[3465,50],[3467,50],[3466,50],[3468,50],[3469,50],[3470,50],[3471,50],[3472,50],[3473,50],[3474,50],[3475,50],[3476,50],[3477,50],[3478,50],[3479,50],[3480,50],[3481,50],[3482,50],[3483,50],[3484,50],[3485,50],[3486,50],[3487,50],[3488,50],[3489,50],[3490,50],[3491,50],[3492,50],[3493,50],[3494,50],[3495,50],[3496,50],[3497,50],[3498,50],[3499,50],[3500,50],[3501,50],[3502,50],[3503,50],[3504,50],[3505,50],[3506,50],[3507,50],[3508,50],[3509,50],[3510,50],[3511,50],[3512,50],[3513,50],[3514,50],[3517,50],[3515,50],[3516,50],[3518,50],[3519,50],[3520,50],[3521,50],[3522,50],[3523,50],[3524,50],[3525,50],[3526,50],[3527,50],[3528,50],[3529,50],[3530,50],[3531,50],[3532,50],[3533,50],[3534,50],[3535,50],[3536,50],[3537,50],[3538,50],[3539,50],[3540,50],[3541,50],[3542,50],[3543,50],[3545,50],[3544,50],[3546,50],[3547,50],[3548,50],[3549,50],[3552,50],[3550,50],[3551,50],[3553,50],[3554,50],[3555,50],[3556,50],[3557,50],[3558,50],[3559,50],[3560,50],[3561,50],[3562,50],[3563,50],[3564,50],[3565,50],[3566,50],[3569,50],[3567,50],[3568,50],[3570,50],[3571,50],[3572,50],[3573,50],[3574,50],[3575,50],[3576,50],[3577,50],[3578,50],[3579,50],[3580,50],[3581,50],[3582,50],[3583,50],[3584,50],[3585,50],[3586,50],[3587,50],[3588,50],[3589,50],[3590,50],[3591,50],[3592,50],[3593,50],[3594,50],[3595,50],[3596,50],[3597,50],[3598,50],[3599,50],[3600,50],[3601,50],[3602,50],[3604,50],[3603,50],[3605,50],[3606,50],[3607,50],[3608,50],[3609,50],[3610,50],[3611,50],[3612,50],[3613,50],[3614,50],[3617,50],[3615,50],[3616,50],[3618,50],[3619,50],[3620,50],[3621,50],[3622,50],[3623,50],[3624,50],[3625,50],[3627,50],[3626,50],[3628,50],[3629,50],[3630,50],[3631,50],[3632,50],[3633,50],[3634,50],[3635,50],[3636,50],[3637,50],[3638,50],[3639,50],[3640,50],[3641,50],[3642,50],[3643,50],[3644,50],[3645,50],[3646,50],[3647,50],[3648,50],[3649,50],[3650,50],[3651,50],[3652,50],[3653,50],[3654,50],[3656,50],[3655,50],[3657,50],[3658,50],[3659,50],[3660,50],[3661,50],[3662,50],[3663,50],[3664,50],[3666,50],[3667,50],[3668,50],[3669,50],[3670,50],[3671,50],[3672,50],[3673,50],[3674,50],[3675,50],[3676,50],[3677,50],[3678,50],[3679,50],[3680,50],[3681,50],[3683,50],[3684,50],[3685,50],[3686,50],[3687,50],[3682,50],[3688,50],[3706,50],[3689,50],[3690,50],[3691,50],[3692,50],[3693,50],[3694,50],[3695,50],[3696,50],[3697,50],[3698,50],[3699,50],[3700,50],[3701,50],[3702,50],[3703,50],[3704,50],[3705,50],[3707,50],[3708,50],[3709,50],[3710,50],[3711,50],[3712,50],[3713,50],[3714,50],[3715,50],[3716,50],[3717,50],[3718,50],[3719,50],[3721,50],[3720,50],[3722,50],[3723,50],[3724,50],[3725,50],[3726,50],[3727,50],[3728,50],[3729,50],[3730,50],[3731,50],[3732,50],[3733,50],[3734,50],[3735,50],[3736,50],[3737,50],[3738,50],[3739,50],[3740,50],[3741,50],[3742,50],[3743,50],[3744,50],[3745,50],[3748,50],[3746,50],[3747,50],[3749,50],[3750,50],[3751,50],[3752,50],[3753,50],[3754,50],[3755,50],[3756,50],[3757,50],[3758,50],[3759,50],[3760,50],[3761,50],[3762,50],[3763,50],[3665,50],[3764,50],[3765,50],[3766,50],[3767,50],[3768,50],[3769,50],[3770,50],[3771,50],[3772,50],[3773,50],[3774,50],[3775,50],[3776,50],[3777,50],[3778,50],[3779,50],[3780,50],[3781,50],[3782,50],[3783,50],[3784,50],[3785,50],[3786,50],[3787,50],[3788,50],[3792,50],[3793,50],[3789,50],[3790,50],[3794,50],[3791,50],[3795,50],[3796,50],[3797,50],[3798,50],[3799,50],[3800,50],[3801,50],[3802,50],[3803,50],[3804,50],[3805,50],[3806,50],[3807,50],[3808,50],[3809,50],[3810,50],[3811,50],[3812,50],[3813,50],[3814,50],[3815,50],[3816,50],[3817,50],[3818,50],[3819,50],[3824,50],[3825,50],[3826,50],[3820,50],[3821,50],[3822,50],[3823,50],[3827,50],[3828,50],[3829,50],[3830,50],[3831,50],[3832,50],[3833,50],[3834,50],[3835,50],[3836,50],[3837,50],[3838,50],[3839,50],[3840,50],[3841,50],[3842,50],[3843,50],[3844,50],[3845,50],[3846,50],[3847,50],[3848,50],[3849,50],[3850,50],[3851,50],[3852,51],[823,52],[824,52],[826,53],[825,52],[822,54],[827,50],[828,50],[829,50],[831,50],[832,50],[833,50],[834,50],[835,50],[836,50],[837,50],[830,50],[838,50],[839,50],[840,50],[841,50],[842,50],[843,50],[844,50],[845,50],[846,50],[847,50],[848,50],[849,50],[850,50],[851,50],[852,50],[853,50],[854,50],[855,50],[856,50],[857,50],[858,50],[859,50],[862,50],[863,50],[864,50],[860,50],[861,50],[865,50],[866,50],[867,50],[868,50],[869,50],[870,50],[871,50],[872,50],[873,50],[874,50],[875,50],[876,50],[877,50],[878,50],[879,50],[880,50],[881,50],[882,50],[883,50],[884,50],[885,50],[886,50],[887,50],[888,50],[889,50],[890,50],[891,50],[892,50],[893,50],[894,50],[895,50],[896,50],[897,50],[898,50],[899,50],[900,50],[901,50],[902,50],[903,50],[904,50],[905,50],[906,50],[908,50],[909,50],[910,50],[911,50],[907,50],[912,50],[913,50],[914,50],[915,50],[916,50],[917,50],[918,50],[919,50],[920,50],[921,50],[922,50],[923,50],[945,50],[946,50],[947,50],[948,50],[949,50],[950,50],[951,50],[952,50],[953,50],[954,50],[955,50],[956,50],[957,50],[958,50],[959,50],[960,50],[924,50],[925,50],[926,50],[927,50],[928,50],[929,50],[930,50],[931,50],[932,50],[933,50],[961,50],[962,50],[934,50],[935,50],[936,50],[937,50],[942,50],[943,50],[944,50],[938,50],[939,50],[940,50],[941,50],[963,50],[964,50],[965,50],[966,50],[967,50],[968,50],[969,50],[970,50],[971,50],[972,50],[973,50],[974,50],[975,50],[976,50],[977,50],[978,50],[979,50],[980,50],[981,50],[982,50],[983,50],[984,50],[985,50],[986,50],[987,50],[988,50],[989,50],[990,50],[991,50],[992,50],[993,50],[994,50],[995,50],[996,50],[997,50],[998,50],[999,50],[1000,50],[1001,50],[1002,50],[1003,50],[1004,50],[1005,50],[1006,50],[1007,50],[1008,50],[1009,50],[1010,50],[1011,50],[1012,50],[1013,50],[1014,50],[1015,50],[1016,50],[1017,50],[1018,50],[1019,50],[1020,50],[1021,50],[1022,50],[1023,50],[1024,50],[1025,50],[1026,50],[1027,50],[1028,50],[1029,50],[1030,50],[1031,50],[1032,50],[1033,50],[1034,50],[1035,50],[1036,50],[1037,50],[1038,50],[1042,50],[1044,50],[1043,50],[1045,50],[1039,50],[1040,50],[1041,50],[1046,50],[1047,50],[1048,50],[1049,50],[1050,50],[1052,50],[1051,50],[1053,50],[1054,50],[1055,50],[1056,50],[1057,50],[1058,50],[1059,50],[1060,50],[1061,50],[1062,50],[1063,50],[1064,50],[1065,50],[1066,50],[1067,50],[1068,50],[1069,50],[1071,50],[1070,50],[1072,50],[1074,50],[1073,50],[1075,50],[1076,50],[1077,50],[1078,50],[1079,50],[1080,50],[1081,50],[1082,50],[1083,50],[1085,50],[1084,50],[1086,50],[1087,50],[1088,50],[1089,50],[1090,50],[1091,50],[1092,50],[1093,50],[1094,50],[1095,50],[1096,50],[1097,50],[1098,50],[1099,50],[1100,50],[1102,50],[1101,50],[1105,50],[1106,50],[1107,50],[1108,50],[1109,50],[1110,50],[1111,50],[1112,50],[1113,50],[1114,50],[1115,50],[1116,50],[1117,50],[1118,50],[1119,50],[1120,50],[1121,50],[1122,50],[1123,50],[1124,50],[1125,50],[1126,50],[1127,50],[1128,50],[1129,50],[1103,50],[1130,50],[1104,50],[1131,50],[1132,50],[1133,50],[1134,50],[1135,50],[1136,50],[1137,50],[1138,50],[1139,50],[1140,50],[1141,50],[1142,50],[1143,50],[1144,50],[1145,50],[1146,50],[1147,50],[1148,50],[1149,50],[1150,50],[1151,50],[1152,50],[1153,50],[1154,50],[1155,50],[1156,50],[1157,50],[1158,50],[1159,50],[1160,50],[1161,50],[1162,50],[1163,50],[1164,50],[1165,50],[1166,50],[1167,50],[1168,50],[1169,50],[1176,50],[1177,50],[1170,50],[1178,50],[1171,50],[1172,50],[1173,50],[1174,50],[1175,50],[1179,50],[1180,50],[1184,50],[1181,50],[1185,50],[1182,50],[1183,50],[1186,50],[1187,50],[1188,50],[1189,50],[1190,50],[1191,50],[1192,50],[1193,50],[1194,50],[1195,50],[1196,50],[1197,50],[1198,50],[1199,50],[1200,50],[1201,50],[1202,50],[1203,50],[1204,50],[1206,50],[1205,50],[1207,50],[1208,50],[1209,50],[1210,50],[1211,50],[1214,50],[1212,50],[1213,50],[1215,50],[1216,50],[1217,50],[1218,50],[1219,50],[1220,50],[1221,50],[1222,50],[1223,50],[1224,50],[1225,50],[1226,50],[1227,50],[1228,50],[1230,50],[1229,50],[1232,50],[1233,50],[1231,50],[1235,50],[1234,50],[1236,50],[1238,50],[1237,50],[1239,50],[1240,50],[1241,50],[1242,50],[1243,50],[1244,50],[1245,50],[1246,50],[1247,50],[1248,50],[1249,50],[1250,50],[1251,50],[1252,50],[1254,50],[1255,50],[1253,50],[1256,50],[1257,50],[1258,50],[1259,50],[1260,50],[1261,50],[1262,50],[1263,50],[1264,50],[1265,50],[1266,50],[1267,50],[1268,50],[1269,50],[1270,50],[1271,50],[1272,50],[1273,50],[1274,50],[1275,50],[1276,50],[1277,50],[1278,50],[1279,50],[1280,50],[1281,50],[1282,50],[1283,50],[1284,50],[1285,50],[1286,50],[1287,50],[1288,50],[1289,50],[1290,50],[1291,50],[1292,50],[1293,50],[1294,50],[1295,50],[1296,50],[1297,50],[1298,50],[1299,50],[1301,50],[1302,50],[1303,50],[1304,50],[1305,50],[1309,50],[1306,50],[1307,50],[1308,50],[1300,50],[1310,50],[1311,50],[1312,50],[1313,50],[1314,50],[1315,50],[1316,50],[1317,50],[1318,50],[1319,50],[1320,50],[1321,50],[1322,50],[1323,50],[1324,50],[1325,50],[1326,50],[1327,50],[1328,50],[1329,50],[1330,50],[1331,50],[1332,50],[1333,50],[1334,50],[1335,50],[1336,50],[1337,50],[1338,50],[1339,50],[1340,50],[1341,50],[1342,50],[1343,50],[1348,50],[1344,50],[1345,50],[1346,50],[1347,50],[1349,50],[1350,50],[1351,50],[1352,50],[1353,50],[1354,50],[1355,50],[1356,50],[1357,50],[1358,50],[1359,50],[1360,50],[1361,50],[1362,50],[1363,50],[1364,50],[1365,50],[1366,50],[1367,50],[1368,50],[1369,50],[1370,50],[1371,50],[1372,50],[1373,50],[1375,50],[1376,50],[1377,50],[1378,50],[1374,50],[1380,50],[1379,50],[1381,50],[1382,50],[1383,50],[1384,50],[1385,50],[1386,50],[1387,50],[1388,50],[1389,50],[1390,50],[1391,50],[1396,50],[1392,50],[1393,50],[1394,50],[1395,50],[1397,50],[1399,50],[1400,50],[1401,50],[1398,50],[1402,50],[1403,50],[1404,50],[1405,50],[1406,50],[1407,50],[1408,50],[1409,50],[1410,50],[1411,50],[1412,50],[1413,50],[1414,50],[1415,50],[1416,50],[1417,50],[1418,50],[1419,50],[1420,50],[1421,50],[1433,50],[1422,50],[1423,50],[1424,50],[1425,50],[1426,50],[1427,50],[1428,50],[1429,50],[1430,50],[1431,50],[1432,50],[1434,50],[1435,50],[1436,50],[1437,50],[1438,50],[1439,50],[1440,50],[1441,50],[1442,50],[1443,50],[1444,50],[1445,50],[1446,50],[1447,50],[1448,50],[1451,50],[1449,50],[1450,50],[1452,50],[1453,50],[1454,50],[1455,50],[1456,50],[1457,50],[1458,50],[1460,50],[1459,50],[1461,50],[1462,50],[1463,50],[1464,50],[1465,50],[1466,50],[1467,50],[1468,50],[1469,50],[1470,50],[1471,50],[1481,50],[1472,50],[1473,50],[1474,50],[1475,50],[1476,50],[1477,50],[1478,50],[1479,50],[1480,50],[1482,50],[1483,50],[1484,50],[1485,50],[1486,50],[1487,50],[1488,50],[1489,50],[1490,50],[1491,50],[1492,50],[1493,50],[1494,50],[1495,50],[1496,50],[1497,50],[1498,50],[1499,50],[1500,50],[1501,50],[1502,50],[1503,50],[1504,50],[1505,50],[1506,50],[1507,50],[1508,50],[1509,50],[1510,50],[1511,50],[1512,50],[1513,50],[1514,50],[1515,50],[1518,50],[1519,50],[1520,50],[1516,50],[1517,50],[1521,50],[1522,50],[1523,50],[1524,50],[1525,50],[1526,50],[1527,50],[1528,50],[1529,50],[1530,50],[1531,50],[1532,50],[1533,50],[1534,50],[1535,50],[1536,50],[1537,50],[1538,50],[1539,50],[1540,50],[1541,50],[1542,50],[1543,50],[1544,50],[1545,50],[1546,50],[1547,50],[1548,50],[1549,50],[1550,50],[1551,50],[1552,50],[1553,50],[1554,50],[1557,50],[1555,50],[1556,50],[1573,50],[1574,50],[1558,50],[1559,50],[1560,50],[1561,50],[1562,50],[1563,50],[1564,50],[1565,50],[1566,50],[1575,50],[1577,50],[1567,50],[1576,50],[1568,50],[1569,50],[1570,50],[1571,50],[1572,50],[1578,50],[1579,50],[1580,50],[1581,50],[1582,50],[1583,50],[1584,50],[1585,50],[1586,50],[1587,50],[1592,50],[1588,50],[1589,50],[1590,50],[1591,50],[1593,50],[1594,50],[1595,50],[1596,50],[1597,50],[1598,50],[1599,50],[1600,50],[1601,50],[1602,50],[1603,50],[1604,50],[1605,50],[1606,50],[1607,50],[1608,50],[1609,50],[1610,50],[1611,50],[1612,50],[1613,50],[1614,50],[1615,50],[1616,50],[1617,50],[1618,50],[1620,50],[1619,50],[1621,50],[2339,55],[1622,50],[1623,50],[1624,50],[1625,50],[1628,50],[1626,50],[1627,50],[1629,50],[1630,50],[1631,50],[1632,50],[1633,50],[1634,50],[1635,50],[1636,50],[1638,50],[1639,50],[1637,50],[1640,50],[1641,50],[1642,50],[1643,50],[1644,50],[1645,50],[1646,50],[1647,50],[1648,50],[1649,50],[1650,50],[1651,50],[1652,50],[1653,50],[1654,50],[1655,50],[1656,50],[1657,50],[1658,50],[1659,50],[1660,50],[1661,50],[1663,50],[1662,50],[1664,50],[1665,50],[1666,50],[1667,50],[1668,50],[1669,50],[1674,50],[1670,50],[1671,50],[1672,50],[1673,50],[1675,50],[1676,50],[1677,50],[1678,50],[1679,50],[1680,50],[1681,50],[1682,50],[1683,50],[1684,50],[1685,50],[1686,50],[1694,50],[1687,50],[1688,50],[1689,50],[1690,50],[1691,50],[1692,50],[1693,50],[1695,50],[1696,50],[1697,50],[1698,50],[1699,50],[1700,50],[1701,50],[1702,50],[1704,50],[1703,50],[1705,50],[1706,50],[1708,50],[1707,50],[1709,50],[1710,50],[1711,50],[1712,50],[1713,50],[1714,50],[1715,50],[1716,50],[1717,50],[1718,50],[1719,50],[1720,50],[1721,50],[1722,50],[1723,50],[1724,50],[1725,50],[1726,50],[1727,50],[1728,50],[1729,50],[1730,50],[1731,50],[1732,50],[1733,50],[1734,50],[1735,50],[1736,50],[1737,50],[1738,50],[1739,50],[1740,50],[1741,50],[1742,50],[1743,50],[1745,50],[1744,50],[1746,50],[1747,50],[1748,50],[1749,50],[1750,50],[1751,50],[1752,50],[1753,50],[1754,50],[1755,50],[1756,50],[1757,50],[1758,50],[1759,50],[1760,50],[1762,50],[1761,50],[1763,50],[1764,50],[1765,50],[1766,50],[1767,50],[1768,50],[1769,50],[1770,50],[1771,50],[1772,50],[1777,50],[1779,50],[1778,50],[1781,50],[1782,50],[1780,50],[1773,50],[1783,50],[1784,50],[1774,50],[1775,50],[1776,50],[1785,50],[1786,50],[1787,50],[1788,50],[1789,50],[1790,50],[1791,50],[1792,50],[1793,50],[1794,50],[1795,50],[1796,50],[1797,50],[1798,50],[1799,50],[1800,50],[1801,50],[1802,50],[1803,50],[1804,50],[1805,50],[1806,50],[1807,50],[1808,50],[1809,50],[1810,50],[1811,50],[1812,50],[1813,50],[1814,50],[1815,50],[1816,50],[1817,50],[1818,50],[1819,50],[1820,50],[1821,50],[1822,50],[1823,50],[1824,50],[1825,50],[1826,50],[1827,50],[1828,50],[1829,50],[1830,50],[1831,50],[1832,50],[1833,50],[1834,50],[1835,50],[1839,50],[1840,50],[1836,50],[1837,50],[1838,50],[1841,50],[1842,50],[1843,50],[1844,50],[1845,50],[1846,50],[1847,50],[1848,50],[1849,50],[1850,50],[1851,50],[1852,50],[1853,50],[1856,50],[1857,50],[1858,50],[1859,50],[1860,50],[1861,50],[1862,50],[1863,50],[1854,50],[1855,50],[1864,50],[1865,50],[1866,50],[1867,50],[1869,50],[1868,50],[1871,50],[1870,50],[1872,50],[1873,50],[1874,50],[1875,50],[1876,50],[1877,50],[1878,50],[1879,50],[1880,50],[1881,50],[1882,50],[1883,50],[1884,50],[1885,50],[1886,50],[1887,50],[1888,50],[1889,50],[1890,50],[1891,50],[1892,50],[1893,50],[1894,50],[1895,50],[1896,50],[1897,50],[1898,50],[1899,50],[1900,50],[1902,50],[1901,50],[1903,50],[1904,50],[1905,50],[1906,50],[1907,50],[1908,50],[1909,50],[1910,50],[1911,50],[1912,50],[1914,50],[1913,50],[1915,50],[1916,50],[1917,50],[1918,50],[1919,50],[1920,50],[1921,50],[1922,50],[1923,50],[1924,50],[1925,50],[1926,50],[1927,50],[1928,50],[1929,50],[1930,50],[1931,50],[1932,50],[1933,50],[1934,50],[1935,50],[1936,50],[1937,50],[1938,50],[1939,50],[1940,50],[1941,50],[1942,50],[1943,50],[1944,50],[1945,50],[1946,50],[1947,50],[1948,50],[1949,50],[1950,50],[1951,50],[1952,50],[1954,50],[1953,50],[1955,50],[1956,50],[1957,50],[1958,50],[1959,50],[1960,50],[1961,50],[1962,50],[1963,50],[1964,50],[1965,50],[1966,50],[1967,50],[1968,50],[1969,50],[1970,50],[1971,50],[1972,50],[1973,50],[1974,50],[1975,50],[1976,50],[1977,50],[1978,50],[1979,50],[1980,50],[1981,50],[1982,50],[1983,50],[1984,50],[1985,50],[1986,50],[1987,50],[1988,50],[1989,50],[1990,50],[1991,50],[1992,50],[1993,50],[1994,50],[1995,50],[1996,50],[1997,50],[1998,50],[1999,50],[2000,50],[2001,50],[2004,50],[2002,50],[2003,50],[2005,50],[2006,50],[2007,50],[2008,50],[2009,50],[2010,50],[2011,50],[2012,50],[2013,50],[2014,50],[2015,50],[2016,50],[2017,50],[2018,50],[2019,50],[2020,50],[2021,50],[2022,50],[2023,50],[2024,50],[2025,50],[2026,50],[2027,50],[2028,50],[2029,50],[2030,50],[2032,50],[2031,50],[2033,50],[2034,50],[2035,50],[2036,50],[2039,50],[2037,50],[2038,50],[2040,50],[2041,50],[2042,50],[2043,50],[2044,50],[2045,50],[2046,50],[2047,50],[2048,50],[2049,50],[2050,50],[2051,50],[2052,50],[2053,50],[2056,50],[2054,50],[2055,50],[2057,50],[2058,50],[2059,50],[2060,50],[2061,50],[2062,50],[2063,50],[2064,50],[2065,50],[2066,50],[2067,50],[2068,50],[2069,50],[2070,50],[2071,50],[2072,50],[2073,50],[2074,50],[2075,50],[2076,50],[2077,50],[2078,50],[2079,50],[2080,50],[2081,50],[2082,50],[2083,50],[2084,50],[2085,50],[2086,50],[2087,50],[2088,50],[2089,50],[2091,50],[2090,50],[2092,50],[2093,50],[2094,50],[2095,50],[2096,50],[2097,50],[2098,50],[2099,50],[2100,50],[2101,50],[2104,50],[2102,50],[2103,50],[2105,50],[2106,50],[2107,50],[2108,50],[2109,50],[2110,50],[2111,50],[2112,50],[2114,50],[2113,50],[2115,50],[2116,50],[2117,50],[2118,50],[2119,50],[2120,50],[2121,50],[2122,50],[2123,50],[2124,50],[2125,50],[2126,50],[2127,50],[2128,50],[2129,50],[2130,50],[2131,50],[2132,50],[2133,50],[2134,50],[2135,50],[2136,50],[2137,50],[2138,50],[2139,50],[2140,50],[2141,50],[2143,50],[2142,50],[2144,50],[2145,50],[2146,50],[2147,50],[2148,50],[2149,50],[2150,50],[2151,50],[2153,50],[2154,50],[2155,50],[2156,50],[2157,50],[2158,50],[2159,50],[2160,50],[2161,50],[2162,50],[2163,50],[2164,50],[2165,50],[2166,50],[2167,50],[2168,50],[2170,50],[2171,50],[2172,50],[2173,50],[2174,50],[2169,50],[2175,50],[2193,50],[2176,50],[2177,50],[2178,50],[2179,50],[2180,50],[2181,50],[2182,50],[2183,50],[2184,50],[2185,50],[2186,50],[2187,50],[2188,50],[2189,50],[2190,50],[2191,50],[2192,50],[2194,50],[2195,50],[2196,50],[2197,50],[2198,50],[2199,50],[2200,50],[2201,50],[2202,50],[2203,50],[2204,50],[2205,50],[2206,50],[2208,50],[2207,50],[2209,50],[2210,50],[2211,50],[2212,50],[2213,50],[2214,50],[2215,50],[2216,50],[2217,50],[2218,50],[2219,50],[2220,50],[2221,50],[2222,50],[2223,50],[2224,50],[2225,50],[2226,50],[2227,50],[2228,50],[2229,50],[2230,50],[2231,50],[2232,50],[2235,50],[2233,50],[2234,50],[2236,50],[2237,50],[2238,50],[2239,50],[2240,50],[2241,50],[2242,50],[2243,50],[2244,50],[2245,50],[2246,50],[2247,50],[2248,50],[2249,50],[2250,50],[2152,50],[2251,50],[2252,50],[2253,50],[2254,50],[2255,50],[2256,50],[2257,50],[2258,50],[2259,50],[2260,50],[2261,50],[2262,50],[2263,50],[2264,50],[2265,50],[2266,50],[2267,50],[2268,50],[2269,50],[2270,50],[2271,50],[2272,50],[2273,50],[2274,50],[2275,50],[2279,50],[2280,50],[2276,50],[2277,50],[2281,50],[2278,50],[2282,50],[2283,50],[2284,50],[2285,50],[2286,50],[2287,50],[2288,50],[2289,50],[2290,50],[2291,50],[2292,50],[2293,50],[2294,50],[2295,50],[2296,50],[2297,50],[2298,50],[2299,50],[2300,50],[2301,50],[2302,50],[2303,50],[2304,50],[2305,50],[2306,50],[2311,50],[2312,50],[2313,50],[2307,50],[2308,50],[2309,50],[2310,50],[2314,50],[2315,50],[2316,50],[2317,50],[2318,50],[2319,50],[2320,50],[2321,50],[2322,50],[2323,50],[2324,50],[2325,50],[2326,50],[2327,50],[2328,50],[2329,50],[2330,50],[2331,50],[2332,50],[2333,50],[2334,50],[2335,50],[2336,50],[2337,50],[2338,50],[3912,56],[3958,57],[3911,58],[3899,54],[3956,57],[3957,57],[3962,59],[3960,60],[3961,57],[3900,54],[3901,58],[3937,58],[3902,61],[3959,2],[720,62],[723,63],[730,64],[731,63],[732,65],[727,66],[725,2],[726,67],[708,68],[736,69],[735,70],[706,71],[705,2],[810,2],[3988,72],[557,73],[556,2],[558,74],[555,74],[3989,75],[608,2],[3987,2],[154,76],[155,76],[156,77],[94,78],[157,79],[158,80],[159,81],[92,2],[160,82],[161,83],[162,84],[163,85],[164,86],[165,87],[166,87],[167,88],[168,89],[169,90],[170,91],[95,2],[93,2],[171,92],[172,93],[173,94],[213,95],[174,96],[175,97],[176,96],[177,98],[178,99],[179,100],[180,101],[181,101],[182,101],[183,102],[184,103],[185,104],[186,105],[187,106],[188,107],[189,107],[190,108],[191,2],[192,2],[193,109],[194,110],[195,109],[196,111],[197,112],[198,113],[199,114],[200,115],[201,116],[202,117],[203,118],[204,119],[205,120],[206,121],[207,122],[208,123],[209,124],[210,125],[96,96],[97,2],[98,126],[99,127],[100,2],[101,128],[102,2],[145,129],[146,130],[147,131],[148,131],[149,132],[150,2],[151,79],[152,133],[153,130],[211,134],[212,135],[217,136],[481,54],[218,137],[216,138],[483,139],[482,140],[214,141],[479,2],[215,142],[83,2],[85,143],[478,54],[248,54],[554,2],[808,144],[809,145],[3926,146],[3925,147],[3924,2],[84,2],[3951,148],[747,149],[811,54],[3922,150],[3934,151],[813,152],[3856,54],[734,153],[3932,152],[812,2],[814,154],[3939,54],[752,155],[605,156],[3933,157],[738,158],[737,159],[743,160],[741,161],[744,162],[742,161],[750,161],[703,163],[740,164],[739,161],[751,165],[748,162],[749,161],[746,161],[745,161],[604,166],[3890,167],[3889,168],[815,169],[603,165],[606,18],[607,170],[704,2],[3978,171],[817,172],[816,173],[3979,174],[3981,175],[819,172],[820,176],[609,177],[3914,178],[3897,18],[3898,18],[3904,179],[3918,180],[3915,54],[3945,18],[3940,181],[3941,182],[3907,18],[3908,179],[3953,183],[3913,184],[3927,185],[3936,186],[3963,187],[3938,188],[3903,189],[3919,18],[3917,18],[3943,190],[3944,191],[3965,192],[3952,193],[3955,194],[3954,195],[3942,196],[3931,197],[3935,198],[3964,199],[3930,200],[3928,201],[3929,18],[3906,202],[3905,203],[3966,204],[3921,205],[3923,206],[3973,54],[764,207],[754,208],[755,208],[761,209],[756,210],[757,211],[758,212],[759,208],[760,208],[762,213],[763,214],[719,215],[718,216],[802,217],[801,218],[800,214],[696,2],[562,219],[561,220],[560,221],[695,222],[694,223],[698,224],[697,225],[700,226],[699,227],[587,228],[586,223],[589,229],[588,223],[593,230],[592,223],[591,231],[590,232],[794,233],[768,234],[769,235],[770,235],[771,235],[772,235],[773,235],[774,235],[775,235],[776,235],[777,235],[778,235],[792,236],[779,235],[780,235],[781,235],[782,235],[783,235],[784,235],[785,235],[786,235],[788,235],[789,235],[787,235],[790,235],[791,235],[793,235],[767,237],[585,238],[565,239],[566,239],[567,239],[568,239],[569,239],[570,239],[571,240],[573,239],[572,239],[584,241],[574,239],[576,239],[575,239],[578,239],[577,239],[579,239],[580,239],[581,239],[582,239],[583,239],[564,239],[563,242],[689,243],[687,244],[688,244],[692,245],[690,244],[691,244],[693,244],[559,2],[3920,54],[504,246],[509,1],[516,247],[499,248],[252,2],[260,249],[400,250],[403,251],[375,2],[388,252],[395,253],[277,2],[377,2],[258,2],[374,254],[420,255],[259,2],[250,256],[402,257],[404,258],[405,259],[476,260],[369,261],[322,262],[382,263],[383,264],[381,265],[380,2],[376,266],[401,267],[261,268],[446,2],[447,269],[288,270],[262,271],[289,270],[325,270],[228,270],[398,272],[397,2],[387,273],[494,2],[237,2],[515,274],[454,275],[455,276],[451,277],[533,2],[352,2],[456,18],[452,278],[538,279],[537,280],[532,2],[303,2],[355,281],[354,2],[531,282],[453,54],[308,283],[315,284],[317,285],[307,2],[312,286],[314,287],[316,288],[311,289],[309,2],[313,290],[534,2],[530,2],[536,291],[535,2],[306,292],[525,293],[528,294],[296,295],[295,296],[294,297],[541,54],[293,298],[282,2],[543,2],[544,54],[545,299],[220,2],[384,300],[385,301],[386,302],[224,2],[389,2],[244,303],[219,2],[468,54],[226,304],[467,305],[466,306],[457,2],[458,2],[465,2],[460,2],[463,307],[459,2],[461,308],[464,309],[462,308],[257,2],[254,2],[255,270],[409,2],[414,310],[415,311],[413,312],[411,313],[412,314],[407,2],[474,18],[249,18],[503,315],[510,316],[514,317],[343,318],[342,2],[337,2],[490,319],[498,320],[370,321],[371,322],[449,323],[359,2],[472,324],[347,54],[364,325],[475,326],[360,2],[363,327],[361,2],[473,328],[470,329],[469,2],[471,2],[367,2],[445,330],[232,331],[345,332],[349,333],[365,334],[368,335],[357,336],[350,337],[497,338],[423,339],[341,340],[229,341],[496,342],[225,343],[416,344],[408,2],[417,345],[434,346],[406,2],[433,347],[91,2],[428,348],[253,2],[448,349],[424,2],[238,2],[240,2],[379,2],[432,350],[256,2],[280,351],[366,352],[286,353],[346,2],[431,2],[410,2],[436,354],[437,355],[378,2],[439,356],[441,357],[440,358],[390,2],[430,341],[443,359],[340,360],[429,361],[435,362],[265,2],[269,2],[268,2],[267,2],[272,2],[266,2],[275,2],[274,2],[271,2],[270,2],[273,2],[276,363],[264,2],[332,364],[331,2],[336,365],[333,366],[335,367],[338,365],[334,366],[245,368],[324,369],[493,370],[491,2],[520,371],[522,372],[486,373],[521,374],[233,375],[230,375],[263,2],[247,376],[246,377],[242,378],[243,379],[251,380],[279,380],[290,380],[326,381],[291,381],[235,382],[234,2],[330,383],[329,384],[328,385],[327,386],[236,387],[477,388],[278,389],[485,390],[450,391],[480,392],[484,393],[373,394],[372,395],[353,396],[339,397],[321,398],[323,399],[320,400],[442,401],[344,2],[508,2],[241,402],[444,403],[492,404],[351,2],[281,405],[358,406],[356,407],[283,408],[418,409],[487,2],[284,410],[419,410],[506,2],[505,2],[507,2],[489,2],[488,2],[421,411],[348,2],[318,412],[239,413],[297,2],[223,414],[285,2],[512,54],[222,2],[524,415],[305,54],[518,18],[304,416],[501,417],[302,415],[227,2],[526,418],[300,54],[301,54],[292,2],[221,2],[299,419],[298,420],[287,421],[362,105],[422,105],[438,2],[426,422],[425,2],[310,292],[231,2],[319,54],[495,303],[502,423],[86,54],[89,424],[90,425],[87,54],[88,2],[399,127],[394,426],[393,2],[392,427],[391,2],[500,428],[511,429],[513,430],[517,431],[519,432],[523,433],[551,434],[527,434],[550,435],[529,436],[539,437],[540,438],[542,439],[546,440],[549,303],[548,2],[547,441],[729,442],[728,2],[717,443],[714,444],[715,2],[716,2],[713,445],[807,446],[766,447],[765,448],[702,449],[701,163],[796,450],[795,451],[427,452],[722,453],[733,454],[707,63],[721,455],[724,456],[3968,2],[3970,457],[3969,458],[753,2],[712,459],[710,460],[711,461],[709,2],[3948,462],[3947,2],[3895,2],[600,463],[599,2],[81,2],[82,2],[13,2],[14,2],[16,2],[15,2],[2,2],[17,2],[18,2],[19,2],[20,2],[21,2],[22,2],[23,2],[24,2],[3,2],[25,2],[26,2],[4,2],[27,2],[31,2],[28,2],[29,2],[30,2],[32,2],[33,2],[34,2],[5,2],[35,2],[36,2],[37,2],[38,2],[6,2],[42,2],[39,2],[40,2],[41,2],[43,2],[7,2],[44,2],[49,2],[50,2],[45,2],[46,2],[47,2],[48,2],[8,2],[54,2],[51,2],[52,2],[53,2],[55,2],[9,2],[56,2],[57,2],[58,2],[60,2],[59,2],[61,2],[62,2],[10,2],[63,2],[64,2],[65,2],[11,2],[66,2],[67,2],[68,2],[69,2],[70,2],[1,2],[71,2],[72,2],[12,2],[76,2],[74,2],[79,2],[78,2],[73,2],[77,2],[75,2],[80,2],[121,464],[133,465],[118,466],[134,446],[143,467],[109,468],[110,469],[108,470],[142,441],[137,471],[141,472],[112,473],[130,474],[111,475],[140,476],[106,477],[107,471],[113,478],[114,2],[120,479],[117,478],[104,480],[144,481],[135,482],[124,483],[123,478],[125,484],[128,485],[122,486],[126,487],[138,441],[115,488],[116,489],[129,490],[105,446],[132,491],[131,478],[119,489],[127,492],[136,2],[103,2],[139,493],[602,494],[598,2],[601,495],[595,496],[594,74],[597,497],[596,498],[686,499],[680,500],[684,501],[681,501],[677,500],[685,502],[682,503],[683,501],[678,504],[679,505],[673,506],[617,507],[619,508],[672,2],[618,509],[676,510],[675,511],[674,512],[610,2],[620,507],[621,2],[612,513],[616,514],[611,2],[613,515],[614,516],[615,2],[622,517],[623,517],[624,517],[625,517],[626,517],[627,517],[628,517],[629,517],[630,517],[631,517],[632,517],[633,517],[634,517],[636,517],[635,517],[637,517],[638,517],[639,517],[640,517],[671,518],[641,517],[642,517],[643,517],[644,517],[645,517],[646,517],[647,517],[648,517],[649,517],[650,517],[651,517],[652,517],[653,517],[655,517],[654,517],[656,517],[657,517],[658,517],[659,517],[660,517],[661,517],[662,517],[663,517],[664,517],[665,517],[666,517],[667,517],[670,517],[668,517],[669,517],[818,519]],"affectedFilesPendingEmit":[3986,3984,3980,3982,821,3977,3891,3949,3946,3950,3893,3894,3855,3910,3967,3971,3853,3972,3976,3909,3974,3975,3896,3892,3854,3916,818],"version":"5.9.3"} \ No newline at end of file diff --git a/web/app/api/api.server.ts b/web/app/api/api.server.ts index e05458f62..e9a0dd13a 100644 --- a/web/app/api/api.server.ts +++ b/web/app/api/api.server.ts @@ -10,6 +10,7 @@ import { isPersistentSession, } from '~/utils/session.server' import { API_URL } from '~/lib/constants' +import type { V2Filter } from '~/api/v2/types' // ============================================================================ // MARK: API utils @@ -725,34 +726,6 @@ export async function getGeneralStats(request: Request): Promise { // MARK: Analytics Data API (Deferred Loading) // ============================================================================ -export interface AnalyticsFilter { - column: string - filter: string - isExclusive: boolean - isContains?: boolean -} - -export interface TrafficLogResponse { - params?: Record - chart: { - x: string[] - visits: number[] - uniques: number[] - sdur: number[] - bounces?: number[] - concurrency?: number[] - } - customs: Record - properties: Record - appliedFilters?: AnalyticsFilter[] - timeBucket?: string[] - meta?: { - key: string - current: { sum: number; avg: number } - previous: { sum: number; avg: number } - }[] -} - interface OverallPeriodStats { all: number unique?: number @@ -776,7 +749,7 @@ export interface OverallObject { export interface AnalyticsParams { timeBucket: string period: string - filters: AnalyticsFilter[] + filters: V2Filter[] from?: string to?: string timezone: string @@ -787,41 +760,10 @@ export interface AnalyticsParams { includeConcurrency?: boolean } -function serializeFiltersForUrl(filters: AnalyticsFilter[]): string { +function serializeFiltersForUrl(filters: V2Filter[]): string { return JSON.stringify(filters) } -export async function getProjectDataServer( - request: Request, - pid: string, - params: AnalyticsParams, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('timeBucket', params.timeBucket) - queryParams.append('period', params.period) - queryParams.append('filters', serializeFiltersForUrl(params.filters)) - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - if (params.mode) queryParams.append('mode', params.mode) - if (params.metrics) queryParams.append('metrics', params.metrics) - if (params.includeConcurrency) { - queryParams.append('includeConcurrency', 'true') - } - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log?${queryParams.toString()}`, - { headers }, - ) -} - export async function getOverallStatsServer( request: Request, pids: string[], @@ -854,123 +796,6 @@ export async function getOverallStatsServer( ) } -interface CustomEventsChartResponse { - chart?: { - events?: Record - } -} - -export async function getCustomEventsDataServer( - request: Request, - pid: string, - params: AnalyticsParams, - customEvents: string[], -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('timeBucket', params.timeBucket) - queryParams.append('period', params.period) - queryParams.append('filters', serializeFiltersForUrl(params.filters)) - queryParams.append('customEvents', JSON.stringify(customEvents)) - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/customEvents?${queryParams.toString()}`, - { headers }, - ) -} - -export interface PerformanceDataResponse { - params: Record - chart: { - x: string[] - dns?: number[] - tls?: number[] - conn?: number[] - response?: number[] - render?: number[] - domLoad?: number[] - pageLoad?: number[] - ttfb?: number[] - p50?: number[] - p75?: number[] - p95?: number[] - } - appliedFilters: AnalyticsFilter[] - timeBucket?: string[] -} - -export async function getPerfDataServer( - request: Request, - pid: string, - params: AnalyticsParams & { measure?: string }, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('timeBucket', params.timeBucket) - queryParams.append('period', params.period) - queryParams.append('filters', serializeFiltersForUrl(params.filters)) - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - if (params.measure) queryParams.append('measure', params.measure) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/performance?${queryParams.toString()}`, - { headers }, - ) -} - -export interface PerformanceOverallObject { - current: { frontend: number; backend: number; network: number } - previous: { frontend: number; backend: number; network: number } - frontendChange: number - backendChange: number - networkChange: number -} - -export async function getPerformanceOverallStatsServer( - request: Request, - pids: string[], - params: AnalyticsParams & { measure?: string }, -): Promise>> { - const pidsParam = `[${pids.map((pid) => `"${pid}"`).join(',')}]` - - const queryParams = new URLSearchParams() - queryParams.append('pids', pidsParam) - queryParams.append('period', params.period) - queryParams.append('filters', serializeFiltersForUrl(params.filters)) - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - if (params.measure) queryParams.append('measure', params.measure) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch>( - request, - `log/performance/birdseye?${queryParams.toString()}`, - { headers }, - ) -} - interface Session { psid: string cc: string | null @@ -994,13 +819,6 @@ interface Session { replayExpiresAt?: string | null } -export interface SessionsResponse { - sessions: Session[] - take: number - skip: number - appliedFilters: AnalyticsFilter[] -} - type SessionReplayPrivacy = 'total' | 'normal' | 'none' export interface SessionReplayListItem extends Session { @@ -1019,45 +837,6 @@ export interface SessionReplaysResponse { replays: SessionReplayListItem[] take: number skip: number - appliedFilters: AnalyticsFilter[] -} - -export type SessionEventType = 'traffic' | 'performance' | 'error' - -export async function getSessionsServer( - request: Request, - pid: string, - params: AnalyticsParams & { - take?: number - skip?: number - sessionEvent?: SessionEventType - }, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('period', params.period) - queryParams.append('filters', serializeFiltersForUrl(params.filters)) - queryParams.append('take', String(params.take || 30)) - queryParams.append('skip', String(params.skip || 0)) - if (params.sessionEvent) { - queryParams.append('sessionEvent', params.sessionEvent) - } - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/sessions?${queryParams.toString()}`, - { - headers, - }, - ) } export async function getSessionReplaysServer( @@ -1092,64 +871,12 @@ export async function getSessionReplaysServer( ) } -export interface FunnelSessionsResponse { - sessions: Session[] - take: number - skip: number -} - export interface GoalSessionsResponse { sessions: Session[] take: number skip: number } -export async function getFunnelSessionsServer( - request: Request, - pid: string, - params: { - period: string - from?: string - to?: string - timezone?: string - funnelId?: string - step: number - take?: number - skip?: number - password?: string - filters?: AnalyticsFilter[] - dropoff?: boolean - }, -): Promise> { - const step = Math.max(1, Math.floor(Number(params.step) || 1)) - const take = Math.min(150, Math.max(1, Math.floor(Number(params.take) || 30))) - const skip = Math.max(0, Math.floor(Number(params.skip) || 0)) - - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('period', params.period) - queryParams.append('step', String(step)) - queryParams.append('take', String(take)) - queryParams.append('skip', String(skip)) - queryParams.append('filters', JSON.stringify(params.filters || [])) - if (params.funnelId) queryParams.append('funnelId', params.funnelId) - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - if (params.dropoff) queryParams.append('dropoff', 'true') - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/funnel-sessions?${queryParams.toString()}`, - { headers }, - ) -} - export interface JourneySessionsResponse { sessions: Session[] take: number @@ -1169,7 +896,7 @@ export async function getJourneySessionsServer( take?: number skip?: number password?: string - filters?: AnalyticsFilter[] + filters?: V2Filter[] }, ): Promise> { const step = Math.min(10, Math.max(1, Math.floor(Number(params.step) || 1))) @@ -1229,15 +956,6 @@ export async function getGoalSessionsServer( ) } -interface PageflowItem { - type: 'pageview' | 'event' | 'error' | 'sale' | 'subscription' | 'refund' - value: string - created: string - metadata?: { key: string; value: string }[] - amount?: number - currency?: string -} - export interface SessionReplayMetadata { hasReplay: boolean replayId: string @@ -1274,74 +992,6 @@ export interface SessionReplayExportResponse { error?: string } -export interface SessionDetailsResponse { - details: { - psid: string - cc: string | null - os: string | null - osv: string | null - br: string | null - brv: string | null - dv: string | null - lc: string | null - ref: string | null - so: string | null - me: string | null - ca: string | null - te: string | null - co: string | null - rg: string | null - ct: string | null - pageviews: number - customEvents: number - errors: number - revenue?: number - refunds?: number - created: string - sdur?: number - isLive?: boolean - profileId: string | null - isIdentified: 1 | 0 - isFirstSession: 1 | 0 - } - chart?: { - x: string[] - pageviews?: number[] - customEvents?: number[] - errors?: number[] - } - pages?: PageflowItem[] - timeBucket?: string - replay?: SessionReplayMetadata | null -} - -export async function getSessionServer( - request: Request, - pid: string, - psid: string, - timezone: string, - password?: string, -): Promise> { - const queryParams = new URLSearchParams({ - pid, - psid, - timezone, - }) - - const headers: Record = {} - if (password) { - headers['x-password'] = password - } - - return serverFetch( - request, - `log/session?${queryParams.toString()}`, - { - headers, - }, - ) -} - export async function getSessionReplayServer( request: Request, pid: string, @@ -1405,177 +1055,28 @@ export async function deleteSessionReplayServer( ) } -interface SwetrixError { - eid: string - name: string - message: string - filename: string - count: number - last_seen: string - status: 'active' | 'regressed' | 'fixed' | 'resolved' - users: number - sessions: number -} +// ============================================================================ +// MARK: Feature Flags API (Deferred Loading) +// ============================================================================ -export interface ErrorsResponse { - errors: SwetrixError[] +export interface TargetingRule { + column: string + filter: string + isExclusive: boolean } -export async function getErrorsServer( - request: Request, - pid: string, - params: AnalyticsParams & { - take?: number - skip?: number - options?: Record - }, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('timeBucket', params.timeBucket) - queryParams.append('period', params.period) - queryParams.append('filters', serializeFiltersForUrl(params.filters)) - queryParams.append('options', JSON.stringify(params.options || {})) - queryParams.append('take', String(params.take || 30)) - queryParams.append('skip', String(params.skip || 0)) - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/errors?${queryParams.toString()}`, - { headers }, - ) +export interface FeatureFlagSchedule { + enabled?: boolean + rolloutPercentage?: number + applyAt: string } -export interface ErrorDetailsResponse { - details: { - eid: string - name: string - message: string - filename: string - lineno: number - colno: number - count: number - last_seen: string - first_seen: string - status: 'active' | 'regressed' | 'fixed' | 'resolved' - users: number - sessions: number - stackTrace?: string - } - chart?: { - x: string[] - occurrences: number[] - affectedUsers: number[] - } - params?: Record - metadata?: { key: string; value: string; count: number }[] - timeBucket?: string -} - -export async function getErrorServer( - request: Request, - pid: string, - eid: string, - params: AnalyticsParams & { options?: Record }, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('eid', eid) - queryParams.append('timeBucket', params.timeBucket) - queryParams.append('period', params.period) - queryParams.append('filters', serializeFiltersForUrl(params.filters)) - queryParams.append('options', JSON.stringify(params.options || {})) - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/get-error?${queryParams.toString()}`, - { - headers, - }, - ) -} - -export interface ErrorOverviewResponse { - chart?: { - x: string[] - occurrences: number[] - affectedUsers: number[] - } - stats?: { - totalErrors: number - uniqueErrors: number - affectedUsers: number - errorRate?: number - affectedSessions?: number - } - appliedFilters?: AnalyticsFilter[] -} - -export async function getErrorOverviewServer( - request: Request, - pid: string, - params: AnalyticsParams & { options?: Record }, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('timeBucket', params.timeBucket) - queryParams.append('period', params.period) - queryParams.append('filters', serializeFiltersForUrl(params.filters)) - queryParams.append('options', JSON.stringify(params.options || {})) - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/error-overview?${queryParams.toString()}`, - { headers }, - ) -} - -// ============================================================================ -// MARK: Feature Flags API (Deferred Loading) -// ============================================================================ - -export interface TargetingRule { - column: string - filter: string - isExclusive: boolean -} - -export interface FeatureFlagSchedule { - enabled?: boolean - rolloutPercentage?: number - applyAt: string -} - -export type FeatureFlagStatus = - | 'enabled' - | 'disabled' - | 'scheduled' - | 'killed' - | 'stale' +export type FeatureFlagStatus = + | 'enabled' + | 'disabled' + | 'scheduled' + | 'killed' + | 'stale' export type FeatureFlagStaleReason = | 'not_evaluated_recently' @@ -1809,7 +1310,7 @@ export async function getGoalStatsServer( from = '', to = '', timezone?: string, - filters: AnalyticsFilter[] = [], + filters: V2Filter[] = [], ): Promise> { const params = new URLSearchParams({ period }) if (from) params.append('from', from) @@ -1837,7 +1338,7 @@ export async function getGoalChartServer( to = '', timeBucket = 'day', timezone?: string, - filters: AnalyticsFilter[] = [], + filters: V2Filter[] = [], ): Promise> { const params = new URLSearchParams({ period, timeBucket }) if (from) params.append('from', from) @@ -1851,131 +1352,6 @@ export async function getGoalChartServer( ) } -// ============================================================================ -// MARK: Funnels API (Deferred Loading) -// ============================================================================ - -export interface FunnelDataResponse { - funnel: { - value: string - events: number - eventsPerc: number - eventsPercStep: number - dropoff: number - dropoffPerc: number - dropoffPercStep: number - topCountries: Record - topSources: Record - breakdowns?: ConversionBreakdowns - }[] - totalPageviews: number - timeToConvert?: ConversionTimeToConvert -} - -export async function getFunnelDataServer( - request: Request, - pid: string, - funnelId: string, - period: string, - from: string, - to: string, - timezone: string, - password?: string, - filters: AnalyticsFilter[] = [], -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('period', period) - queryParams.append('funnelId', funnelId) - queryParams.append('filters', JSON.stringify(filters)) - if (from) queryParams.append('from', from) - if (to) queryParams.append('to', to) - if (timezone) queryParams.append('timezone', timezone) - - const headers: Record = {} - if (password) { - headers['x-password'] = password - } - - return serverFetch( - request, - `log/funnel?${queryParams.toString()}`, - { - headers, - }, - ) -} - -// ============================================================================ -// MARK: Captcha API (Deferred Loading) -// ============================================================================ - -interface CaptchaPanelData { - name: string - count: number - cc?: string -} - -interface CaptchaChartData { - x: string[] - results?: number[] - generated?: number[] - passed?: number[] - failed?: number[] - validationFailed?: number[] - replayed?: number[] -} - -interface CaptchaSummaryData { - generated: number - passed: number - passRate: number | null - solveP50: number - solveP75: number - solveP95: number - difficulty: CaptchaPanelData[] - solveTime: CaptchaPanelData[] -} - -export interface CaptchaDataResponse { - params: Record - chart?: CaptchaChartData - summary?: CaptchaSummaryData - previousSummary?: CaptchaSummaryData | null -} - -export async function getCaptchaDataServer( - request: Request, - pid: string, - timeBucket = 'hour', - period = '3d', - filters: AnalyticsFilter[] = [], - from = '', - to = '', - password?: string, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('timeBucket', timeBucket) - queryParams.append('period', period) - queryParams.append('filters', JSON.stringify(filters)) - if (from) queryParams.append('from', from) - if (to) queryParams.append('to', to) - - const headers: Record = {} - if (password) { - headers['x-password'] = password - } - - return serverFetch( - request, - `log/captcha?${queryParams.toString()}`, - { - headers, - }, - ) -} - // ============================================================================ // MARK: Experiments API (Deferred Loading) // ============================================================================ @@ -2074,7 +1450,7 @@ export async function getExperimentResultsServer( from = '', to = '', timezone?: string, - filters: AnalyticsFilter[] = [], + filters: V2Filter[] = [], ): Promise> { const params = new URLSearchParams({ period, timeBucket }) if (from) params.append('from', from) @@ -2091,384 +1467,61 @@ export async function getExperimentResultsServer( export async function getGoalServer( request: Request, goalId: string, -): Promise> { - return serverFetch(request, `goal/${goalId}`) -} - -// ============================================================================ -// MARK: Profiles API (Deferred Loading) -// ============================================================================ - -export interface ApiProfile { - profileId: string - isIdentified: boolean - sessionsCount: number - pageviewsCount: number - eventsCount: number - errorsCount: number - firstSeen: string - lastSeen: string - cc: string | null - os: string | null - br: string | null - dv: string | null -} - -export interface ProfilesResponse { - profiles: ApiProfile[] -} - -export async function getProfilesServer( - request: Request, - pid: string, - period = '7d', - filters: AnalyticsFilter[] = [], - from = '', - to = '', - take = 30, - skip = 0, - timezone = '', - profileType: 'all' | 'anonymous' | 'identified' = 'all', - password?: string, -): Promise> { - const params = new URLSearchParams() - params.append('pid', pid) - params.append('take', String(take)) - params.append('skip', String(skip)) - params.append('period', period) - params.append('filters', JSON.stringify(filters)) - params.append('profileType', profileType) - if (from) params.append('from', from) - if (to) params.append('to', to) - if (timezone) params.append('timezone', timezone) - - const headers: Record = {} - if (password) { - headers['x-password'] = password - } - - return serverFetch( - request, - `log/profiles?${params.toString()}`, - { headers }, - ) -} - -interface ProfileDetails { - profileId: string - isIdentified: boolean - sessionsCount: number - pageviewsCount: number - eventsCount: number - errorsCount: number - firstSeen: string - lastSeen: string - avgDuration: number - cc: string | null - rg: string | null - ct: string | null - os: string | null - osv: string | null - br: string | null - brv: string | null - dv: string | null - lc: string | null - totalRevenue?: number - revenueCurrency?: string - topPages?: { page: string; count: number }[] - activityCalendar?: { date: string; pageviews: number; events: number }[] -} - -export type ProfileDetailsResponse = ProfileDetails - -export async function getProfileServer( - request: Request, - pid: string, - profileId: string, - timezone = '', - password?: string, -): Promise> { - const params = new URLSearchParams() - params.append('pid', pid) - params.append('profileId', profileId) - if (timezone) params.append('timezone', timezone) - - const headers: Record = {} - if (password) { - headers['x-password'] = password - } - - return serverFetch( - request, - `log/profile?${params.toString()}`, - { - headers, - }, - ) -} - -interface ProfileSession { - psid: string - cc: string | null - os: string | null - br: string | null - dv: string | null - pageviews: number - customEvents: number - errors: number - revenue?: number - refunds?: number - created: string - sessionStart?: string - lastActivity?: string - sdur?: number - duration?: number - active?: boolean - isLive?: 1 | 0 | boolean - pages?: PageflowItem[] -} - -export interface ProfileSessionsResponse { - sessions: ProfileSession[] -} - -export async function getProfileSessionsServer( - request: Request, - pid: string, - profileId: string, - period = 'all', - filters: AnalyticsFilter[] = [], - from = '', - to = '', - take = 30, - skip = 0, - timezone = '', - password?: string, -): Promise> { - const params = new URLSearchParams() - params.append('pid', pid) - params.append('profileId', profileId) - params.append('take', String(take)) - params.append('skip', String(skip)) - params.append('period', period) - params.append('filters', JSON.stringify(filters)) - if (from) params.append('from', from) - if (to) params.append('to', to) - if (timezone) params.append('timezone', timezone) - - const headers: Record = {} - if (password) { - headers['x-password'] = password - } - - return serverFetch( - request, - `log/profile/sessions?${params.toString()}`, - { headers }, - ) -} - -// ============================================================================ -// MARK: Projects API -// ============================================================================ - -export interface Project { - id: string - name: string - origins: string[] | null - ipBlacklist: string[] | null - active: boolean - public: boolean - isPasswordProtected: boolean - passwordHash: string | null - admin: { - id: string - email: string - } - created: string - isOwner: boolean - isLocked: boolean - featureAccess?: ProjectFeatureAccess - isDataExists?: boolean - isErrorDataExists?: boolean - isCaptchaDataExists?: boolean - isReplayDataExists?: boolean - isCaptchaProject: boolean - isCaptchaEnabled: boolean - isAnalyticsProject: boolean - captchaSecretKey: string | null - role: string - botsProtectionLevel: string - isTransferring: boolean - share?: { - id: string - confirmed: boolean - role: string - } - funnels?: { id: string; name: string }[] -} - -export async function getProjectServer( - request: Request, - pid: string, - password?: string, -): Promise> { - const headers: Record = {} - if (password) { - headers['x-password'] = password - } - - return serverFetch(request, `project/${pid}`, { headers }) -} - -// ============================================================================ -// MARK: Custom Events Metadata API -// ============================================================================ - -export interface CustomEventsMetadataResponse { - result: Array<{ key: string; value: string; count: number }> -} - -export async function getCustomEventsMetadataServer( - request: Request, - pid: string, - event: string, - params: { - timeBucket?: string - period?: string - from?: string - to?: string - timezone?: string - password?: string - } = {}, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('event', event) - queryParams.append('timeBucket', params.timeBucket || 'hour') - queryParams.append('period', params.period || '1d') - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/meta?${queryParams.toString()}`, - { - headers, - }, - ) -} - -// ============================================================================ -// MARK: Property Metadata API -// ============================================================================ - -export interface PropertyMetadataResponse { - result: Array<{ key: string; value: string; count: number }> -} - -export async function getPropertyMetadataServer( - request: Request, - pid: string, - property: string, - params: { - timeBucket?: string - period?: string - from?: string - to?: string - filters?: AnalyticsFilter[] - timezone?: string - password?: string - } = {}, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('property', property) - queryParams.append('timeBucket', params.timeBucket || 'hour') - queryParams.append('period', params.period || '1d') - queryParams.append('filters', JSON.stringify(params.filters || [])) - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/property?${queryParams.toString()}`, - { - headers, - }, - ) +): Promise> { + return serverFetch(request, `goal/${goalId}`) } // ============================================================================ -// MARK: Error Sessions API +// MARK: Projects API // ============================================================================ -export interface ErrorAffectedSession { - psid: string - profileId: string | null - cc: string | null - br: string | null - os: string | null - firstErrorAt: string - lastErrorAt: string - errorCount: number -} - -export interface ErrorSessionsResponse { - sessions: ErrorAffectedSession[] - total: number +export interface Project { + id: string + name: string + origins: string[] | null + ipBlacklist: string[] | null + active: boolean + public: boolean + isPasswordProtected: boolean + passwordHash: string | null + admin: { + id: string + email: string + } + created: string + isOwner: boolean + isLocked: boolean + featureAccess?: ProjectFeatureAccess + isDataExists?: boolean + isErrorDataExists?: boolean + isCaptchaDataExists?: boolean + isReplayDataExists?: boolean + isCaptchaProject: boolean + isCaptchaEnabled: boolean + isAnalyticsProject: boolean + captchaSecretKey: string | null + role: string + botsProtectionLevel: string + isTransferring: boolean + share?: { + id: string + confirmed: boolean + role: string + } + funnels?: { id: string; name: string }[] } -export async function getErrorSessionsServer( +export async function getProjectServer( request: Request, pid: string, - eid: string, - params: { - timeBucket?: string - period?: string - from?: string - to?: string - filters?: AnalyticsFilter[] - timezone?: string - take?: number - skip?: number - password?: string - } = {}, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('eid', eid) - queryParams.append('timeBucket', params.timeBucket || 'hour') - queryParams.append('period', params.period || '7d') - queryParams.append('take', String(params.take || 10)) - queryParams.append('skip', String(params.skip || 0)) - queryParams.append('filters', serializeFiltersForUrl(params.filters || [])) - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - + password?: string, +): Promise> { const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password + if (password) { + headers['x-password'] = password } - return serverFetch( - request, - `log/error-sessions?${queryParams.toString()}`, - { headers }, - ) + return serverFetch(request, `project/${pid}`, { headers }) } // ============================================================================ @@ -2496,31 +1549,6 @@ export async function getLiveVisitorsServer( }) } -export interface LiveVisitorInfo { - psid: string - dv: string - br: string - os: string - cc: string -} - -export async function getLiveVisitorsInfoServer( - request: Request, - pid: string, - password?: string, -): Promise> { - const headers: Record = {} - if (password) { - headers['x-password'] = password - } - - return serverFetch( - request, - `log/live-visitors?pid=${pid}`, - { headers }, - ) -} - // ============================================================================ // MARK: Bot Protection Stats API // ============================================================================ @@ -2554,55 +1582,6 @@ export async function getBotProtectionStatsServer( ) } -// ============================================================================ -// MARK: Custom Events Chart API -// ============================================================================ - -export interface ProjectDataCustomEventsResponse { - chart?: { - events?: Record - } - customs?: Record -} - -export async function getProjectDataCustomEventsServer( - request: Request, - pid: string, - params: { - timeBucket?: string - period?: string - filters?: AnalyticsFilter[] - from?: string - to?: string - timezone?: string - customEvents?: string[] - password?: string - } = {}, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('timeBucket', params.timeBucket || 'hour') - queryParams.append('period', params.period || '3d') - queryParams.append('filters', JSON.stringify(params.filters || [])) - queryParams.append('customEvents', JSON.stringify(params.customEvents || [])) - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/custom-events?${queryParams.toString()}`, - { - headers, - }, - ) -} - // ============================================================================ // MARK: SSO API // ============================================================================ @@ -2729,7 +1708,6 @@ interface Journey { export interface JourneysResponse { journeys: Journey[] totalSessions: number - appliedFilters?: AnalyticsFilter[] } export async function getJourneysServer( @@ -2737,7 +1715,7 @@ export async function getJourneysServer( pid: string, params: { period?: string - filters?: AnalyticsFilter[] + filters?: V2Filter[] from?: string to?: string timezone?: string @@ -2808,7 +1786,7 @@ export async function getDataDeletionPreviewServer( request: Request, pid: string, options: { - filters?: AnalyticsFilter[] + filters?: V2Filter[] from?: string to?: string }, @@ -2828,50 +1806,6 @@ export async function getDataDeletionPreviewServer( ) } -export async function getErrorsFiltersServer( - request: Request, - pid: string, - type: string, - password?: string, -): Promise> { - const headers: Record = {} - if (password) { - headers['x-password'] = password - } - - return serverFetch( - request, - `log/errors-filters?pid=${pid}&type=${type}`, - { headers }, - ) -} - -export interface VersionFilter { - name: string - version: string -} - -export async function getVersionFiltersServer( - request: Request, - pid: string, - type: 'traffic' | 'errors', - column: 'br' | 'os', - password?: string, -): Promise> { - const headers: Record = {} - if (password) { - headers['x-password'] = password - } - - return serverFetch( - request, - `log/filters/versions?pid=${pid}&type=${type}&column=${column}`, - { - headers, - }, - ) -} - // ============================================================================ // MARK: Google Search Console API // ============================================================================ @@ -2947,196 +1881,6 @@ export async function getGSCKeywordsServer( ) } -interface GSCDateSeriesEntry { - date: string - clicks: number - impressions: number - ctr: number - position: number -} - -interface GSCTopPageEntry { - page: string - clicks: number - impressions: number - ctr: number - position: number -} - -interface GSCTopQueryEntry { - name: string - count: number - impressions: number - position: number - ctr: number -} - -interface GSCTopCountryEntry { - country: string - clicks: number - impressions: number - ctr: number - position: number -} - -interface GSCTopDeviceEntry { - device: string - clicks: number - impressions: number - ctr: number - position: number -} - -type GSCImpressionPositionBucketKey = - | 'pos1To3' - | 'pos4To10' - | 'pos11To20' - | 'pos21Plus' - -type GSCOrganicPositionBucketKey = - | 'pos1To3' - | 'pos4To10' - | 'pos11To20' - | 'pos21To50' - | 'pos51Plus' - -interface GSCImpressionsByPositionEntry { - key: GSCImpressionPositionBucketKey - label: string - impressions: number - percentage: number -} - -type GSCOrganicPositionEntry = { date: string } & Record< - GSCOrganicPositionBucketKey, - number -> - -interface GSCBrandedTraffic { - branded: number - nonBranded: number - skipped?: false -} - -interface GSCSkippedBrandedTraffic { - skipped: true - branded: null - nonBranded: null -} - -export interface GSCDashboardResponse { - notConnected?: boolean - noProperty?: boolean - summary?: { - clicks: number - impressions: number - ctr: number - position: number - } - previousSummary?: { - clicks: number - impressions: number - ctr: number - position: number - } | null - dateSeries?: GSCDateSeriesEntry[] - topPages?: GSCTopPageEntry[] - topQueries?: GSCTopQueryEntry[] - topCountries?: GSCTopCountryEntry[] - topDevices?: GSCTopDeviceEntry[] - brandedTraffic?: GSCBrandedTraffic | GSCSkippedBrandedTraffic - positionAnalyticsSkipped?: boolean - impressionsByPosition?: GSCImpressionsByPositionEntry[] | null - organicPositions?: GSCOrganicPositionEntry[] | null -} - -export async function getGSCDashboardServer( - request: Request, - pid: string, - params: { - period?: string - from?: string - to?: string - timezone?: string - password?: string - timeBucket?: string - filters?: AnalyticsFilter[] - } = {}, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('period', params.period || '7d') - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - if (params.timeBucket) queryParams.append('timeBucket', params.timeBucket) - if (params.filters) - queryParams.append('filters', serializeFiltersForUrl(params.filters)) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/gsc-dashboard?${queryParams.toString()}`, - { - headers, - timeoutMs: 60000, - }, - ) -} - -export interface GSCDetailsResponse { - type: 'queries' | 'pages' | 'none' - data: Array<{ - name?: string - page?: string - count?: number - clicks?: number - impressions: number - ctr: number - position: number - }> -} - -export async function getGSCDetailsServer( - request: Request, - pid: string, - params: { - period?: string - from?: string - to?: string - timezone?: string - password?: string - page?: string - query?: string - } = {}, -): Promise> { - const queryParams = new URLSearchParams() - queryParams.append('pid', pid) - queryParams.append('period', params.period || '7d') - if (params.from) queryParams.append('from', params.from) - if (params.to) queryParams.append('to', params.to) - if (params.timezone) queryParams.append('timezone', params.timezone) - if (params.page) queryParams.append('page', params.page) - if (params.query) queryParams.append('query', params.query) - - const headers: Record = {} - if (params.password) { - headers['x-password'] = params.password - } - - return serverFetch( - request, - `log/gsc-details?${queryParams.toString()}`, - { - headers, - }, - ) -} - // ============================================================================ // MARK: Revenue API // ============================================================================ diff --git a/web/app/api/v2/client.ts b/web/app/api/v2/client.ts new file mode 100644 index 000000000..bd8af4b1b --- /dev/null +++ b/web/app/api/v2/client.ts @@ -0,0 +1,78 @@ +import { V2Envelope } from './types' + +class V2ApiError extends Error { + status: number + + constructor(status: number, message: string) { + super(message) + this.name = 'V2ApiError' + this.status = status + } +} + +export type V2QueryParams = Record< + string, + string | number | boolean | string[] | object | null | undefined +> + +const serializeParams = (params: V2QueryParams): string => { + const searchParams = new URLSearchParams() + + for (const [key, value] of Object.entries(params)) { + if (value === undefined || value === null) { + continue + } + + if (Array.isArray(value)) { + if (value.length === 0) { + continue + } + + if (typeof value[0] === 'object') { + searchParams.set(key, JSON.stringify(value)) + } else { + searchParams.set(key, value.join(',')) + } + continue + } + + if (typeof value === 'object') { + searchParams.set(key, JSON.stringify(value)) + continue + } + + searchParams.set(key, String(value)) + } + + const qs = searchParams.toString() + return qs ? `?${qs}` : '' +} + +export async function fetchV2( + path: string, + params: V2QueryParams = {}, + signal?: AbortSignal, +): Promise> { + const response = await fetch(`/api/v2/${path}${serializeParams(params)}`, { + signal, + }) + + let body: any = null + try { + body = await response.json() + } catch { + // ignore + } + + if (!response.ok) { + const message = + (Array.isArray(body?.message) + ? body.message.join('; ') + : body?.message) || + body?.error || + `Request failed with status ${response.status}` + throw new V2ApiError(response.status, message) + } + + return body as V2Envelope +} diff --git a/web/app/api/v2/endpoints.ts b/web/app/api/v2/endpoints.ts new file mode 100644 index 000000000..d78521323 --- /dev/null +++ b/web/app/api/v2/endpoints.ts @@ -0,0 +1,302 @@ +import { fetchV2, V2QueryParams } from './client' +import { + BreakdownRow, + CaptchaSummaryData, + CustomEventRow, + DimensionValues, + ErrorListItem, + LiveVisitorsData, + MetadataRow, + MetricSums, + PagePropertyRow, + PerformanceSummaryData, + SeoBrandedTrafficData, + SeoPositionsData, + SeoStatusData, + SeoSummaryData, + TimeseriesRow, + TrafficSummaryData, + V2CommonParams, + V2DataType, +} from './types' + +type Common = V2CommonParams & V2QueryParams + +type SummaryData = T extends 'traffic' + ? TrafficSummaryData + : T extends 'performance' + ? PerformanceSummaryData + : T extends 'captcha' + ? CaptchaSummaryData + : T extends 'seo' + ? SeoSummaryData + : Record + +export const getSummary = ( + pid: string, + dataType: T, + params: Common & { measure?: string }, + signal?: AbortSignal, +) => + fetchV2>(`projects/${pid}/${dataType}/summary`, params, signal) + +export const getTimeseries = ( + pid: string, + dataType: V2DataType, + params: Common & { + metrics?: string[] + timeBucket?: string + mode?: 'periodical' | 'cumulative' + measure?: string + }, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/${dataType}/timeseries`, + params, + signal, + ) + +export const getBreakdown = ( + pid: string, + dataType: V2DataType, + params: Common & { + dimension: string + metrics?: string[] + limit?: number + offset?: number + sort?: string + measure?: string + }, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/${dataType}/breakdown`, + params, + signal, + ) + +export const getSeoStatus = (pid: string, signal?: AbortSignal) => + fetchV2(`projects/${pid}/seo/status`, {}, signal) + +// Both of the below return `data: null` with `meta.skipped` when the range is +// too wide or Search Console did not answer within the deadline. +export const getSeoBrandedTraffic = ( + pid: string, + params: Common, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/seo/branded-traffic`, + params, + signal, + ) + +export const getSeoPositions = ( + pid: string, + params: Common, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/seo/positions`, + params, + signal, + ) + +export const getCustomEvents = ( + pid: string, + params: Common & { limit?: number; offset?: number }, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/traffic/custom-events`, + params, + signal, + ) + +export const getCustomEventsTimeseries = ( + pid: string, + params: Common & { events: string[]; timeBucket?: string }, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/traffic/custom-events/timeseries`, + params, + signal, + ) + +export const getCustomEventsMetadata = ( + pid: string, + params: Common & { event: string }, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/traffic/custom-events/metadata`, + params, + signal, + ) + +export const getCustomMetrics = ( + pid: string, + params: Common & { metrics: string }, + signal?: AbortSignal, +) => + fetchV2<{ key: string; current: MetricSums; previous: MetricSums }[]>( + `projects/${pid}/traffic/custom-metrics`, + params, + signal, + ) + +export const getPageProperties = ( + pid: string, + params: Common & { limit?: number; offset?: number }, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/traffic/page-properties`, + params, + signal, + ) + +export const getPagePropertiesMetadata = ( + pid: string, + params: Common & { property: string }, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/traffic/page-properties/metadata`, + params, + signal, + ) + +export const getErrorsList = ( + pid: string, + params: Common & { limit?: number; offset?: number; show_resolved?: boolean }, + signal?: AbortSignal, +) => fetchV2(`projects/${pid}/errors`, params, signal) + +export const getErrorsOverview = ( + pid: string, + params: Common & { timeBucket?: string }, + signal?: AbortSignal, +) => + fetchV2>( + `projects/${pid}/errors/overview`, + params, + signal, + ) + +export const getErrorDetails = ( + pid: string, + eid: string, + params: Common & { timeBucket?: string }, + signal?: AbortSignal, +) => + fetchV2>( + `projects/${pid}/errors/${encodeURIComponent(eid)}`, + params, + signal, + ) + +export const getErrorSessions = ( + pid: string, + eid: string, + params: Common & { limit?: number; offset?: number }, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/errors/${encodeURIComponent(eid)}/sessions`, + params, + signal, + ) + +export const getSessionsList = ( + pid: string, + params: Common & { + limit?: number + offset?: number + event_type?: 'traffic' | 'performance' | 'error' + }, + signal?: AbortSignal, +) => fetchV2(`projects/${pid}/sessions`, params, signal) + +export const getSessionDetails = ( + pid: string, + psid: string, + params: { timezone?: string } & V2QueryParams, + signal?: AbortSignal, +) => + fetchV2>( + `projects/${pid}/sessions/${encodeURIComponent(psid)}`, + params, + signal, + ) + +export const getProfilesList = ( + pid: string, + params: Common & { + limit?: number + offset?: number + profile_type?: 'all' | 'anonymous' | 'identified' + search?: string + }, + signal?: AbortSignal, +) => fetchV2(`projects/${pid}/profiles`, params, signal) + +export const getProfileDetails = ( + pid: string, + profileId: string, + params: { timezone?: string } & V2QueryParams, + signal?: AbortSignal, +) => + fetchV2>( + `projects/${pid}/profiles/${encodeURIComponent(profileId)}`, + params, + signal, + ) + +export const getProfileSessions = ( + pid: string, + profileId: string, + params: Common & { limit?: number; offset?: number }, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/profiles/${encodeURIComponent(profileId)}/sessions`, + params, + signal, + ) + +export const getFunnel = ( + pid: string, + params: Common & { funnelId?: string; steps?: string[] }, + signal?: AbortSignal, +) => fetchV2>(`projects/${pid}/funnel`, params, signal) + +export const getFunnelSessions = ( + pid: string, + params: Common & { + funnelId?: string + steps?: string[] + step: number + dropoff?: boolean + limit?: number + offset?: number + }, + signal?: AbortSignal, +) => fetchV2(`projects/${pid}/funnel/sessions`, params, signal) + +export const getLiveVisitors = (pid: string, signal?: AbortSignal) => + fetchV2(`projects/${pid}/live-visitors`, {}, signal) + +export const getDimensionValues = ( + pid: string, + dimension: string, + params: { type?: V2DataType } & V2QueryParams, + signal?: AbortSignal, +) => + fetchV2( + `projects/${pid}/dimensions/${encodeURIComponent(dimension)}/values`, + params, + signal, + ) diff --git a/web/app/api/v2/types.ts b/web/app/api/v2/types.ts new file mode 100644 index 000000000..f39aad2ef --- /dev/null +++ b/web/app/api/v2/types.ts @@ -0,0 +1,201 @@ +export type V2DataType = + | 'traffic' + | 'performance' + | 'errors' + | 'captcha' + | 'seo' + +export type V2FilterOperator = 'is' | 'is_not' | 'contains' | 'contains_not' + +export interface V2Filter { + dimension: string + operator: V2FilterOperator + value: string | null | (string | null)[] + key?: string +} + +interface V2Meta { + pid: string + period?: string | null + from?: string + to?: string + timezone?: string + appliedFilters?: V2Filter[] + total?: number + limit?: number + offset?: number + sort?: string + dimension?: string + metrics?: string[] + timeBucket?: string + allowedTimeBuckets?: string[] | null + mode?: string + windowMinutes?: number + [key: string]: unknown +} + +export interface V2Envelope { + data: T + meta: V2Meta +} + +export type V2CommonParams = { + period?: string + from?: string + to?: string + timezone?: string + filters?: V2Filter[] +} + +export interface BreakdownRow { + value: string | null + [metricOrExtraField: string]: unknown +} + +export interface TimeseriesRow { + timestamp: string + [metric: string]: number | string +} + +interface TrafficSummaryStats { + visitors: number + pageviews: number + users: number + bounce_rate: number + session_duration: number +} + +export interface TrafficSummaryData { + current: TrafficSummaryStats + previous: TrafficSummaryStats + change: TrafficSummaryStats +} + +interface PerformanceSummaryStats { + frontend: number + network: number + backend: number +} + +export interface PerformanceSummaryData { + current: PerformanceSummaryStats + previous: PerformanceSummaryStats + change: PerformanceSummaryStats +} + +interface CaptchaSummaryStats { + generated: number + passed: number + passRate: number | null + solveP50: number | null + solveP75: number | null + solveP95: number | null + difficulty: { value: string; count: number }[] + solveTime: { value: string; count: number }[] +} + +export interface CaptchaSummaryData { + current: CaptchaSummaryStats + previous: CaptchaSummaryStats | null +} + +interface SeoMetricValues { + clicks: number + impressions: number + ctr: number + position: number +} + +export interface SeoSummaryData { + current: SeoMetricValues + previous: SeoMetricValues + change: SeoMetricValues +} + +export interface SeoStatusData { + connected: boolean + property: string | null +} + +export interface SeoBrandedTrafficData { + branded: number + nonBranded: number +} + +type SeoImpressionPositionBucketKey = + | 'pos1To3' + | 'pos4To10' + | 'pos11To20' + | 'pos21Plus' + +type SeoOrganicPositionBucketKey = + | 'pos1To3' + | 'pos4To10' + | 'pos11To20' + | 'pos21To50' + | 'pos51Plus' + +interface SeoImpressionsByPositionEntry { + key: SeoImpressionPositionBucketKey + label: string + impressions: number + percentage: number +} + +type SeoOrganicPositionEntry = { date: string } & Record< + SeoOrganicPositionBucketKey, + number +> + +export interface SeoPositionsData { + impressionsByPosition: SeoImpressionsByPositionEntry[] + organicPositions: SeoOrganicPositionEntry[] +} + +export interface CustomEventRow { + event: string + count: number +} + +export interface MetricSums { + sum: number + avg: number +} + +export interface PagePropertyRow { + property: string + count: number +} + +export interface MetadataRow { + key: string + value: string + count: number +} + +export interface ErrorListItem { + eid: string + name: string + message: string | null + filename: string | null + count: number + last_seen: string + users: number + sessions: number + status: 'active' | 'regressed' | 'resolved' +} + +export interface LiveVisitor { + psid: string + device: string | null + browser: string | null + os: string | null + country: string | null +} + +export interface LiveVisitorsData { + count: number + visitors: LiveVisitor[] +} + +export type DimensionValues = string[] | { name: string; version: string }[] diff --git a/web/app/hooks/useAnalyticsProxy.ts b/web/app/hooks/useAnalyticsProxy.ts index 65ad2c0e7..dd0b01f25 100644 --- a/web/app/hooks/useAnalyticsProxy.ts +++ b/web/app/hooks/useAnalyticsProxy.ts @@ -1,43 +1,30 @@ -import { useCallback, useRef, useState } from 'react' +import { useCallback, useState } from 'react' import type { - SessionsResponse, SessionReplaysResponse, SessionReplayResponse, DeleteSessionReplayResponse, SessionReplayExportResponse, - ErrorsResponse, FeatureFlagStats, FeatureFlagProfilesResponse, GoalStats, GoalChartData, - CaptchaDataResponse, ExperimentResults, Experiment, Goal, GoalsResponse, FeatureFlagsResponse, - ProfilesResponse, - ProfileDetailsResponse, - ProfileSessionsResponse, - VersionFilter, - CustomEventsMetadataResponse, - PropertyMetadataResponse, - ErrorSessionsResponse, LiveStats, - LiveVisitorInfo, BotProtectionStats, BotProtectionPeriod, - ProjectDataCustomEventsResponse, JourneysResponse, - GSCDashboardResponse, RevenueStatus, RevenueDataResponse, OverallObject, - AnalyticsFilter, DataDeletionPreview, AnalyticsParams as ServerAnalyticsParams, } from '~/api/api.server' +import type { V2Filter } from '~/api/v2/types' type ClientAnalyticsParams = Partial< Omit @@ -78,36 +65,6 @@ async function postAnalytics(payload: unknown): Promise> { return result } -export function useSessionsProxy() { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(false) - - const fetchSessions = useCallback( - async (projectId: string, params: ClientAnalyticsParams) => { - setIsLoading(true) - setError(null) - - try { - const result = await postAnalytics({ - action: 'getSessions', - projectId, - params, - }) - setData(result.data) - setError(result.error) - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error') - } finally { - setIsLoading(false) - } - }, - [], - ) - - return { fetchSessions, data, error, isLoading } -} - export function useSessionReplaysProxy() { const [data, setData] = useState(null) const [error, setError] = useState(null) @@ -282,36 +239,6 @@ export function useSessionReplayExportProxy() { } } -export function useErrorsProxy() { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(false) - - const fetchErrors = useCallback( - async (projectId: string, params: ClientAnalyticsParams) => { - setIsLoading(true) - setError(null) - - try { - const result = await postAnalytics({ - action: 'getErrors', - projectId, - params, - }) - setData(result.data) - setError(result.error) - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error') - } finally { - setIsLoading(false) - } - }, - [], - ) - - return { fetchErrors, data, error, isLoading } -} - export function useFeatureFlagStatsProxy() { const [data, setData] = useState(null) const [error, setError] = useState(null) @@ -440,38 +367,6 @@ export function useGoalChartProxy() { return { fetchChart, data, error, isLoading } } -export function useCaptchaProxy() { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(false) - - const fetchCaptcha = useCallback( - async (projectId: string, params: ClientAnalyticsParams) => { - setIsLoading(true) - setError(null) - - try { - const result = await postAnalytics({ - action: 'getCaptchaData', - projectId, - params, - }) - setData(result.data) - setError(result.error) - return result.data - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error') - return null - } finally { - setIsLoading(false) - } - }, - [], - ) - - return { fetchCaptcha, data, error, isLoading } -} - export function useExperimentResultsProxy() { const [data, setData] = useState(null) const [error, setError] = useState(null) @@ -626,117 +521,6 @@ export function useProjectFeatureFlagsProxy() { return { fetchFeatureFlags, data, error, isLoading } } -export function useProfilesProxy() { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(false) - - const fetchProfiles = useCallback( - async ( - projectId: string, - params: ClientAnalyticsParams & { - profileType?: 'all' | 'anonymous' | 'identified' - } = {}, - ) => { - setIsLoading(true) - setError(null) - - try { - const result = await postAnalytics({ - action: 'getProfiles', - projectId, - params, - }) - setData(result.data) - setError(result.error) - return result.data - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error') - return null - } finally { - setIsLoading(false) - } - }, - [], - ) - - return { fetchProfiles, data, error, isLoading } -} - -export function useProfileProxy() { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(false) - - const fetchProfile = useCallback( - async ( - projectId: string, - profileId: string, - params: ClientAnalyticsParams = {}, - ) => { - setIsLoading(true) - setError(null) - - try { - const result = await postAnalytics({ - action: 'getProfile', - projectId, - profileId, - params, - }) - setData(result.data) - setError(result.error) - return result.data - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error') - return null - } finally { - setIsLoading(false) - } - }, - [], - ) - - return { fetchProfile, data, error, isLoading } -} - -export function useProfileSessionsProxy() { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(false) - - const fetchProfileSessions = useCallback( - async ( - projectId: string, - profileId: string, - params: ClientAnalyticsParams = {}, - ) => { - setIsLoading(true) - setError(null) - - try { - const result = await postAnalytics({ - action: 'getProfileSessions', - projectId, - profileId, - params, - }) - setData(result.data) - setError(result.error) - return result.data - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error') - return null - } finally { - setIsLoading(false) - } - }, - [], - ) - - return { fetchProfileSessions, data, error, isLoading } -} - export function useFiltersProxy() { const fetchFilters = useCallback( async (projectId: string, filterType: string): Promise => { @@ -756,48 +540,7 @@ export function useFiltersProxy() { [], ) - const fetchErrorsFilters = useCallback( - async (projectId: string, filterType: string): Promise => { - try { - const result = await postAnalytics({ - action: 'getErrorsFilters', - projectId, - filterType, - params: {}, - }) - return result.data - } catch (err) { - console.error('[useFiltersProxy] Error:', err) - throw err - } - }, - [], - ) - - const fetchVersionFilters = useCallback( - async ( - projectId: string, - dataType: 'traffic' | 'errors', - filterColumn: 'br' | 'os', - ): Promise => { - try { - const result = await postAnalytics({ - action: 'getVersionFilters', - projectId, - dataType, - filterColumn, - params: {}, - }) - return result.data - } catch (err) { - console.error('[useFiltersProxy] Error:', err) - throw err - } - }, - [], - ) - - return { fetchFilters, fetchErrorsFilters, fetchVersionFilters } + return { fetchFilters } } export function useDataDeletionPreviewProxy() { @@ -805,7 +548,7 @@ export function useDataDeletionPreviewProxy() { async ( projectId: string, options: { - filters?: AnalyticsFilter[] + filters?: V2Filter[] from?: string to?: string }, @@ -827,95 +570,6 @@ export function useDataDeletionPreviewProxy() { return { fetchPreview } } -export function useCustomEventsMetadataProxy() { - const fetchMetadata = useCallback( - async ( - projectId: string, - event: string, - params: ClientAnalyticsParams = {}, - ): Promise => { - try { - const result = await postAnalytics({ - action: 'getCustomEventsMetadata', - projectId, - event, - params, - }) - return result.data - } catch (err) { - console.error('[useCustomEventsMetadataProxy] Error:', err) - throw err - } - }, - [], - ) - - return { fetchMetadata } -} - -export function usePropertyMetadataProxy() { - const fetchMetadata = useCallback( - async ( - projectId: string, - property: string, - params: ClientAnalyticsParams = {}, - ): Promise => { - try { - const result = await postAnalytics({ - action: 'getPropertyMetadata', - projectId, - property, - params, - }) - return result.data - } catch (err) { - console.error('[usePropertyMetadataProxy] Error:', err) - throw err - } - }, - [], - ) - - return { fetchMetadata } -} - -export function useErrorSessionsProxy() { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(false) - - const fetchErrorSessions = useCallback( - async ( - projectId: string, - errorId: string, - params: ClientAnalyticsParams = {}, - ) => { - setIsLoading(true) - setError(null) - - try { - const result = await postAnalytics({ - action: 'getErrorSessions', - projectId, - errorId, - params, - }) - setData(result.data) - setError(result.error) - return result.data - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error') - return null - } finally { - setIsLoading(false) - } - }, - [], - ) - - return { fetchErrorSessions, data, error, isLoading } -} - export function useLiveVisitorsProxy() { const fetchLiveVisitors = useCallback( async (pids: string[]): Promise => { @@ -938,24 +592,7 @@ export function useLiveVisitorsProxy() { [], ) - const fetchLiveVisitorsInfo = useCallback( - async (projectId: string): Promise => { - try { - const result = await postAnalytics({ - action: 'getLiveVisitorsInfo', - projectId, - params: {}, - }) - return result.data - } catch (err) { - console.error('[useLiveVisitorsProxy] Error:', err) - throw err - } - }, - [], - ) - - return { fetchLiveVisitors, fetchLiveVisitorsInfo } + return { fetchLiveVisitors } } export function useBotProtectionStatsProxy() { @@ -982,32 +619,6 @@ export function useBotProtectionStatsProxy() { return { fetchBotProtectionStats } } -export function useProjectDataCustomEventsProxy() { - const fetchCustomEvents = useCallback( - async ( - projectId: string, - customEvents: string[], - params: ClientAnalyticsParams = {}, - ): Promise => { - try { - const result = await postAnalytics({ - action: 'getProjectDataCustomEvents', - projectId, - customEvents, - params, - }) - return result.data - } catch (err) { - console.error('[useProjectDataCustomEventsProxy] Error:', err) - throw err - } - }, - [], - ) - - return { fetchCustomEvents } -} - export function useJourneysProxy() { const [data, setData] = useState(null) const [error, setError] = useState(null) @@ -1046,59 +657,6 @@ export function useJourneysProxy() { return { fetchJourneys, data, error, isLoading } } -export function useGSCDashboardProxy() { - const [data, setData] = useState(null) - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(false) - const requestIdRef = useRef(0) - - const fetchDashboard = useCallback( - async (projectId: string, params: ClientAnalyticsParams = {}) => { - const requestId = requestIdRef.current + 1 - requestIdRef.current = requestId - setIsLoading(true) - setError(null) - - try { - const result = await postAnalytics({ - action: 'getGSCDashboard', - projectId, - params, - }) - - if (requestId !== requestIdRef.current) { - return result.data - } - - if (result.data) { - setData(result.data) - } - setError(result.error) - return result.data - } catch (err) { - if (requestId === requestIdRef.current) { - setError(err instanceof Error ? err.message : 'Unknown error') - } - return null - } finally { - if (requestId === requestIdRef.current) { - setIsLoading(false) - } - } - }, - [], - ) - - const resetData = useCallback(() => { - requestIdRef.current += 1 - setData(null) - setError(null) - setIsLoading(false) - }, []) - - return { fetchDashboard, data, error, isLoading, resetData } -} - export function useRevenueProxy() { const [statusData, setStatusData] = useState(null) const [revenueData, setRevenueData] = useState( diff --git a/web/app/hooks/useInViewOnce.ts b/web/app/hooks/useInViewOnce.ts new file mode 100644 index 000000000..bba14ce09 --- /dev/null +++ b/web/app/hooks/useInViewOnce.ts @@ -0,0 +1,44 @@ +import { useEffect, useRef, useState } from 'react' + +interface UseInViewOnceOptions { + rootMargin?: string + disabled?: boolean +} + +export const useInViewOnce = ({ + rootMargin = '400px 0px', + disabled, +}: UseInViewOnceOptions = {}) => { + const ref = useRef(null) + const [hasBeenInView, setHasBeenInView] = useState(false) + + useEffect(() => { + if (disabled || hasBeenInView) { + if (disabled) setHasBeenInView(true) + return + } + + const element = ref.current + + if (!element || typeof IntersectionObserver === 'undefined') { + setHasBeenInView(true) + return + } + + const observer = new IntersectionObserver( + (entries) => { + if (entries.some((entry) => entry.isIntersecting)) { + setHasBeenInView(true) + observer.disconnect() + } + }, + { rootMargin }, + ) + + observer.observe(element) + + return () => observer.disconnect() + }, [disabled, hasBeenInView, rootMargin]) + + return { ref, hasBeenInView } +} diff --git a/web/app/hooks/v2/useV2Queries.ts b/web/app/hooks/v2/useV2Queries.ts new file mode 100644 index 000000000..a1580dbdb --- /dev/null +++ b/web/app/hooks/v2/useV2Queries.ts @@ -0,0 +1,542 @@ +import { + keepPreviousData, + useInfiniteQuery, + useQuery, +} from '@tanstack/react-query' +import { useMemo } from 'react' +import { useSearchParams } from 'react-router' + +import * as v2 from '~/api/v2/endpoints' +import { V2CommonParams, V2DataType, V2Filter } from '~/api/v2/types' +import { VALID_DIMENSIONS_BY_TYPE } from '~/lib/v2Dimensions' +import { useViewProjectContext } from '~/pages/Project/View/ViewProject' +import { useCurrentProject } from '~/providers/CurrentProjectProvider' + +export const useV2CommonParams = (dataType: V2DataType) => { + const { id: pid } = useCurrentProject() + const { period, timezone, filters } = useViewProjectContext() + const [searchParams] = useSearchParams() + + const from = searchParams.get('from') || undefined + const to = searchParams.get('to') || undefined + + return useMemo(() => { + const validDimensions = VALID_DIMENSIONS_BY_TYPE[dataType] + const applicableFilters = filters.filter((filter) => + validDimensions.includes(filter.dimension), + ) + + const isCustomRange = period === 'custom' && from && to + + const common: V2CommonParams = { + ...(isCustomRange ? { from, to } : { period }), + timezone, + filters: applicableFilters, + } + + return { pid, common } + }, [pid, dataType, period, from, to, timezone, filters]) +} + +const useCompareParams = () => { + const { isActiveCompare } = useViewProjectContext() + const [searchParams] = useSearchParams() + + const compareFrom = searchParams.get('compareFrom') + const compareTo = searchParams.get('compareTo') + + if (!isActiveCompare || !compareFrom || !compareTo) { + return null + } + + return { from: compareFrom, to: compareTo } +} + +const compareOverride = ( + common: V2CommonParams, + compare: { from: string; to: string }, +): V2CommonParams => { + const { period: _period, ...rest } = common + return { ...rest, from: compare.from, to: compare.to } +} + +export const useSummaryQuery = ( + dataType: T, + opts: { measure?: string; enabled?: boolean } = {}, +) => { + const { pid, common } = useV2CommonParams(dataType) + const params = { ...common, measure: opts.measure } + + return useQuery({ + queryKey: ['v2', pid, dataType, 'summary', params], + queryFn: ({ signal }) => v2.getSummary(pid, dataType, params, signal), + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +export const useCompareSummaryQuery = ( + dataType: T, + opts: { measure?: string; enabled?: boolean } = {}, +) => { + const { pid, common } = useV2CommonParams(dataType) + const compare = useCompareParams() + const params = compare + ? { ...compareOverride(common, compare), measure: opts.measure } + : null + + return useQuery({ + queryKey: ['v2', pid, dataType, 'summary', params], + queryFn: ({ signal }) => v2.getSummary(pid, dataType, params!, signal), + placeholderData: keepPreviousData, + enabled: Boolean(params) && opts.enabled !== false, + }) +} + +interface TimeseriesOpts { + metrics?: string[] + mode?: 'periodical' | 'cumulative' + measure?: string + enabled?: boolean + // Overrides the bucket from the URL, for tabs whose data source supports a + // narrower set of buckets than the project view offers. + timeBucket?: string +} + +export const useTimeseriesQuery = ( + dataType: V2DataType, + opts: TimeseriesOpts = {}, +) => { + const { pid, common } = useV2CommonParams(dataType) + const { timeBucket } = useViewProjectContext() + const params = { + ...common, + timeBucket: opts.timeBucket || timeBucket || undefined, + metrics: opts.metrics, + mode: opts.mode, + measure: opts.measure, + } + + return useQuery({ + queryKey: ['v2', pid, dataType, 'timeseries', params], + queryFn: ({ signal }) => v2.getTimeseries(pid, dataType, params, signal), + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +export const useCompareTimeseriesQuery = ( + dataType: V2DataType, + opts: TimeseriesOpts = {}, +) => { + const { pid, common } = useV2CommonParams(dataType) + const { timeBucket } = useViewProjectContext() + const compare = useCompareParams() + const params = compare + ? { + ...compareOverride(common, compare), + timeBucket: opts.timeBucket || timeBucket || undefined, + metrics: opts.metrics, + mode: opts.mode, + measure: opts.measure, + } + : null + + return useQuery({ + queryKey: ['v2', pid, dataType, 'timeseries', params], + queryFn: ({ signal }) => v2.getTimeseries(pid, dataType, params!, signal), + placeholderData: keepPreviousData, + enabled: Boolean(params) && opts.enabled !== false, + }) +} + +interface BreakdownOpts { + dimension: string + metrics?: string[] + limit?: number + offset?: number + sort?: string + measure?: string + extraFilters?: V2Filter[] + enabled?: boolean +} + +export const useBreakdownQuery = ( + dataType: V2DataType, + opts: BreakdownOpts, +) => { + const { pid, common } = useV2CommonParams(dataType) + const params = { + ...common, + filters: opts.extraFilters + ? [...(common.filters || []), ...opts.extraFilters] + : common.filters, + dimension: opts.dimension, + metrics: opts.metrics, + limit: opts.limit, + offset: opts.offset, + sort: opts.sort, + measure: opts.measure, + } + + return useQuery({ + queryKey: ['v2', pid, dataType, 'breakdown', params], + queryFn: ({ signal }) => v2.getBreakdown(pid, dataType, params, signal), + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +const DETAILS_PAGE_SIZE = 100 + +export const useBreakdownDetailsQuery = ( + dataType: V2DataType, + opts: Omit, +) => { + const { pid, common } = useV2CommonParams(dataType) + const params = { + ...common, + filters: opts.extraFilters + ? [...(common.filters || []), ...opts.extraFilters] + : common.filters, + dimension: opts.dimension, + metrics: opts.metrics, + sort: opts.sort, + measure: opts.measure, + } + + return useInfiniteQuery({ + queryKey: ['v2', pid, dataType, 'breakdown-details', params], + queryFn: ({ signal, pageParam }) => + v2.getBreakdown( + pid, + dataType, + { ...params, limit: DETAILS_PAGE_SIZE, offset: pageParam }, + signal, + ), + initialPageParam: 0, + getNextPageParam: (lastPage, pages) => { + // An empty page cannot advance the offset, so honouring `total` here + // would hand back the offset we just fetched and loop forever. + if (lastPage.data.length === 0) { + return undefined + } + + const loaded = pages.reduce((acc, page) => acc + page.data.length, 0) + const total = lastPage.meta.total + + // Search Console reports no row count, so fall back to treating a full + // page as a hint that another one may follow. + if (total === undefined || total === null) { + return lastPage.data.length === DETAILS_PAGE_SIZE ? loaded : undefined + } + + return loaded < total ? loaded : undefined + }, + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +export const useSeoStatusQuery = () => { + const { id: pid } = useCurrentProject() + + return useQuery({ + queryKey: ['v2', pid, 'seo', 'status'], + queryFn: ({ signal }) => v2.getSeoStatus(pid, signal), + placeholderData: keepPreviousData, + }) +} + +export const useSeoBrandedTrafficQuery = (opts: { enabled?: boolean } = {}) => { + const { pid, common } = useV2CommonParams('seo') + + return useQuery({ + queryKey: ['v2', pid, 'seo', 'branded-traffic', common], + queryFn: ({ signal }) => v2.getSeoBrandedTraffic(pid, common, signal), + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +/** + * Impressions-by-position and organic positions come out of a single Search + * Console rollup, so both panels share one query rather than paying for it + * twice. + */ +export const useSeoPositionsQuery = (opts: { enabled?: boolean } = {}) => { + const { pid, common } = useV2CommonParams('seo') + + return useQuery({ + queryKey: ['v2', pid, 'seo', 'positions', common], + queryFn: ({ signal }) => v2.getSeoPositions(pid, common, signal), + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +export const useCustomEventsQuery = ( + opts: { limit?: number; enabled?: boolean } = {}, +) => { + const { pid, common } = useV2CommonParams('traffic') + const params = { ...common, limit: opts.limit } + + return useQuery({ + queryKey: ['v2', pid, 'traffic', 'custom-events', params], + queryFn: ({ signal }) => v2.getCustomEvents(pid, params, signal), + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +export const useCustomEventsTimeseriesQuery = ( + events: string[], + opts: { enabled?: boolean } = {}, +) => { + const { pid, common } = useV2CommonParams('traffic') + const { timeBucket } = useViewProjectContext() + const params = { ...common, timeBucket: timeBucket || undefined, events } + + return useQuery({ + queryKey: ['v2', pid, 'traffic', 'custom-events-timeseries', params], + queryFn: ({ signal }) => v2.getCustomEventsTimeseries(pid, params, signal), + placeholderData: keepPreviousData, + enabled: events.length > 0 && opts.enabled !== false, + }) +} + +export const useCustomMetricsQuery = ( + customMetrics: unknown[], + opts: { enabled?: boolean } = {}, +) => { + const { pid, common } = useV2CommonParams('traffic') + const metrics = customMetrics.length ? JSON.stringify(customMetrics) : null + const params = { ...common, metrics: metrics! } + + return useQuery({ + queryKey: ['v2', pid, 'traffic', 'custom-metrics', params], + queryFn: ({ signal }) => v2.getCustomMetrics(pid, params, signal), + placeholderData: keepPreviousData, + enabled: Boolean(metrics) && opts.enabled !== false, + }) +} + +export const usePagePropertiesQuery = ( + opts: { limit?: number; enabled?: boolean } = {}, +) => { + const { pid, common } = useV2CommonParams('traffic') + const params = { ...common, limit: opts.limit } + + return useQuery({ + queryKey: ['v2', pid, 'traffic', 'page-properties', params], + queryFn: ({ signal }) => v2.getPageProperties(pid, params, signal), + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +const LIST_PAGE_SIZE = 30 + +const listNextPageParam = (pageSize: number) => { + return (lastPage: { data: unknown[] }, pages: { data: unknown[] }[]) => + lastPage.data.length === pageSize + ? pages.reduce((acc, page) => acc + page.data.length, 0) + : undefined +} + +export const useErrorsListQuery = ( + opts: { showResolved?: boolean; enabled?: boolean } = {}, +) => { + const { pid, common } = useV2CommonParams('errors') + const params = { ...common, show_resolved: opts.showResolved } + + return useInfiniteQuery({ + queryKey: ['v2', pid, 'errors', 'list', params], + queryFn: ({ signal, pageParam }) => + v2.getErrorsList( + pid, + { ...params, limit: LIST_PAGE_SIZE, offset: pageParam }, + signal, + ), + initialPageParam: 0, + getNextPageParam: listNextPageParam(LIST_PAGE_SIZE), + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +export const useErrorsOverviewQuery = (opts: { enabled?: boolean } = {}) => { + const { pid, common } = useV2CommonParams('errors') + const { timeBucket } = useViewProjectContext() + const params = { ...common, timeBucket: timeBucket || undefined } + + return useQuery({ + queryKey: ['v2', pid, 'errors', 'overview', params], + queryFn: ({ signal }) => v2.getErrorsOverview(pid, params, signal), + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +export const useErrorDetailsQuery = ( + eid: string | null, + opts: { enabled?: boolean } = {}, +) => { + const { pid, common } = useV2CommonParams('errors') + const { timeBucket } = useViewProjectContext() + const params = { ...common, timeBucket: timeBucket || undefined } + + return useQuery({ + queryKey: ['v2', pid, 'errors', 'details', eid, params], + queryFn: ({ signal }) => v2.getErrorDetails(pid, eid!, params, signal), + placeholderData: keepPreviousData, + enabled: Boolean(eid) && opts.enabled !== false, + }) +} + +const ERROR_SESSIONS_PAGE_SIZE = 10 + +export const useErrorSessionsQuery = ( + eid: string | null, + opts: { enabled?: boolean } = {}, +) => { + const { pid, common } = useV2CommonParams('errors') + + return useInfiniteQuery({ + queryKey: ['v2', pid, 'errors', 'sessions', eid, common], + queryFn: ({ signal, pageParam }) => + v2.getErrorSessions( + pid, + eid!, + { ...common, limit: ERROR_SESSIONS_PAGE_SIZE, offset: pageParam }, + signal, + ), + initialPageParam: 0, + getNextPageParam: listNextPageParam(ERROR_SESSIONS_PAGE_SIZE), + placeholderData: keepPreviousData, + enabled: Boolean(eid) && opts.enabled !== false, + }) +} + +export const useSessionsListQuery = ( + opts: { + eventType?: 'traffic' | 'performance' | 'error' + enabled?: boolean + } = {}, +) => { + const { pid, common } = useV2CommonParams('traffic') + const params = { ...common, event_type: opts.eventType } + + return useInfiniteQuery({ + queryKey: ['v2', pid, 'sessions', 'list', params], + queryFn: ({ signal, pageParam }) => + v2.getSessionsList( + pid, + { ...params, limit: LIST_PAGE_SIZE, offset: pageParam }, + signal, + ), + initialPageParam: 0, + getNextPageParam: listNextPageParam(LIST_PAGE_SIZE), + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +export const useSessionDetailsQuery = ( + psid: string | null, + opts: { enabled?: boolean } = {}, +) => { + const { id: pid } = useCurrentProject() + const { timezone } = useViewProjectContext() + const params = { timezone } + + return useQuery({ + queryKey: ['v2', pid, 'sessions', 'details', psid, params], + queryFn: ({ signal }) => v2.getSessionDetails(pid, psid!, params, signal), + placeholderData: keepPreviousData, + enabled: Boolean(psid) && opts.enabled !== false, + }) +} + +export const useProfilesListQuery = ( + opts: { + profileType?: 'all' | 'anonymous' | 'identified' + search?: string + enabled?: boolean + } = {}, +) => { + const { pid, common } = useV2CommonParams('traffic') + const params = { + ...common, + profile_type: opts.profileType, + search: opts.search || undefined, + } + + return useInfiniteQuery({ + queryKey: ['v2', pid, 'profiles', 'list', params], + queryFn: ({ signal, pageParam }) => + v2.getProfilesList( + pid, + { ...params, limit: LIST_PAGE_SIZE, offset: pageParam }, + signal, + ), + initialPageParam: 0, + getNextPageParam: listNextPageParam(LIST_PAGE_SIZE), + placeholderData: keepPreviousData, + enabled: opts.enabled, + }) +} + +export const useProfileDetailsQuery = ( + profileId: string | null, + opts: { enabled?: boolean } = {}, +) => { + const { id: pid } = useCurrentProject() + const { timezone } = useViewProjectContext() + const params = { timezone } + + return useQuery({ + queryKey: ['v2', pid, 'profiles', 'details', profileId, params], + queryFn: ({ signal }) => + v2.getProfileDetails(pid, profileId!, params, signal), + placeholderData: keepPreviousData, + enabled: Boolean(profileId) && opts.enabled !== false, + }) +} + +export const useProfileSessionsQuery = ( + profileId: string | null, + opts: { enabled?: boolean } = {}, +) => { + const { pid, common } = useV2CommonParams('traffic') + + return useInfiniteQuery({ + queryKey: ['v2', pid, 'profiles', 'sessions', profileId, common], + queryFn: ({ signal, pageParam }) => + v2.getProfileSessions( + pid, + profileId!, + { ...common, limit: LIST_PAGE_SIZE, offset: pageParam }, + signal, + ), + initialPageParam: 0, + getNextPageParam: listNextPageParam(LIST_PAGE_SIZE), + placeholderData: keepPreviousData, + enabled: Boolean(profileId) && opts.enabled !== false, + }) +} + +export const useFunnelQuery = ( + funnelId: string | null, + opts: { enabled?: boolean } = {}, +) => { + const { pid, common } = useV2CommonParams('traffic') + const params = { ...common, funnelId: funnelId! } + + return useQuery({ + queryKey: ['v2', pid, 'funnel', params], + queryFn: ({ signal }) => v2.getFunnel(pid, params, signal), + placeholderData: keepPreviousData, + enabled: Boolean(funnelId) && opts.enabled !== false, + }) +} diff --git a/web/app/lib/constants/index.ts b/web/app/lib/constants/index.ts index 6fac94218..61c7d68ab 100644 --- a/web/app/lib/constants/index.ts +++ b/web/app/lib/constants/index.ts @@ -225,58 +225,6 @@ export const PERIOD_PAIRS_COMPARE = { DISABLE: 'disable', } as const -// the order of panels in the project view -export const TRAFFIC_PANELS_ORDER = [ - 'location', - 'pg', - 'devices', - 'traffic-sources', - 'network', -] -export const PERFORMANCE_PANELS_ORDER = ['location', 'pg', 'devices', 'network'] -export const ERROR_PANELS_ORDER = ['location', 'pg', 'devices', 'network'] -export const FILTERS_PANELS_ORDER = [ - 'pg', - 'entryPage', - 'exitPage', - 'host', - 'cc', - 'rg', - 'ct', - 'lc', - 'dv', - 'br', - 'brv', - 'os', - 'osv', - 'ref', - 'so', - 'me', - 'ca', - 'te', - 'co', - 'isp', - 'og', - 'ut', - 'ctp', -] -export const ERRORS_FILTERS_PANELS_ORDER = [ - 'cc', - 'rg', - 'ct', - 'pg', - 'br', - 'brv', - 'os', - 'osv', - 'lc', - 'dv', - 'isp', - 'og', - 'ut', - 'ctp', -] - // the maximum amount of months user can go back when picking a date in flat picker (project view) export const MAX_MONTHS_IN_PAST = 24 diff --git a/web/app/lib/models/Entry.ts b/web/app/lib/models/Entry.ts index c8c6250ac..571fdfea9 100644 --- a/web/app/lib/models/Entry.ts +++ b/web/app/lib/models/Entry.ts @@ -4,8 +4,3 @@ export interface Entry { cc?: string rgc?: string } - -export interface CountryEntry extends Entry { - cc: string - rgc?: string -} diff --git a/web/app/lib/models/Project.ts b/web/app/lib/models/Project.ts index c2e2acf2d..88af6d8d9 100644 --- a/web/app/lib/models/Project.ts +++ b/web/app/lib/models/Project.ts @@ -73,18 +73,6 @@ export interface Annotation { created: string } -export interface SwetrixError { - eid: string - name: string - message: string - filename: string - count: number - last_seen: string - status: 'active' | 'regressed' | 'fixed' | 'resolved' - users: number - sessions: number -} - type SessionReplayPrivacy = 'total' | 'normal' | 'none' export interface SessionReplayMetadata { @@ -99,17 +87,16 @@ export interface SessionReplayMetadata { export interface Session { psid: string - cc: string | null + country: string | null os: string | null - br: string | null + browser: string | null pageviews: number customEvents: number errors: number revenue?: number refunds?: number - created: string isLive: 1 | 0 - sdur?: number + duration?: number | null sessionStart: string lastActivity: string @@ -123,24 +110,24 @@ export interface Session { } export interface SessionDetails { - cc: string | null + country: string | null os: string | null - osv: string | null - br: string | null - brv: string | null - lc: string | null - ref: string | null - so: string | null - me: string | null - ca: string | null - te: string | null - co: string | null - rg: string | null - ct: string | null - dv: string | null + os_version: string | null + browser: string | null + browser_version: string | null + locale: string | null + referrer: string | null + utm_source: string | null + utm_medium: string | null + utm_campaign: string | null + utm_term: string | null + utm_content: string | null + region: string | null + city: string | null + device: string | null profileId: string | null - sdur?: number + duration?: number isLive?: boolean revenue?: number refunds?: number @@ -155,19 +142,19 @@ export interface Profile { errorsCount: number firstSeen: string lastSeen: string - cc: string | null + country: string | null os: string | null - br: string | null - dv: string | null + browser: string | null + device: string | null } export interface ProfileDetails extends Profile { avgDuration: number - rg: string | null - ct: string | null - lc: string | null - osv: string | null - brv: string | null + region: string | null + city: string | null + locale: string | null + os_version: string | null + browser_version: string | null topPages: { page: string; count: number }[] activityCalendar: { date: string; pageviews: number; events: number }[] totalRevenue?: number diff --git a/web/app/lib/v2Dimensions.ts b/web/app/lib/v2Dimensions.ts new file mode 100644 index 000000000..797759589 --- /dev/null +++ b/web/app/lib/v2Dimensions.ts @@ -0,0 +1,139 @@ +import { V2DataType } from '~/api/v2/types' + +export const V1_TO_V2_DIMENSION: Record = { + cc: 'country', + rg: 'region', + ct: 'city', + pg: 'page', + host: 'host', + lc: 'locale', + br: 'browser', + brv: 'browser_version', + os: 'os', + osv: 'os_version', + dv: 'device', + ref: 'referrer', + refn: 'referrer_name', + so: 'utm_source', + me: 'utm_medium', + ca: 'utm_campaign', + te: 'utm_term', + co: 'utm_content', + isp: 'isp', + og: 'organization', + ut: 'user_type', + ctp: 'connection_type', + entryPage: 'entry_page', + exitPage: 'exit_page', + ev: 'event', + // The SEO tab's keyword filter predates v2, which names the dimension after + // Search Console's own 'query'. + keywords: 'query', +} + +export const KEYED_DIMENSIONS = ['event_metadata', 'page_property'] as const + +export const V1_KEYED_PREFIX_TO_V2: Record = { + 'ev:key:': 'event_metadata', + 'tag:key:': 'page_property', +} + +const TRAFFIC_DIMENSIONS = [ + 'country', + 'region', + 'city', + 'page', + 'host', + 'locale', + 'browser', + 'browser_version', + 'os', + 'os_version', + 'device', + 'referrer', + 'referrer_name', + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_term', + 'utm_content', + 'isp', + 'organization', + 'user_type', + 'connection_type', + 'entry_page', + 'exit_page', + 'event', + 'event_metadata', + 'page_property', +] + +const PERFORMANCE_DIMENSIONS = [ + 'country', + 'region', + 'city', + 'page', + 'host', + 'browser', + 'browser_version', + 'device', + 'isp', + 'organization', + 'user_type', + 'connection_type', +] + +const ERROR_DIMENSIONS = [ + 'country', + 'region', + 'city', + 'page', + 'host', + 'locale', + 'browser', + 'browser_version', + 'os', + 'os_version', + 'device', + 'isp', + 'organization', + 'user_type', + 'connection_type', + 'entry_page', + 'exit_page', + 'error_name', + 'error_message', + 'error_filename', + 'page_property', +] + +const CAPTCHA_DIMENSIONS = [ + 'country', + 'browser', + 'os', + 'device', + 'captcha_event', + 'captcha_difficulty', + 'captcha_reason', + 'solve_time', +] + +const SEO_DIMENSIONS = ['query', 'page', 'country', 'device'] + +export const VALID_DIMENSIONS_BY_TYPE: Record = { + traffic: TRAFFIC_DIMENSIONS, + performance: PERFORMANCE_DIMENSIONS, + errors: ERROR_DIMENSIONS, + captcha: CAPTCHA_DIMENSIONS, + seo: SEO_DIMENSIONS, +} + +export const ALL_VALID_DIMENSIONS = Array.from( + new Set([ + ...TRAFFIC_DIMENSIONS, + ...PERFORMANCE_DIMENSIONS, + ...ERROR_DIMENSIONS, + ...CAPTCHA_DIMENSIONS, + ...SEO_DIMENSIONS, + ]), +) diff --git a/web/app/pages/Project/Settings/components/DeleteDataModal.tsx b/web/app/pages/Project/Settings/components/DeleteDataModal.tsx index 3c34720f6..25b53bc06 100644 --- a/web/app/pages/Project/Settings/components/DeleteDataModal.tsx +++ b/web/app/pages/Project/Settings/components/DeleteDataModal.tsx @@ -23,8 +23,9 @@ import Modal from '~/ui/Modal' import { Text } from '~/ui/Text' import { cn } from '~/utils/generic' +import type { V2Filter } from '~/api/v2/types' + import FilterRowsEditor from '../../View/components/FilterRowsEditor' -import type { Filter } from '../../View/interfaces/traffic' const EMPTY_TN_MAPPING: Record = {} @@ -277,7 +278,7 @@ const DeleteDataModal = ({ pid, isOpen, onClose }: DeleteDataModalProps) => { i18n: { language }, } = useTranslation('common') - const [filters, setFilters] = useState([]) + const [filters, setFilters] = useState([]) const [period, setPeriod] = useState('all') const [customRange, setCustomRange] = useState([]) // null = follow the auto default (every type that has matching rows) diff --git a/web/app/pages/Project/View/Panels.tsx b/web/app/pages/Project/View/Panels.tsx index 074766a74..cf236fbdd 100644 --- a/web/app/pages/Project/View/Panels.tsx +++ b/web/app/pages/Project/View/Panels.tsx @@ -47,15 +47,14 @@ import { createPortal } from 'react-dom' import { Link } from '~/ui/Link' import { LinkProps, useNavigate } from 'react-router' -import { useProjectDataCustomEventsProxy } from '~/hooks/useAnalyticsProxy' import { PROJECT_TABS } from '~/lib/constants' import { Entry } from '~/lib/models/Entry' -import { useCurrentProject } from '~/providers/CurrentProjectProvider' import Button from '~/ui/Button' import Dropdown from '~/ui/Dropdown' import Input from '~/ui/Input' import Sort from '~/ui/icons/Sort' import Spin from '~/ui/icons/Spin' +import Loader from '~/ui/Loader' import Modal from '~/ui/Modal' import { Text } from '~/ui/Text' import { @@ -69,18 +68,27 @@ import { nFormatter, getLocaleDisplayName } from '~/utils/generic' import countries from '~/utils/isoCountries' import { MainChart } from './components/MainChart' -import { Customs, Filter } from './interfaces/traffic' +import { V2Filter } from '~/api/v2/types' +import { Customs } from './interfaces/traffic' +import { RefetchIndicator } from './v2/loading' import { useViewProjectContext } from './ViewProject' +import { useCustomEventsTimeseriesQuery } from '~/hooks/v2/useV2Queries' import { - getFormatDate, getSettingsCustomEventsStacked, typeNameMapping, } from './ViewProject.helpers' +import { pivotCustomEventsTimeseries } from './v2/adapters' const ENTRIES_PER_PANEL = 8 const ENTRIES_PER_CUSTOM_EVENTS_PANEL = 7 const CUSTOM_EVENTS_TOP_LIMITS = [1, 3, 5, 10] as const -const UTM_TRACKING_PANEL_IDS = new Set(['so', 'me', 'ca', 'te', 'co']) +const UTM_TRACKING_PANEL_IDS = new Set([ + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_term', + 'utm_content', +]) const PANEL_HEADER_ACTIONS_CLASS = 'panel-header-actions flex min-w-0 max-w-full items-center gap-2.5 overflow-x-auto overflow-y-hidden pb-0.5 scrollbar-thin' const PANEL_EMPTY_STATE_DOCS = { @@ -152,6 +160,14 @@ const PanelEmptyState = ({ ) +// Mirrors PanelEmptyState's box model so a panel is the same height whether it +// is loading, empty or full. +const PanelLoadingState = () => ( +
+ +
+) + const PanelDocsEmptyState = ({ variant, }: { @@ -213,7 +229,12 @@ interface PanelContainerProps { onTabChange?: (tab: string) => void activeTabId?: string onDetailsClick?: () => void + /** Renders the details button in a disabled state, keeping the panel height stable while there is nothing to drill into. */ + detailsDisabled?: boolean dropdownPlaceholder?: string + isRefetching?: boolean + /** Replaces the panel body with a centered spinner, keeping the panel at its usual height. */ + isLoading?: boolean } const PanelContainer = ({ @@ -228,7 +249,10 @@ const PanelContainer = ({ onTabChange, activeTabId, onDetailsClick, + detailsDisabled, dropdownPlaceholder, + isRefetching, + isLoading, }: PanelContainerProps) => { const { t } = useTranslation('common') const underlineId = useId() @@ -236,7 +260,7 @@ const PanelContainer = ({ return (
+ {isRefetching ? : null} {!hideHeader ? (
) : null}
- {children} + {isLoading ? : children}
{onDetailsClick ? (
@@ -348,6 +374,7 @@ const PanelContainer = ({ className='gap-1.5 px-3 py-2' type='button' onClick={onDetailsClick} + disabled={detailsDisabled} aria-label={t('common.details')} > @@ -362,7 +389,7 @@ const PanelContainer = ({ interface CustomEventsProps { customs?: Customs chartData: any - filters: Filter[] + filters: V2Filter[] getCustomEventMetadata: (event: string) => Promise getFilterLink: (column: string, value: string | null) => LinkProps['to'] } @@ -612,10 +639,7 @@ const CustomEvents = ({ getFilterLink, }: CustomEventsProps) => { const { t } = useTranslation('common') - const { id } = useCurrentProject() - const { timeBucket, timezone, period, dateRange, timeFormat, rotateXAxis } = - useViewProjectContext() - const { fetchCustomEvents } = useProjectDataCustomEventsProxy() + const { timeBucket, timeFormat, rotateXAxis } = useViewProjectContext() const [detailsOpened, setDetailsOpened] = useState(false) const [activeEvents, setActiveEvents] = useState({}) @@ -624,11 +648,6 @@ const CustomEvents = ({ const [eventsData, setEventsData] = useState(customs) const [triggerEventWhenFiltersChange, setTriggerEventWhenFiltersChange] = useState(null) - const [stackedChartLoading, setStackedChartLoading] = useState(false) - const [stackedChart, setStackedChart] = useState<{ - x: string[] - events: Record> - } | null>(null) const [chartTopLimit, setChartTopLimit] = useState<(typeof CUSTOM_EVENTS_TOP_LIMITS)[number]>(5) @@ -677,68 +696,18 @@ const CustomEvents = ({ }) }, [customs]) - useEffect(() => { - let cancelled = false - - const loadStackedChart = async () => { - if (!id || _isEmpty(topEventsForChart)) { - setStackedChart(null) - return - } - - setStackedChartLoading(true) - - try { - const isCustomPeriod = period === 'custom' && dateRange - const from = isCustomPeriod ? getFormatDate(dateRange![0]) : '' - const to = isCustomPeriod ? getFormatDate(dateRange![1]) : '' - const periodValue = isCustomPeriod ? '' : period - - const data = await fetchCustomEvents(id, topEventsForChart, { - timeBucket, - period: periodValue, - filters, - from, - to, - timezone, - }) - - if (cancelled) { - return - } - - setStackedChart((data?.chart as unknown as typeof stackedChart) || null) - } catch (reason) { - console.error( - '[ERROR](CustomEvents) Failed to load stacked custom events chart', - reason, - ) - if (!cancelled) { - setStackedChart(null) - } - } finally { - if (!cancelled) { - setStackedChartLoading(false) - } - } + const stackedChartQuery = useCustomEventsTimeseriesQuery(topEventsForChart) + const stackedChartLoading = stackedChartQuery.isLoading + const stackedChart = useMemo(() => { + if (_isEmpty(topEventsForChart) || !stackedChartQuery.data) { + return null } - loadStackedChart() - - return () => { - cancelled = true - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - id, - timeBucket, - period, - dateRange, - timezone, - filters, - topEventsForChart, - fetchCustomEvents, - ]) + return pivotCustomEventsTimeseries( + stackedChartQuery.data.data, + topEventsForChart, + ) + }, [stackedChartQuery.data, topEventsForChart]) useEffect(() => { setEventsMetadata({}) @@ -846,7 +815,7 @@ const CustomEvents = ({ } const _getFilterLink = (column: string | null, value: string | null) => { - return getFilterLink('ev' + (column ? `:key:${column}` : ''), value) + return getFilterLink(column ? `event_metadata:${column}` : 'event', value) } const CustomEventsTable = () => ( @@ -1152,10 +1121,10 @@ interface MetadataKeyPanelProps { ) => Promise<{ result: Array<{ key: string; value: string; count: number }> }> getFilterLink: (column: string, value: string | null) => LinkProps['to'] chartData: any - filters: Filter[] + filters: V2Filter[] activeKey: string onKeyChange: (key: string) => void - filterPrefix: 'ev:key' | 'tag:key' + filterPrefix: 'event_metadata' | 'page_property' // For 'property' mode: dropdown shows property keys, values are for that key // For 'customEvent' mode: dropdown shows event names, values show ALL metadata for that event (grouped by key) mode: 'property' | 'customEvent' @@ -1402,14 +1371,9 @@ const MetadataKeyBody = ({ const hasExistingData = mode === 'customEvent' ? !_isEmpty(rawMetadata) : !_isEmpty(valuesData) - const hasOverflowData = - mode === 'customEvent' - ? _size(rawMetadata) > ENTRIES_PER_PANEL - : _size(valuesData) > ENTRIES_PER_PANEL - useEffect(() => { - onShowDetailsChange?.(hasOverflowData) - }, [hasOverflowData, onShowDetailsChange]) + onShowDetailsChange?.(hasExistingData) + }, [hasExistingData, onShowDetailsChange]) useEffect(() => { if (detailsTrigger && detailsTrigger > 0) { @@ -1434,11 +1398,7 @@ const MetadataKeyBody = ({ // Only show loader on initial load when there's no existing data if (loading && !hasExistingData) { - return ( -
- -
- ) + return } if ( @@ -1654,7 +1614,7 @@ interface CombinedMetadataPanelProps { } getFilterLink: (column: string, value: string | null) => LinkProps['to'] chartData: any - filters: Filter[] + filters: V2Filter[] } const CombinedMetadataPanel = ({ @@ -1686,8 +1646,8 @@ const CombinedMetadataPanel = ({ }, [activeMode, hasProperty, hasCustomEvent]) const activeSection = activeMode === 'property' ? property : customEvent - const activeFilterPrefix: 'ev:key' | 'tag:key' = - activeMode === 'property' ? 'tag:key' : 'ev:key' + const activeFilterPrefix: 'event_metadata' | 'page_property' = + activeMode === 'property' ? 'page_property' : 'event_metadata' const dropdownItems = useMemo( () => activeSection.metadataKeys.map((key) => ({ id: key, label: key })), @@ -1789,20 +1749,19 @@ const CombinedMetadataPanel = ({ detailsTrigger={detailsTrigger} />
- {canShowDetails ? ( -
- -
- ) : null} +
+ +
) } @@ -1826,7 +1785,7 @@ const FilterWrapper = ({ children, as, to, ...props }: FilterWrapperProps) => { return
{children}
} -interface PanelProps { +export interface PanelProps { name: string data: Entry[] rowMapper?: (row: any) => React.ReactNode @@ -1872,6 +1831,12 @@ interface PanelProps { rowTooltipRenderer?: (entry: Entry) => React.ReactNode /** When true with rowTooltipRenderer, tooltip is fixed to the cursor (chart-style) instead of Radix anchor positioning. */ rowTooltipFollowCursor?: boolean + isRefetching?: boolean + /** Shows a centered spinner in place of the rows while the first page is being fetched. */ + isLoading?: boolean + serverTotal?: number + onLoadMore?: () => void + loadingMore?: boolean } interface DetailsTableProps extends Pick< @@ -1886,6 +1851,9 @@ interface DetailsTableProps extends Pick< | 'valueMapper' | 'getFilterLink' | 'activeTab' + | 'serverTotal' + | 'onLoadMore' + | 'loadingMore' > { total: number closeDetails: () => void @@ -1915,6 +1883,9 @@ const DetailsTable = ({ hidePercentageInDetails, detailsExtraColumns, activeTab = PROJECT_TABS.traffic, + serverTotal, + onLoadMore, + loadingMore, }: DetailsTableProps) => { const { t, @@ -1932,7 +1903,7 @@ const DetailsTable = ({ const getNameLabel = useCallback( (entry: Entry): string => { - if (activeTabId === 'cc') { + if (activeTabId === 'country') { const code = entry.cc || entry.name try { const countryName = code ? countries.getName(code, language) : '' @@ -1942,7 +1913,7 @@ const DetailsTable = ({ } } - if (activeTabId === 'lc') { + if (activeTabId === 'locale') { try { return ( getLocaleDisplayName(entry?.name, language) || entry?.name || '' @@ -2240,6 +2211,26 @@ const DetailsTable = ({
+ {onLoadMore ? ( +
+ + {serverTotal ? ( + + {data.length} / {nFormatter(serverTotal, 1)} + + ) : null} +
+ ) : null} ) } @@ -2272,6 +2263,11 @@ const Panel = ({ selectedRow, rowTooltipRenderer, rowTooltipFollowCursor = false, + isRefetching, + isLoading, + serverTotal, + onLoadMore, + loadingMore, }: PanelProps) => { const ctx = useViewProjectContext() const dataLoading = dataLoadingProp ?? ctx.dataLoading @@ -2321,17 +2317,19 @@ const Panel = ({ return ( ENTRIES_PER_PANEL - ? () => setDetailsOpened(true) - : undefined - } + // A custom body (a map, an error state) has no rows to drill into, so it + // gets no details button at all. Row panels always keep theirs — disabled + // rather than absent — so the panel is the same height while loading. + onDetailsClick={customRenderer ? undefined : () => setDetailsOpened(true)} + detailsDisabled={isLoading || _isEmpty(data)} name={name} icon={icon} type={id} tabs={tabs} onTabChange={onTabChange} activeTabId={activeTabId} + isRefetching={isRefetching} + isLoading={isLoading} > {customRenderer ? ( customRenderer() @@ -2722,6 +2720,9 @@ const Panel = ({ hidePercentageInDetails={hidePercentageInDetails} detailsExtraColumns={detailsExtraColumns} activeTab={activeTab} + serverTotal={serverTotal} + onLoadMore={onLoadMore} + loadingMore={loadingMore} /> } size='large' @@ -2973,6 +2974,7 @@ const MetadataPanelMemo = memo(MetadataPanel) as typeof MetadataPanel export { PanelContainer, PanelEmptyState, + PanelLoadingState, PanelMemo as Panel, CustomEventsMemo as CustomEvents, CombinedMetadataPanelMemo as CombinedMetadataPanel, diff --git a/web/app/pages/Project/View/ViewProject.helpers.tsx b/web/app/pages/Project/View/ViewProject.helpers.tsx index 88fa2df21..8303bea75 100644 --- a/web/app/pages/Project/View/ViewProject.helpers.tsx +++ b/web/app/pages/Project/View/ViewProject.helpers.tsx @@ -2341,36 +2341,37 @@ const getSettingsPerf = ( } const typeNameMapping = (t: typeof i18next.t) => ({ - cc: t('project.mapping.cc'), + country: t('project.mapping.cc'), host: t('project.mapping.host'), - rg: t('project.mapping.rg'), - ct: t('project.mapping.ct'), - pg: t('project.mapping.pg'), - keywords: t('project.seo.topQueries'), - entryPage: t('project.entryPages'), - exitPage: t('project.exitPages'), - lc: t('project.mapping.lc'), - ref: t('project.mapping.ref'), - refn: t('project.mapping.ref'), - dv: t('project.mapping.dv'), - br: t('project.mapping.br'), - brv: t('project.mapping.brv'), + region: t('project.mapping.rg'), + city: t('project.mapping.ct'), + page: t('project.mapping.pg'), + query: t('project.seo.query'), + entry_page: t('project.entryPages'), + exit_page: t('project.exitPages'), + locale: t('project.mapping.lc'), + referrer: t('project.mapping.ref'), + referrer_name: t('project.mapping.ref'), + device: t('project.mapping.dv'), + browser: t('project.mapping.br'), + browser_version: t('project.mapping.brv'), os: t('project.mapping.os'), - osv: t('project.mapping.osv'), - so: t('project.mapping.so'), - me: t('project.mapping.me'), - ca: t('project.mapping.ca'), - te: t('project.mapping.te'), - co: t('project.mapping.co'), + os_version: t('project.mapping.osv'), + utm_source: t('project.mapping.so'), + utm_medium: t('project.mapping.me'), + utm_campaign: t('project.mapping.ca'), + utm_term: t('project.mapping.te'), + utm_content: t('project.mapping.co'), isp: t('project.mapping.isp'), - og: t('project.mapping.og'), - ut: t('project.mapping.ut'), - ctp: t('project.mapping.ctp'), - ev: t('project.event'), - 'tag:key': t('project.metamapping.tag.key'), - 'tag:value': t('project.metamapping.tag.value'), - 'ev:key': t('project.metamapping.ev.key'), - 'ev:value': t('project.metamapping.ev.value'), + organization: t('project.mapping.og'), + user_type: t('project.mapping.ut'), + connection_type: t('project.mapping.ctp'), + event: t('project.event'), + event_metadata: t('project.metamapping.ev.key'), + page_property: t('project.metamapping.tag.key'), + error_name: t('project.mapping.error_name'), + error_message: t('project.mapping.error_message'), + error_filename: t('project.mapping.error_filename'), searchEngine: t('project.mapping.searchEngine'), aiReferral: t('project.mapping.aiReferral'), captcha_event: t('project.event'), @@ -2379,7 +2380,6 @@ const typeNameMapping = (t: typeof i18next.t) => ({ solve_time: t('project.mapping.solve_time'), // Combined panel types location: t('project.location'), - browser: t('project.browser'), devices: t('project.devices'), network: t('project.network'), map: t('project.map'), @@ -2387,14 +2387,14 @@ const typeNameMapping = (t: typeof i18next.t) => ({ const iconClassName = 'w-5 h-5' const panelIconMapping = { - cc: , - pg: , - lc: , - ref: , - dv: , - br: , + country: , + page: , + locale: , + referrer: , + device: , + browser: , os: , - so: , + utm_source: , isp: , } diff --git a/web/app/pages/Project/View/ViewProject.tsx b/web/app/pages/Project/View/ViewProject.tsx index 01a03d3a6..451d2c791 100644 --- a/web/app/pages/Project/View/ViewProject.tsx +++ b/web/app/pages/Project/View/ViewProject.tsx @@ -1,3 +1,4 @@ +import { useQueryClient } from '@tanstack/react-query' import cx from 'clsx' import dayjs from 'dayjs' import { AnimatePresence, motion } from 'motion/react' @@ -95,12 +96,9 @@ import ProjectSidebar, { MobileSidebarTrigger, } from './components/ProjectSidebar' import SearchFilters from './components/SearchFilters' -import { - Filter, - ProjectView, - ProjectViewCustomEvent, -} from './interfaces/traffic' -import { parseFilters } from './utils/filters' +import { ProjectView, ProjectViewCustomEvent } from './interfaces/traffic' +import { V2Filter } from '~/api/v2/types' +import { parseFiltersFromUrl } from '~/utils/analyticsUrl' import { getProjectViewCreateType, getProjectViewFilterOptions, @@ -108,7 +106,6 @@ import { import { getFormatDate, typeNameMapping, - CHART_METRICS_MAPPING, SHORTCUTS_TABS_LISTENERS, SHORTCUTS_TABS_MAP, SHORTCUTS_GENERAL_LISTENERS, @@ -148,7 +145,7 @@ interface ViewProjectContextType { size: ReturnType[1] dataLoading: boolean activeTab: keyof typeof PROJECT_TABS - filters: Filter[] + filters: V2Filter[] projectViews: ProjectView[] projectViewsLoading: boolean | null loadProjectViews: (forced?: boolean) => void @@ -173,11 +170,11 @@ interface ViewProjectContextType { onMainChartZoom: (domain: [Date, Date] | null) => void shouldEnableZoom: boolean - getFilterLink: (column: string, value: string | null) => LinkProps['to'] + getFilterLink: (dimension: string, value: string | null) => LinkProps['to'] getVersionFilterLink: ( parent: string | null, version: string | null, - panelType: 'br' | 'os', + panelType: 'browser' | 'os', ) => string updatePeriod: (newPeriod: { period: Period; label?: string }) => void @@ -195,19 +192,11 @@ interface ViewProjectContextType { } interface RefreshTriggersContextType { - captchaRefreshTrigger: number goalsRefreshTrigger: number experimentsRefreshTrigger: number featureFlagsRefreshTrigger: number - sessionsRefreshTrigger: number - errorsRefreshTrigger: number - performanceRefreshTrigger: number - trafficRefreshTrigger: number - funnelsRefreshTrigger: number journeysRefreshTrigger: number - profilesRefreshTrigger: number replaysRefreshTrigger: number - seoRefreshTrigger: number } const defaultViewProjectContext: ViewProjectContextType = { @@ -265,25 +254,17 @@ const defaultViewProjectContext: ViewProjectContextType = { } const defaultRefreshTriggersContext: RefreshTriggersContextType = { - captchaRefreshTrigger: 0, goalsRefreshTrigger: 0, experimentsRefreshTrigger: 0, featureFlagsRefreshTrigger: 0, - sessionsRefreshTrigger: 0, - errorsRefreshTrigger: 0, - performanceRefreshTrigger: 0, - trafficRefreshTrigger: 0, - funnelsRefreshTrigger: 0, journeysRefreshTrigger: 0, - profilesRefreshTrigger: 0, replaysRefreshTrigger: 0, - seoRefreshTrigger: 0, } -export const ViewProjectContext = createContext( +const ViewProjectContext = createContext( defaultViewProjectContext, ) -export const RefreshTriggersContext = createContext( +const RefreshTriggersContext = createContext( defaultRefreshTriggersContext, ) @@ -339,6 +320,7 @@ const ViewProjectContent = () => { }, [isMapFullscreen]) const navigate = useNavigate() + const queryClient = useQueryClient() const [searchParams, setSearchParams] = useSearchParams() @@ -356,33 +338,12 @@ const ViewProjectContent = () => { const [isAddAViewOpened, setIsAddAViewOpened] = useState(false) const [dataLoading] = useState(false) - const [captchaRefreshTrigger, setCaptchaRefreshTrigger] = useState(0) const [goalsRefreshTrigger, setGoalsRefreshTrigger] = useState(0) const [experimentsRefreshTrigger, setExperimentsRefreshTrigger] = useState(0) const [featureFlagsRefreshTrigger, setFeatureFlagsRefreshTrigger] = useState(0) - const [sessionsRefreshTrigger, setSessionsRefreshTrigger] = useState(0) - const [errorsRefreshTrigger, setErrorsRefreshTrigger] = useState(0) - const [performanceRefreshTrigger, setPerformanceRefreshTrigger] = useState(0) - const [trafficRefreshTrigger, setTrafficRefreshTrigger] = useState(0) - const [funnelsRefreshTrigger, setFunnelsRefreshTrigger] = useState(0) const [journeysRefreshTrigger, setJourneysRefreshTrigger] = useState(0) - const [profilesRefreshTrigger, setProfilesRefreshTrigger] = useState(0) const [replaysRefreshTrigger, setReplaysRefreshTrigger] = useState(0) - const [seoRefreshTrigger, setSeoRefreshTrigger] = useState(0) - const [activeChartMetrics] = useState< - Record - >({ - [CHART_METRICS_MAPPING.unique]: true, - [CHART_METRICS_MAPPING.views]: false, - [CHART_METRICS_MAPPING.sessionDuration]: false, - [CHART_METRICS_MAPPING.bounce]: false, - [CHART_METRICS_MAPPING.viewsPerUnique]: false, - [CHART_METRICS_MAPPING.trendlines]: false, - [CHART_METRICS_MAPPING.cumulativeMode]: false, - [CHART_METRICS_MAPPING.customEvents]: false, - [CHART_METRICS_MAPPING.revenue]: false, - } as Record) const customMetrics = useMemo(() => { const raw = searchParams.get('metrics') if (!raw) return [] @@ -392,8 +353,8 @@ const ViewProjectContent = () => { return [] } }, [searchParams]) - const filters = useMemo(() => { - return parseFilters(searchParams) + const filters = useMemo(() => { + return parseFiltersFromUrl(searchParams) }, [searchParams]) const tnMapping = typeNameMapping(t) @@ -540,10 +501,6 @@ const ViewProjectContent = () => { const viewsLoadFetcher = useFetcher() - const mode = activeChartMetrics[CHART_METRICS_MAPPING.cumulativeMode] - ? 'cumulative' - : 'periodical' - const loadProjectViews = useCallback( (forced?: boolean) => { if (!forced && projectViewsLoading !== null) { @@ -581,18 +538,15 @@ const ViewProjectContent = () => { }, [viewsLoadFetcher.state, viewsLoadFetcher.data]) const getVersionFilterLink = useCallback( - (parent: string | null, version: string | null, panelType: 'br' | 'os') => { + ( + parent: string | null, + version: string | null, + panelType: 'browser' | 'os', + ) => { const filterParams = new URLSearchParams(searchParams.toString()) - if (panelType === 'br') { - // Apply both browser and browser version filters together - filterParams.set('br', parent ?? 'null') - filterParams.set('brv', version ?? 'null') - } else if (panelType === 'os') { - // Apply both OS and OS version filters together - filterParams.set('os', parent ?? 'null') - filterParams.set('osv', version ?? 'null') - } + filterParams.set(panelType, parent ?? 'null') + filterParams.set(`${panelType}_version`, version ?? 'null') return `?${filterParams.toString()}` }, @@ -926,76 +880,58 @@ const ViewProjectContent = () => { [dataLoading, searchParams, setSearchParams], ) + const V2_TABS = useMemo( + () => + new Set([ + PROJECT_TABS.traffic, + PROJECT_TABS.performance, + PROJECT_TABS.sessions, + PROJECT_TABS.errors, + PROJECT_TABS.funnels, + PROJECT_TABS.profiles, + PROJECT_TABS.captcha, + PROJECT_TABS.seo, + ]), + [], + ) + const refreshStats = useCallback( async (_isManual = true) => { - if (!authLoading && !dataLoading) { - if (activeTab === PROJECT_TABS.funnels) { - setFunnelsRefreshTrigger((prev) => prev + 1) - return - } - - if (activeTab === PROJECT_TABS.journeys) { - setJourneysRefreshTrigger((prev) => prev + 1) - return - } - - if (activeTab === PROJECT_TABS.profiles) { - setProfilesRefreshTrigger((prev) => prev + 1) - return - } - - if (activeTab === PROJECT_TABS.captcha) { - setCaptchaRefreshTrigger((prev) => prev + 1) - return - } - - if (activeTab === PROJECT_TABS.goals) { - setGoalsRefreshTrigger((prev) => prev + 1) - return - } - - if (activeTab === PROJECT_TABS.experiments) { - setExperimentsRefreshTrigger((prev) => prev + 1) - return - } - - if (activeTab === PROJECT_TABS.featureFlags) { - setFeatureFlagsRefreshTrigger((prev) => prev + 1) - return - } + if (authLoading || dataLoading) { + return + } - if (activeTab === PROJECT_TABS.sessions) { - setSessionsRefreshTrigger((prev) => prev + 1) - return - } + if (V2_TABS.has(activeTab)) { + await queryClient.invalidateQueries({ queryKey: ['v2', id] }) + return + } - if (activeTab === PROJECT_TABS.replays) { - setReplaysRefreshTrigger((prev) => prev + 1) - return - } + if (activeTab === PROJECT_TABS.journeys) { + setJourneysRefreshTrigger((prev) => prev + 1) + return + } - if (activeTab === PROJECT_TABS.errors) { - setErrorsRefreshTrigger((prev) => prev + 1) - return - } + if (activeTab === PROJECT_TABS.goals) { + setGoalsRefreshTrigger((prev) => prev + 1) + return + } - if (activeTab === PROJECT_TABS.performance) { - setPerformanceRefreshTrigger((prev) => prev + 1) - return - } + if (activeTab === PROJECT_TABS.experiments) { + setExperimentsRefreshTrigger((prev) => prev + 1) + return + } - if (activeTab === PROJECT_TABS.traffic) { - setTrafficRefreshTrigger((prev) => prev + 1) - return - } + if (activeTab === PROJECT_TABS.featureFlags) { + setFeatureFlagsRefreshTrigger((prev) => prev + 1) + return + } - if (activeTab === PROJECT_TABS.seo) { - setSeoRefreshTrigger((prev) => prev + 1) - return - } + if (activeTab === PROJECT_TABS.replays) { + setReplaysRefreshTrigger((prev) => prev + 1) + return } }, - [authLoading, dataLoading, activeTab], + [authLoading, dataLoading, activeTab, queryClient, id, V2_TABS], ) useEffect(() => { @@ -1086,31 +1022,33 @@ const ViewProjectContent = () => { }, [period, dateRange, isTouchDevice]) const getFilterLink = useCallback( - (column: string, value: string | null): LinkProps['to'] => { - const isFilterActive = - filters.findIndex( - (filter) => filter.column === column && filter.filter === value, - ) >= 0 + (dimension: string, value: string | null): LinkProps['to'] => { + const urlValue = value ?? 'null' + const variants = [ + dimension, + `!${dimension}`, + `~${dimension}`, + `^${dimension}`, + ] + const isFilterActive = variants.some((key) => + searchParams.getAll(key).includes(urlValue), + ) const newSearchParams = new URLSearchParams(searchParams.toString()) - let searchString = '' if (isFilterActive) { - newSearchParams.delete(column, value ?? 'null') - newSearchParams.delete(`!${column}`, value ?? 'null') - newSearchParams.delete(`~${column}`, value ?? 'null') - newSearchParams.delete(`^${column}`, value ?? 'null') - searchString = newSearchParams.toString() + for (const key of variants) { + newSearchParams.delete(key, urlValue) + } } else { - newSearchParams.append(column, value ?? 'null') - searchString = newSearchParams.toString() + newSearchParams.append(dimension, urlValue) } return { - search: searchString, + search: newSearchParams.toString(), } }, - [filters, searchParams], + [searchParams], ) const setChartTypeOnClick = useCallback((type: keyof typeof chartTypes) => { @@ -1325,34 +1263,18 @@ const ViewProjectContent = () => { const refreshTriggersValue = useMemo( () => ({ - captchaRefreshTrigger, goalsRefreshTrigger, experimentsRefreshTrigger, featureFlagsRefreshTrigger, - sessionsRefreshTrigger, - errorsRefreshTrigger, - performanceRefreshTrigger, - trafficRefreshTrigger, - funnelsRefreshTrigger, journeysRefreshTrigger, - profilesRefreshTrigger, replaysRefreshTrigger, - seoRefreshTrigger, }), [ - captchaRefreshTrigger, goalsRefreshTrigger, experimentsRefreshTrigger, featureFlagsRefreshTrigger, - sessionsRefreshTrigger, - errorsRefreshTrigger, - performanceRefreshTrigger, - trafficRefreshTrigger, - funnelsRefreshTrigger, journeysRefreshTrigger, - profilesRefreshTrigger, replaysRefreshTrigger, - seoRefreshTrigger, ], ) @@ -1611,7 +1533,15 @@ const ViewProjectContent = () => { ease: [0.23, 1, 0.32, 1], }} > - }> + {/* Reserve roughly a screenful while the tab chunk + downloads, otherwise the page is briefly short + enough to pull the footer into view and then + jumps once the tab renders. */} + + } + > {activeTab === PROJECT_TABS.ai ? ( ) : null} @@ -1621,7 +1551,6 @@ const ViewProjectContent = () => { customMetrics={customMetrics} onRemoveCustomMetric={onRemoveCustomMetric} resetCustomMetrics={resetCustomMetrics} - mode={mode} /> ) : null} {activeTab === PROJECT_TABS.performance ? ( diff --git a/web/app/pages/Project/View/components/AddAViewModal.tsx b/web/app/pages/Project/View/components/AddAViewModal.tsx index b90e4d44c..18bf392a7 100644 --- a/web/app/pages/Project/View/components/AddAViewModal.tsx +++ b/web/app/pages/Project/View/components/AddAViewModal.tsx @@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next' import { useFetcher } from 'react-router' import { toast } from 'sonner' +import { V2Filter } from '~/api/v2/types' import { ProjectViewActionData } from '~/routes/projects.$id' import Input from '~/ui/Input' import Modal from '~/ui/Modal' @@ -15,11 +16,11 @@ import Select from '~/ui/Select' import { Text } from '~/ui/Text' import { - Filter as FilterType, ProjectView, ProjectViewCustomEvent, ProjectViewCustomEventMetaValueType, } from '../interfaces/traffic' +import { projectViewFiltersToV2 } from '../utils/projectViewSegments' import FilterRowsEditor from './FilterRowsEditor' @@ -212,13 +213,17 @@ const AddAViewModal = ({ const { t } = useTranslation('common') const fetcher = useFetcher() const [name, setName] = useState(defaultView?.name || '') - const [activeFilters, setActiveFilters] = useState( - defaultView?.filters || [], + const [activeFilters, setActiveFilters] = useState(() => + projectViewFiltersToV2(defaultView), ) const [customEvents, setCustomEvents] = useState< Partial[] >(defaultView?.customEvents || []) const [errors, setErrors] = useState({}) + const initialFilters = useMemo( + () => projectViewFiltersToV2(defaultView), + [defaultView], + ) useEffect(() => { if (!defaultView) { @@ -227,7 +232,7 @@ const AddAViewModal = ({ if (defaultView.filters) { // eslint-disable-next-line react-hooks/set-state-in-effect -- Syncing form state with defaultView prop - setActiveFilters(defaultView.filters) + setActiveFilters(projectViewFiltersToV2(defaultView)) } if (defaultView.customEvents) { @@ -389,7 +394,7 @@ const AddAViewModal = ({ - initialFilters?: FilterType[] - currentFilters?: FilterType[] + initialFilters?: V2Filter[] + currentFilters?: V2Filter[] type: 'traffic' | 'errors' filterOptions?: string[] - onChange: (filters: FilterType[]) => void + onChange: (filters: V2Filter[]) => void resetKey?: string prefetchOnOpen?: boolean showCurrentFilters?: boolean @@ -61,27 +57,39 @@ interface FilterRowsEditorProps { projectId?: string } -const EMPTY_FILTERS: FilterType[] = [] +const EMPTY_FILTERS: V2Filter[] = [] -const OPERATORS: { value: FilterOperator; labelKey: string }[] = [ +const OPERATORS: { value: V2FilterOperator; labelKey: string }[] = [ { value: 'is', labelKey: 'common.is' }, - { value: 'isNot', labelKey: 'common.isNot' }, + { value: 'is_not', labelKey: 'common.isNot' }, { value: 'contains', labelKey: 'project.contains.is' }, - { value: 'notContains', labelKey: 'project.contains.not' }, + { value: 'contains_not', labelKey: 'project.contains.not' }, ] -const getMetadataColumnBase = (column: string) => { - if (column === 'ev:key' || column.startsWith('ev:key:')) return 'ev:key' - if (column === 'tag:key' || column.startsWith('tag:key:')) return 'tag:key' - if (column === 'ev:value') return 'ev:value' - if (column === 'tag:value') return 'tag:value' - return column +const parseRowColumn = ( + column: string, +): { dimension: string; key?: string } => { + for (const dimension of KEYED_DIMENSIONS) { + if (column.startsWith(`${dimension}:`)) { + return { dimension, key: column.substring(dimension.length + 1) } + } + } + + return { dimension: column } } +const getMetadataColumnBase = (column: string) => + parseRowColumn(column).dimension + +const isKeyedColumn = (column: string) => + (KEYED_DIMENSIONS as readonly string[]).includes( + getMetadataColumnBase(column), + ) + const getOperatorsForColumn = (column: string) => { - if (column.startsWith('ev:key') || column.startsWith('tag:key')) { + if (isKeyedColumn(column)) { return OPERATORS.filter( - (operator) => operator.value === 'is' || operator.value === 'isNot', + (operator) => operator.value === 'is' || operator.value === 'is_not', ) } @@ -90,8 +98,8 @@ const getOperatorsForColumn = (column: string) => { const normalizeFilterOperator = ( column: string, - operator: FilterOperator, -): FilterOperator => { + operator: V2FilterOperator, +): V2FilterOperator => { const operatorOptions = getOperatorsForColumn(column) return operatorOptions.some((option) => option.value === operator) @@ -112,32 +120,10 @@ const createFilterRow = ({ const createEmptyFilterRow = (): FilterRow => createFilterRow() -const operatorToFilter = ( - operator: FilterOperator, -): { isExclusive: boolean; isContains: boolean } => { - switch (operator) { - case 'is': - return { isExclusive: false, isContains: false } - case 'isNot': - return { isExclusive: true, isContains: false } - case 'contains': - return { isExclusive: false, isContains: true } - case 'notContains': - return { isExclusive: true, isContains: true } - } -} +const filterToRowColumn = (filter: V2Filter): string => + filter.key ? `${filter.dimension}:${filter.key}` : filter.dimension -const filterToOperator = ({ - isExclusive, - isContains, -}: FilterType): FilterOperator => { - if (isContains && isExclusive) return 'notContains' - if (isContains) return 'contains' - if (isExclusive) return 'isNot' - return 'is' -} - -const filtersToRows = (filters: FilterType[] = []): FilterRow[] => { +const filtersToRows = (filters: V2Filter[] = []): FilterRow[] => { const usedIndexes = new Set() return filters.flatMap((filter, index) => { @@ -145,15 +131,15 @@ const filtersToRows = (filters: FilterType[] = []): FilterRow[] => { return [] } - if (filter.column === 'br' || filter.column === 'os') { - const versionColumn = filter.column === 'br' ? 'brv' : 'osv' - const operator = filterToOperator(filter) + if (filter.dimension === 'browser' || filter.dimension === 'os') { + const versionDimension = + filter.dimension === 'browser' ? 'browser_version' : 'os_version' const versionIndex = filters.findIndex( (candidate, candidateIndex) => candidateIndex !== index && !usedIndexes.has(candidateIndex) && - candidate.column === versionColumn && - filterToOperator(candidate) === operator, + candidate.dimension === versionDimension && + candidate.operator === filter.operator, ) if (versionIndex !== -1) { @@ -162,11 +148,11 @@ const filtersToRows = (filters: FilterType[] = []): FilterRow[] => { return [ createFilterRow({ - column: versionColumn, - operator, + column: versionDimension, + operator: filter.operator, value: createVersionValue( - filter.filter, - filters[versionIndex].filter, + filterToUrlValue(filter), + filterToUrlValue(filters[versionIndex]), ), }), ] @@ -177,9 +163,9 @@ const filtersToRows = (filters: FilterType[] = []): FilterRow[] => { return [ createFilterRow({ - column: filter.column, - operator: filterToOperator(filter), - value: filter.filter, + column: filterToRowColumn(filter), + operator: filter.operator, + value: filterToUrlValue(filter), }), ] }) @@ -206,79 +192,55 @@ const InlineButton = ({ ) -const filterRowsToFilters = (filterRows: FilterRow[], language: string) => { - const validFilters: FilterType[] = [] +const filterRowsToFilters = ( + filterRows: FilterRow[], + language: string, +): V2Filter[] => { + const validFilters: V2Filter[] = [] filterRows .filter((row) => row.column && row.value) .forEach((row) => { - const { isExclusive, isContains } = operatorToFilter(row.operator) - - if (row.column === 'brv') { - const parsed = parseVersionValue(row.value) - if (parsed) { - validFilters.push({ - column: 'br', - filter: parsed.parent, - isExclusive, - isContains, - }) - validFilters.push({ - column: 'brv', - filter: parsed.version, - isExclusive, - isContains, - }) - } else { - validFilters.push({ - column: 'brv', - filter: row.value, - isExclusive, - isContains, - }) - } - return - } - - if (row.column === 'osv') { + if (row.column === 'browser_version' || row.column === 'os_version') { + const parentDimension = + row.column === 'browser_version' ? 'browser' : 'os' const parsed = parseVersionValue(row.value) if (parsed) { validFilters.push({ - column: 'os', - filter: parsed.parent, - isExclusive, - isContains, + dimension: parentDimension, + operator: row.operator, + value: parsed.parent, }) validFilters.push({ - column: 'osv', - filter: parsed.version, - isExclusive, - isContains, + dimension: row.column, + operator: row.operator, + value: parsed.version, }) } else { validFilters.push({ - column: 'osv', - filter: row.value, - isExclusive, - isContains, + dimension: row.column, + operator: row.operator, + value: row.value, }) } return } let processedValue = row.value - if (row.column === 'cc') { + if (row.column === 'country') { const alpha2 = countries.getAlpha2Code(row.value, language) if (alpha2) { processedValue = alpha2 } } + const { dimension, key } = parseRowColumn(row.column) + validFilters.push({ - column: row.column, - filter: processedValue, - isExclusive, - isContains, + dimension, + operator: row.operator, + value: processedValue === 'null' ? null : processedValue, + ...(key ? { key } : {}), }) }) @@ -311,14 +273,17 @@ const FilterRowsEditor = ({ >({}) const [loadingColumns, setLoadingColumns] = useState>(new Set()) const inFlightRef = useRef>(new Set()) - const { fetchFilters, fetchErrorsFilters, fetchVersionFilters } = - useFiltersProxy() - const panelOptions = filterOptions?.length - ? filterOptions - : type === 'traffic' - ? FILTERS_PANELS_ORDER - : ERRORS_FILTERS_PANELS_ORDER + const panelOptions = useMemo( + () => + filterOptions?.length + ? filterOptions + : VALID_DIMENSIONS_BY_TYPE[type].filter( + (dimension) => + !(KEYED_DIMENSIONS as readonly string[]).includes(dimension), + ), + [filterOptions, type], + ) const filters = useMemo( () => filterRowsToFilters(filterRows, language), @@ -327,16 +292,15 @@ const FilterRowsEditor = ({ const getColumnLabel = useCallback( (column: string) => { - if (column.startsWith('ev:key:')) { - return t('project.metamapping.ev.dynamicKey', { - key: column.replace(/^ev:key:/, ''), - }) - } - - if (column.startsWith('tag:key:')) { - return t('project.metamapping.tag.dynamicKey', { - key: column.replace(/^tag:key:/, ''), - }) + const { dimension, key } = parseRowColumn(column) + + if (key) { + return t( + `project.metamapping.${dimension === 'page_property' ? 'tag' : 'ev'}.dynamicKey`, + { + key, + }, + ) } return tnMapping[column] || t(`project.mapping.${column}`) @@ -359,11 +323,9 @@ const FilterRowsEditor = ({ const fetchFilterValues = useCallback( async (column: string) => { - if ( - (getMetadataColumnBase(column) !== column || column.includes(':')) && - !column.startsWith('ev:') && - !column.startsWith('tag:') - ) { + const { dimension, key } = parseRowColumn(column) + + if (key || !VALID_DIMENSIONS_BY_TYPE[type].includes(dimension)) { setFilterValuesCache((prev) => prev[column] ? prev : { ...prev, [column]: [] }, ) @@ -381,25 +343,12 @@ const FilterRowsEditor = ({ inFlightRef.current.add(column) setLoadingColumns((prev) => new Set(prev).add(column)) try { - let result: string[] = [] - - if (column === 'brv') { - const dataType = type === 'errors' ? 'errors' : 'traffic' - const pairs = await fetchVersionFilters(id, dataType, 'br') - if (pairs) { - result = pairs.map((p) => createVersionValue(p.name, p.version)) - } - } else if (column === 'osv') { - const dataType = type === 'errors' ? 'errors' : 'traffic' - const pairs = await fetchVersionFilters(id, dataType, 'os') - if (pairs) { - result = pairs.map((p) => createVersionValue(p.name, p.version)) - } - } else if (type === 'errors') { - result = (await fetchErrorsFilters(id, column)) || [] - } else { - result = (await fetchFilters(id, column)) || [] - } + const { data } = await getDimensionValues(id, dimension, { type }) + const result = (data || []).map((item) => + typeof item === 'string' + ? item + : createVersionValue(item.name, item.version), + ) setFilterValuesCache((prev) => ({ ...prev, [column]: result })) } catch (error) { console.error('Failed to fetch filter values:', error) @@ -413,15 +362,7 @@ const FilterRowsEditor = ({ }) } }, - [ - id, - type, - filterValuesCache, - loadingColumns, - fetchFilters, - fetchErrorsFilters, - fetchVersionFilters, - ], + [id, type, filterValuesCache, loadingColumns], ) useEffect(() => { @@ -711,47 +652,47 @@ const FilterRowsEditor = ({ {t('project.currentFilters')}
- {currentFilters.map( - ({ column, filter, isExclusive, isContains }) => { - let displayFilter = filter - if (column === 'cc') { - displayFilter = countries.getName(filter, language) || filter - } - const operatorLabel = isContains - ? isExclusive + {currentFilters.map((filter) => { + const { dimension, operator } = filter + const value = filterToUrlValue(filter) + + let displayFilter = value + if (dimension === 'country') { + displayFilter = countries.getName(value, language) || value + } + const operatorLabel = + operator === 'contains' + ? t('project.contains.is') + : operator === 'contains_not' ? t('project.contains.not') - : t('project.contains.is') - : isExclusive - ? t('common.isNot') - : t('common.is') - - return ( + : operator === 'is_not' + ? t('common.isNot') + : t('common.is') + + return ( + + + {getColumnLabel(filterToRowColumn(filter))} + - - {getColumnLabel(column)} - - - {operatorLabel} - - {displayFilter} + {operatorLabel} - ) - }, - )} + {displayFilter} + + ) + })}
) : null} diff --git a/web/app/pages/Project/View/components/Filters.tsx b/web/app/pages/Project/View/components/Filters.tsx index 4c8542f4d..a7ed71818 100644 --- a/web/app/pages/Project/View/components/Filters.tsx +++ b/web/app/pages/Project/View/components/Filters.tsx @@ -1,7 +1,5 @@ import _isEmpty from 'lodash/isEmpty' import _map from 'lodash/map' -import _replace from 'lodash/replace' -import _startsWith from 'lodash/startsWith' import _truncate from 'lodash/truncate' import { FunnelIcon, XIcon } from '@phosphor-icons/react' import { AnimatePresence, motion } from 'motion/react' @@ -10,35 +8,86 @@ import { useTranslation } from 'react-i18next' import { Link } from '~/ui/Link' import { useSearchParams } from 'react-router' +import { V2Filter, V2FilterOperator } from '~/api/v2/types' +import { + filterToUrlKey, + filterToUrlValue, + isFilterUrlKey, + parseFilterKey, +} from '~/utils/analyticsUrl' import { getLocaleDisplayName, cn } from '~/utils/generic' import countries from '~/utils/isoCountries' -import { isFilterValid } from '../utils/filters' import { splitProjectViewFiltersByTab } from '../utils/projectViewSegments' import { useViewProjectContext } from '../ViewProject' +const OPERATOR_CYCLE: Record = { + is: 'is_not', + is_not: 'contains', + contains: 'contains_not', + contains_not: 'is', +} + +const TOGGLE_TITLE_KEY: Record = { + is: 'toggleFilterToIsNot', + is_not: 'toggleFilterToContains', + contains: 'toggleFilterToNotContains', + contains_not: 'toggleFilterToIs', +} + +const operatorLabel = ( + t: (key: string) => string, + operator: V2FilterOperator, +) => { + if (operator === 'contains') { + return t('project.contains.is') + } + + if (operator === 'contains_not') { + return t('project.contains.not') + } + + return t(`common.${operator === 'is_not' ? 'isNot' : 'is'}`) +} + +const isFilterUrlEntry = ( + filter: V2Filter, + rawKey: string, + rawValue: string, +) => { + const parsed = parseFilterKey(rawKey) + + if ( + !parsed || + parsed.dimension !== filter.dimension || + parsed.key !== filter.key || + parsed.operator !== filter.operator + ) { + return false + } + + const normalisedValue = + rawValue === '' || rawValue === 'null' ? null : rawValue + + return normalisedValue === (filter.value ?? null) +} + interface FilterProps { - column: string - filter: string - isExclusive: boolean + filter: V2Filter tnMapping: Record canChangeExclusive?: boolean removable?: boolean onChangeExclusive?: (e: MouseEvent) => void onRemoveFilter?: (e: MouseEvent) => void - isContains?: boolean } const Filter = ({ - column, filter, - isExclusive, tnMapping, canChangeExclusive, removable, onChangeExclusive, onRemoveFilter, - isContains, }: FilterProps) => { const { dataLoading } = useViewProjectContext() const [searchParams] = useSearchParams() @@ -47,109 +96,91 @@ const Filter = ({ i18n: { language }, } = useTranslation('common') - let displayColumn = tnMapping[column] - let displayFilter = filter + const { dimension, operator, key } = filter + const value = Array.isArray(filter.value) + ? filter.value.map((item) => item ?? 'null').join(', ') + : filter.value + + let displayColumn = tnMapping[dimension] || dimension + let displayFilter: string = value ?? 'null' - if (column === 'cc') { - displayFilter = countries.getName(filter, language) as string + if (dimension === 'country' && value) { + displayFilter = countries.getName(value, language) || value } - if (column === 'pg') { - displayFilter = filter || t('common.notSet') + if (dimension === 'page') { + displayFilter = value || t('common.notSet') } - if (column === 'lc') { - displayFilter = getLocaleDisplayName(displayFilter, language) + if (dimension === 'locale' && value) { + displayFilter = getLocaleDisplayName(value, language) } // Normalise "null" filter values to human-friendly labels - if ( - (displayFilter as unknown as string)?.toString().toLowerCase() === 'null' - ) { - if (column === 'ref') { + if (value === null || value.toLowerCase() === 'null') { + if (dimension === 'referrer') { displayFilter = t('project.directNone') - } else if (column === 'host') { + } else if (dimension === 'host') { displayFilter = t('project.unknownHost') } else { displayFilter = t('common.notSet') } } - if (_startsWith(column, 'ev:key:')) { - const key = _replace(column, /^ev:key:/, '') - displayColumn = t('project.metamapping.ev.dynamicKey', { - key, - }) - } - - if (_startsWith(column, 'tag:key:')) { - const key = _replace(column, /^tag:key:/, '') - displayColumn = t('project.metamapping.tag.dynamicKey', { - key, - }) + if (key) { + displayColumn = t( + `project.metamapping.${dimension === 'page_property' ? 'tag' : 'ev'}.dynamicKey`, + { + key, + }, + ) } const truncatedFilter = _truncate(displayFilter) const createRemoveFilterPath = () => { - const newSearchParams = new URLSearchParams(searchParams.toString()) - const paramKeyInUrl = isContains - ? isExclusive - ? `^${column}` - : `~${column}` - : isExclusive - ? `!${column}` - : column - newSearchParams.delete(paramKeyInUrl, filter) + const newSearchParams = new URLSearchParams() + + for (const [rawKey, rawValue] of searchParams.entries()) { + if (isFilterUrlEntry(filter, rawKey, rawValue)) { + continue + } + + newSearchParams.append(rawKey, rawValue) + } + return { search: newSearchParams.toString() } } const createToggleExclusivePath = () => { - const newSearchParams = new URLSearchParams(searchParams.toString()) + const newSearchParams = new URLSearchParams() - const getKey = (contains: boolean, exclusive: boolean) => - contains - ? exclusive - ? `^${column}` - : `~${column}` - : exclusive - ? `!${column}` - : column - - const c = Boolean(isContains) - const e = Boolean(isExclusive) - - const oldParamKey = getKey(c, e) - - let nextContains = c - let nextExclusive = e - - if (!c && !e) { - // is -> is not - nextContains = false - nextExclusive = true - } else if (!c && e) { - // is not -> contains - nextContains = true - nextExclusive = false - } else if (c && !e) { - // contains -> not contains - nextContains = true - nextExclusive = true - } else { - // not contains -> is - nextContains = false - nextExclusive = false + for (const [rawKey, rawValue] of searchParams.entries()) { + if (isFilterUrlEntry(filter, rawKey, rawValue)) { + continue + } + + newSearchParams.append(rawKey, rawValue) } - const newParamKey = getKey(nextContains, nextExclusive) + const nextFilter: V2Filter = { + ...filter, + operator: OPERATOR_CYCLE[operator], + } - newSearchParams.delete(oldParamKey, filter) - newSearchParams.append(newParamKey, filter) + newSearchParams.append( + filterToUrlKey(nextFilter), + filterToUrlValue(nextFilter), + ) return { search: newSearchParams.toString() } } + const toggleLabel = t(`project.${TOGGLE_TITLE_KEY[operator]}`, { + column: displayColumn, + filter: truncatedFilter, + }) + return ( { if (dataLoading) { @@ -179,49 +210,13 @@ const Filter = ({ } onChangeExclusive?.(e) }} - title={(() => { - const c = Boolean(isContains) - const e = Boolean(isExclusive) - const nextKey = - !c && !e - ? 'toggleFilterToIsNot' - : !c && e - ? 'toggleFilterToContains' - : c && !e - ? 'toggleFilterToNotContains' - : 'toggleFilterToIs' - return t(`project.${nextKey}`, { - column: displayColumn, - filter: truncatedFilter, - }) - })()} - aria-label={(() => { - const c = Boolean(isContains) - const e = Boolean(isExclusive) - const nextKey = - !c && !e - ? 'toggleFilterToIsNot' - : !c && e - ? 'toggleFilterToContains' - : c && !e - ? 'toggleFilterToNotContains' - : 'toggleFilterToIs' - return t(`project.${nextKey}`, { - column: displayColumn, - filter: truncatedFilter, - }) - })()} + title={toggleLabel} + aria-label={toggleLabel} > - {isContains - ? t(`project.contains.${isExclusive ? 'not' : 'is'}`) - : t(`common.${isExclusive ? 'isNot' : 'is'}`)} + {operatorLabel(t, operator)} ) : ( - - {isContains - ? t(`project.contains.${isExclusive ? 'not' : 'is'}`) - : t(`common.${isExclusive ? 'isNot' : 'is'}`)} - + {operatorLabel(t, operator)} )}   @@ -269,6 +264,9 @@ interface FiltersProps { className?: string } +const getFilterKey = (filter: V2Filter) => + `${filterToUrlKey(filter)}-${filterToUrlValue(filter)}` + const Filters = ({ tnMapping, className }: FiltersProps) => { const { activeTab, dataLoading, filters } = useViewProjectContext() const { t } = useTranslation('common') @@ -284,12 +282,7 @@ const Filters = ({ tnMapping, className }: FiltersProps) => { const entries = Array.from(searchParams.entries()) for (const [key] of entries) { - let processedKey = key - if (key.startsWith('!') || key.startsWith('~') || key.startsWith('^')) { - processedKey = key.substring(1) - } - - if (!isFilterValid(processedKey, true)) { + if (!isFilterUrlKey(key)) { continue } @@ -314,47 +307,37 @@ const Filters = ({ tnMapping, className }: FiltersProps) => {
- {_map(supported, (props) => { - const { column, filter, isExclusive, isContains } = props - const key = `${column}-${filter}-${isExclusive}-${isContains}` - - return ( - - - - ) - })} - {_map(unsupported, (props) => { - const { column, filter, isExclusive, isContains } = props - const key = `unsupported-${column}-${filter}-${isExclusive}-${isContains}` - - return ( - - - - ) - })} + {_map(supported, (filter) => ( + + + + ))} + {_map(unsupported, (filter) => ( + + + + ))}
{unsupported.length > 0 ? ( diff --git a/web/app/pages/Project/View/components/LiveVisitorsDropdown.tsx b/web/app/pages/Project/View/components/LiveVisitorsDropdown.tsx index 3b58957ce..3fcafed16 100644 --- a/web/app/pages/Project/View/components/LiveVisitorsDropdown.tsx +++ b/web/app/pages/Project/View/components/LiveVisitorsDropdown.tsx @@ -6,8 +6,8 @@ import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router' import { Link } from '~/ui/Link' -import type { LiveVisitorInfo } from '~/api/api.server' -import { useLiveVisitorsProxy } from '~/hooks/useAnalyticsProxy' +import { getLiveVisitors as getLiveVisitorsV2 } from '~/api/v2/endpoints' +import type { LiveVisitor } from '~/api/v2/types' import { FLOW_TIMING, FLOW_VALUE_CLASS, @@ -29,8 +29,7 @@ const LiveVisitorsDropdown = () => { const location = useLocation() const [isDropdownVisible, setIsDropdownVisible] = useState(false) const [isLoading, setIsLoading] = useState(true) - const [liveInfo, setLiveInfo] = useState([]) - const { fetchLiveVisitorsInfo } = useLiveVisitorsProxy() + const [liveInfo, setLiveInfo] = useState([]) const flowLiveVisitors = useFlowValue(liveVisitors) // The translation interpolates the amount into the sentence, so split it @@ -45,11 +44,11 @@ const LiveVisitorsDropdown = () => { try { // Getting live sessions list and updating live visitors count to make sure it matches the list length - const [info] = await Promise.all([ - fetchLiveVisitorsInfo(id), + const [{ data }] = await Promise.all([ + getLiveVisitorsV2(id), updateLiveVisitors(), ]) - setLiveInfo(info || []) + setLiveInfo(data.visitors || []) } catch (reason) { console.error('[LiveVisitorsDropdown] getLiveVisitors:', reason) } @@ -138,7 +137,7 @@ const LiveVisitorsDropdown = () => { ) : (
- {_map(liveInfo, ({ psid, dv, br, os, cc }) => { + {_map(liveInfo, ({ psid, device, browser, os, country }) => { const params = new URLSearchParams(location.search) params.set('psid', psid) params.set('tab', PROJECT_TABS.sessions) @@ -152,15 +151,17 @@ const LiveVisitorsDropdown = () => {
{os}
-
{br}
-
{dv}
+
{browser}
+
+ {device} +
) })} diff --git a/web/app/pages/Project/View/components/NoEvents.tsx b/web/app/pages/Project/View/components/NoEvents.tsx index 90ad55c78..d61cd8a25 100644 --- a/web/app/pages/Project/View/components/NoEvents.tsx +++ b/web/app/pages/Project/View/components/NoEvents.tsx @@ -3,15 +3,15 @@ import _isEmpty from 'lodash/isEmpty' import { memo } from 'react' import { useTranslation } from 'react-i18next' +import { V2Filter } from '~/api/v2/types' import { Text } from '~/ui/Text' -import { Filter } from '../interfaces/traffic' import { typeNameMapping } from '../ViewProject.helpers' import Filters from './Filters' interface NoEventsProps { - filters: Filter[] + filters: V2Filter[] } const NoEvents = ({ filters }: NoEventsProps) => { diff --git a/web/app/pages/Project/View/components/ProjectViewHeaderActions.tsx b/web/app/pages/Project/View/components/ProjectViewHeaderActions.tsx index d5683a362..8e657e069 100644 --- a/web/app/pages/Project/View/components/ProjectViewHeaderActions.tsx +++ b/web/app/pages/Project/View/components/ProjectViewHeaderActions.tsx @@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next' import { useFetcher, useSearchParams } from 'react-router' import { toast } from 'sonner' +import { V2Filter } from '~/api/v2/types' import { PROJECT_TABS } from '~/lib/constants' import { useCurrentProject } from '~/providers/CurrentProjectProvider' import { ProjectViewActionData } from '~/routes/projects.$id' @@ -17,8 +18,9 @@ import Dropdown from '~/ui/Dropdown' import { Text } from '~/ui/Text' import { trackCustom } from '~/utils/analytics' -import { Filter, ProjectView } from '../interfaces/traffic' +import { ProjectView } from '../interfaces/traffic' import { + projectViewFiltersToV2, splitProjectViewFiltersByTab, supportsProjectViewSegments, } from '../utils/projectViewSegments' @@ -57,14 +59,6 @@ const isAddViewItem = (item: ProjectViewMenuItem): item is AddViewItem => const isEmptyViewItem = (item: ProjectViewMenuItem): item is EmptyViewItem => 'notClickable' in item -const getFilterSearchParamKey = (filter: Filter) => { - if (filter.isContains) { - return filter.isExclusive ? `^${filter.column}` : `~${filter.column}` - } - - return filter.isExclusive ? `!${filter.column}` : filter.column -} - const iconButtonClassName = 'rounded-md p-1 hover:bg-gray-50 hover:text-gray-900 dark:hover:bg-slate-900 dark:hover:text-slate-200' @@ -137,11 +131,20 @@ const ProjectViewHeaderActions = ({ } }, [deleteFetcher.state, deleteFetcher.data, loadProjectViews, t]) - const getFilterColumnLabel = (column: string) => { + const getFilterColumnLabel = (filter: V2Filter) => { + if (filter.key) { + return t( + `project.metamapping.${filter.dimension === 'page_property' ? 'tag' : 'ev'}.dynamicKey`, + { + key: filter.key, + }, + ) + } + return ( - tnMapping[column] || - t(`project.mapping.${column}`, { - defaultValue: column, + tnMapping[filter.dimension] || + t(`project.mapping.${filter.dimension}`, { + defaultValue: filter.dimension, }) ) } @@ -167,7 +170,7 @@ const ProjectViewHeaderActions = ({ } const { supported, unsupported } = splitProjectViewFiltersByTab( - item.filters || [], + projectViewFiltersToV2(item), activeTab, ) const hasTrafficMetrics = (item.customEvents?.length || 0) > 0 @@ -177,7 +180,7 @@ const ProjectViewHeaderActions = ({ supported.length > 0 || (activeTab === PROJECT_TABS.traffic && hasTrafficMetrics) const unsupportedTitle = unsupported - .map(({ column }) => getFilterColumnLabel(column)) + .map(getFilterColumnLabel) .join(', ') return ( @@ -274,7 +277,7 @@ const ProjectViewHeaderActions = ({ } const { supported } = splitProjectViewFiltersByTab( - item.filters || [], + projectViewFiltersToV2(item), activeTab, ) const hasSupportedFilters = supported.length > 0 @@ -288,28 +291,10 @@ const ProjectViewHeaderActions = ({ } let newParams = new URLSearchParams(searchParams) - const supportedFilterKeys = new Set( - supported.map(getFilterSearchParamKey), - ) - - filters.forEach((filter) => { - const key = getFilterSearchParamKey(filter) - - if (!supportedFilterKeys.has(key)) { - newParams.delete(key) - } - }) newParams.delete('metrics') - if (hasSupportedFilters) { - newParams = getFiltersUrlParams( - filters, - supported, - true, - newParams, - ) - } + newParams = getFiltersUrlParams(filters, supported, true, newParams) if (hasTrafficMetrics) { newParams.set('metrics', JSON.stringify(item.customEvents)) diff --git a/web/app/pages/Project/View/components/SearchFilters.tsx b/web/app/pages/Project/View/components/SearchFilters.tsx index f4572e6c1..e7cffccba 100644 --- a/web/app/pages/Project/View/components/SearchFilters.tsx +++ b/web/app/pages/Project/View/components/SearchFilters.tsx @@ -3,35 +3,42 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate, useSearchParams } from 'react-router' +import { V2Filter } from '~/api/v2/types' import Modal from '~/ui/Modal' - -import { Filter as FilterType } from '../interfaces/traffic' -import { isFilterValid } from '../utils/filters' +import { + filterToUrlKey, + filterToUrlValue, + isFilterUrlKey, +} from '~/utils/analyticsUrl' import FilterRowsEditor from './FilterRowsEditor' export const getFiltersUrlParams = ( - filters: FilterType[], - newFilters: FilterType[], + filters: V2Filter[], + newFilters: V2Filter[], override: boolean, searchParams: URLSearchParams, ) => { const currentUrlParams = new URLSearchParams(searchParams.toString()) - const resultingFilters: FilterType[] = [] + const resultingFilters: V2Filter[] = [] if (override) { resultingFilters.push(...newFilters) } else { - const finalUniqueFiltersMap = new Map() + const finalUniqueFiltersMap = new Map() newFilters.forEach((filter) => { - const mapKey = `${filter.isExclusive ? '!' : ''}${filter.isContains ? '~' : ''}${filter.column}=${filter.filter}` - finalUniqueFiltersMap.set(mapKey, filter) + finalUniqueFiltersMap.set( + `${filterToUrlKey(filter)}=${filterToUrlValue(filter)}`, + filter, + ) }) filters.forEach((filter) => { - const mapKey = `${filter.isExclusive ? '!' : ''}${filter.isContains ? '~' : ''}${filter.column}=${filter.filter}` - finalUniqueFiltersMap.set(mapKey, filter) + finalUniqueFiltersMap.set( + `${filterToUrlKey(filter)}=${filterToUrlValue(filter)}`, + filter, + ) }) resultingFilters.push(...Array.from(finalUniqueFiltersMap.values())) @@ -40,21 +47,13 @@ export const getFiltersUrlParams = ( const newUrlParams = new URLSearchParams() for (const [key, value] of currentUrlParams.entries()) { - let processedKey = key - if (key.startsWith('!') || key.startsWith('~') || key.startsWith('^')) { - processedKey = key.substring(1) - } - if (!isFilterValid(processedKey, true)) { + if (!isFilterUrlKey(key)) { newUrlParams.append(key, value) } } resultingFilters.forEach((filter) => { - let filterKey = filter.isExclusive ? `!${filter.column}` : filter.column - if (filter.isContains) { - filterKey = filter.isExclusive ? `^${filter.column}` : `~${filter.column}` - } - newUrlParams.append(filterKey, filter.filter) + newUrlParams.append(filterToUrlKey(filter), filterToUrlValue(filter)) }) return newUrlParams @@ -64,7 +63,7 @@ interface SearchFiltersProps { showModal: boolean setShowModal: (show: boolean) => void tnMapping: Record - filters: FilterType[] + filters: V2Filter[] type: 'traffic' | 'errors' filterOptions?: string[] } @@ -78,7 +77,7 @@ const SearchFilters = ({ filterOptions, }: SearchFiltersProps) => { const { t } = useTranslation('common') - const [draftFilters, setDraftFilters] = useState([]) + const [draftFilters, setDraftFilters] = useState([]) const [searchParams] = useSearchParams() const navigate = useNavigate() diff --git a/web/app/pages/Project/View/interfaces/traffic.ts b/web/app/pages/Project/View/interfaces/traffic.ts index 309284007..2311213ad 100644 --- a/web/app/pages/Project/View/interfaces/traffic.ts +++ b/web/app/pages/Project/View/interfaces/traffic.ts @@ -1,6 +1,7 @@ import { TimeBucket } from '~/lib/constants' +import { V2Filter } from '~/api/v2/types' -export interface Filter { +interface Filter { column: string filter: string isExclusive: boolean @@ -26,7 +27,7 @@ export interface ProjectViewCustomEvent { export interface ProjectView { id: string name: string - filters?: Filter[] + filters?: (Filter | V2Filter)[] customEvents?: ProjectViewCustomEvent[] } @@ -35,18 +36,18 @@ interface Param { count: number } -export type Params = Record +type Params = Record export type Customs = Record -export type Properties = Record +type Properties = Record interface Metric { sum: number avg: number } -export interface TrafficMeta { +interface TrafficMeta { key: string current: Metric previous: Metric diff --git a/web/app/pages/Project/View/utils/filters.tsx b/web/app/pages/Project/View/utils/filters.tsx index 1d258d833..fa0291664 100644 --- a/web/app/pages/Project/View/utils/filters.tsx +++ b/web/app/pages/Project/View/utils/filters.tsx @@ -1,11 +1,3 @@ -import _includes from 'lodash/includes' -import _keys from 'lodash/keys' -import _reduce from 'lodash/reduce' -import _some from 'lodash/some' -import _startsWith from 'lodash/startsWith' - -import { Filter } from '../interfaces/traffic' - export const ERROR_FILTERS_MAPPING = { showResolved: 'showResolved', } @@ -16,102 +8,3 @@ export const FILTER_CHART_METRICS_MAPPING_FOR_COMPARE = [ 'trendlines', 'customEvents', ] - -const validFilters = [ - 'host', - 'cc', - 'rg', - 'ct', - 'pg', - 'entryPage', - 'exitPage', - 'lc', - 'ref', - 'refn', - 'dv', - 'br', - 'brv', - 'os', - 'osv', - 'so', - 'me', - 'ca', - 'te', - 'co', - 'isp', - 'og', - 'ut', - 'ctp', - 'ev', - 'keywords', - 'tag:key', - 'tag:value', - 'ev:key', - 'ev:value', -] -// dynamic filters is when a filter column starts with a specific value and is followed by some arbitrary string -// this is done to build a connnection between dynamic column and value (e.g. for custom event metadata or page properties) -const validDynamicFilters = ['ev:key:', 'tag:key:'] - -export const isFilterValid = (filter: string, checkDynamicFilters = false) => { - // normalise by removing operator prefixes from the URL key - let normalised = filter - if ( - _startsWith(normalised, '!') || - _startsWith(normalised, '~') || - _startsWith(normalised, '^') - ) { - normalised = normalised.substring(1) - } - - if (_includes(validFilters, normalised)) { - return true - } - - if ( - checkDynamicFilters && - _some(validDynamicFilters, (prefix) => _startsWith(normalised, prefix)) - ) { - return true - } - - return false -} - -export const parseFilters = (searchParams: URLSearchParams): Filter[] => { - const filters: Filter[] = [] - - const entries = Array.from(searchParams.entries()) - - for (const [key, value] of entries) { - let actualColumn = key - - let isExclusive = false - let isContains = false - - if (key.startsWith('!')) { - isExclusive = true - actualColumn = key.substring(1) - } else if (key.startsWith('~')) { - isContains = true - actualColumn = key.substring(1) - } else if (key.startsWith('^')) { - isExclusive = true - isContains = true - actualColumn = key.substring(1) - } - - if (!isFilterValid(actualColumn, true)) { - continue - } - - filters.push({ - column: actualColumn, - filter: value, - isExclusive: isExclusive, - isContains, - }) - } - - return filters -} diff --git a/web/app/pages/Project/View/utils/projectViewSegments.ts b/web/app/pages/Project/View/utils/projectViewSegments.ts index 436c7b256..17ae0adf4 100644 --- a/web/app/pages/Project/View/utils/projectViewSegments.ts +++ b/web/app/pages/Project/View/utils/projectViewSegments.ts @@ -1,54 +1,42 @@ -import { - ERRORS_FILTERS_PANELS_ORDER, - FILTERS_PANELS_ORDER, - PROJECT_TABS, -} from '~/lib/constants' +import { V2Filter } from '~/api/v2/types' +import { PROJECT_TABS } from '~/lib/constants' +import { VALID_DIMENSIONS_BY_TYPE } from '~/lib/v2Dimensions' +import { legacyFilterToV2 } from '~/utils/analyticsUrl' -import { Filter } from '../interfaces/traffic' +import { ProjectView } from '../interfaces/traffic' type ProjectTab = keyof typeof PROJECT_TABS -const TRAFFIC_FILTER_OPTIONS = FILTERS_PANELS_ORDER.includes('ev') - ? FILTERS_PANELS_ORDER - : FILTERS_PANELS_ORDER.flatMap((column) => - column === 'exitPage' ? [column, 'ev'] : [column], - ) - -const TRAFFIC_FILTER_COLUMNS = [ - ...TRAFFIC_FILTER_OPTIONS, - 'refn', - 'ev:key', - 'tag:key', - 'tag:value', -] - -const PERFORMANCE_FILTER_COLUMNS = [ - 'pg', - 'host', - 'cc', - 'rg', - 'ct', - 'dv', - 'br', - 'brv', - 'isp', - 'og', - 'ut', - 'ctp', -] - -const ERRORS_FILTER_COLUMNS = [ - 'host', - ...ERRORS_FILTERS_PANELS_ORDER, - 'entryPage', - 'exitPage', - 'tag:key', - 'tag:value', -] - -const CAPTCHA_FILTER_COLUMNS = ['cc', 'br', 'os', 'dv'] - -const SEO_FILTER_COLUMNS = ['pg', 'keywords', 'cc', 'dv'] +export const projectViewFiltersToV2 = (view?: ProjectView): V2Filter[] => + (view?.filters || []).flatMap((filter) => { + if ('dimension' in filter) { + return [filter] + } + const converted = legacyFilterToV2(filter) + return converted ? [converted] : [] + }) + +const KEYED_FILTER_DIMENSIONS = ['event_metadata', 'page_property'] + +const HIDDEN_FILTER_OPTIONS = [...KEYED_FILTER_DIMENSIONS, 'referrer_name'] + +const TRAFFIC_FILTER_OPTIONS = VALID_DIMENSIONS_BY_TYPE.traffic.filter( + (dimension) => !HIDDEN_FILTER_OPTIONS.includes(dimension), +) + +const TRAFFIC_FILTER_COLUMNS = VALID_DIMENSIONS_BY_TYPE.traffic + +const PERFORMANCE_FILTER_COLUMNS = VALID_DIMENSIONS_BY_TYPE.performance + +const ERRORS_FILTER_OPTIONS = VALID_DIMENSIONS_BY_TYPE.errors.filter( + (dimension) => !HIDDEN_FILTER_OPTIONS.includes(dimension), +) + +const ERRORS_FILTER_COLUMNS = VALID_DIMENSIONS_BY_TYPE.errors + +const CAPTCHA_FILTER_COLUMNS = VALID_DIMENSIONS_BY_TYPE.captcha + +const SEO_FILTER_COLUMNS = VALID_DIMENSIONS_BY_TYPE.seo const ANALYTICS_TABS = new Set([ PROJECT_TABS.traffic, @@ -63,23 +51,26 @@ const ANALYTICS_TABS = new Set([ const FILTER_OPTIONS_BY_TAB: Partial> = { [PROJECT_TABS.traffic]: TRAFFIC_FILTER_OPTIONS, [PROJECT_TABS.performance]: PERFORMANCE_FILTER_COLUMNS, - [PROJECT_TABS.seo]: ['pg', 'cc', 'dv'], + [PROJECT_TABS.seo]: SEO_FILTER_COLUMNS, [PROJECT_TABS.sessions]: TRAFFIC_FILTER_OPTIONS, [PROJECT_TABS.replays]: TRAFFIC_FILTER_OPTIONS, [PROJECT_TABS.profiles]: TRAFFIC_FILTER_OPTIONS, [PROJECT_TABS.funnels]: TRAFFIC_FILTER_OPTIONS, [PROJECT_TABS.goals]: TRAFFIC_FILTER_OPTIONS, [PROJECT_TABS.experiments]: TRAFFIC_FILTER_OPTIONS, - [PROJECT_TABS.errors]: ['host', ...ERRORS_FILTERS_PANELS_ORDER], + [PROJECT_TABS.errors]: ERRORS_FILTER_OPTIONS, [PROJECT_TABS.captcha]: CAPTCHA_FILTER_COLUMNS, } -const supportsDynamicFilter = (column: string, tab: ProjectTab) => { - if (column.startsWith('ev:key:')) { +const supportsKeyedFilter = ( + filter: Pick, + tab: ProjectTab, +) => { + if (filter.dimension === 'event_metadata') { return ANALYTICS_TABS.has(tab) } - if (column.startsWith('tag:key:')) { + if (filter.dimension === 'page_property') { return ANALYTICS_TABS.has(tab) || tab === PROJECT_TABS.errors } @@ -111,22 +102,22 @@ const getSupportedColumns = (tab: ProjectTab) => { } const isProjectTabFilterSupported = ( - filter: Pick, + filter: Pick, tab: ProjectTab, ) => { - if (supportsDynamicFilter(filter.column, tab)) { - return true + if (filter.key) { + return supportsKeyedFilter(filter, tab) } - return getSupportedColumns(tab).includes(filter.column) + return getSupportedColumns(tab).includes(filter.dimension) } export const splitProjectViewFiltersByTab = ( - filters: Filter[] = [], + filters: V2Filter[] = [], tab: ProjectTab, ) => { - const supported: Filter[] = [] - const unsupported: Filter[] = [] + const supported: V2Filter[] = [] + const unsupported: V2Filter[] = [] filters.forEach((filter) => { if (isProjectTabFilterSupported(filter, tab)) { diff --git a/web/app/pages/Project/View/v2/BreakdownPanel.tsx b/web/app/pages/Project/View/v2/BreakdownPanel.tsx new file mode 100644 index 000000000..d4f1f2069 --- /dev/null +++ b/web/app/pages/Project/View/v2/BreakdownPanel.tsx @@ -0,0 +1,256 @@ +import { ArrowsClockwiseIcon, WarningOctagonIcon } from '@phosphor-icons/react' +import { useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { LinkProps } from 'react-router' + +import { V2DataType } from '~/api/v2/types' +import { useBreakdownDetailsQuery } from '~/hooks/v2/useV2Queries' +import { useInViewOnce } from '~/hooks/useInViewOnce' +import { Entry } from '~/lib/models/Entry' +import { useViewProjectContext } from '~/pages/Project/View/ViewProject' +import Button from '~/ui/Button' +import { Text } from '~/ui/Text' +import { getItem, setItem } from '~/utils/localstorage' + +import { Panel, PanelContainer, type PanelProps } from '../Panels' +import { groupVersionRows, mapBreakdownRows } from './adapters' + +export interface BreakdownSubTab { + id: string + label: string + dimension?: string + render?: () => React.ReactNode + versionsDimension?: 'browser_version' | 'os_version' + versionsParentField?: 'browser' | 'os' +} + +interface BreakdownPanelProps { + dataType: V2DataType + panelId: string + name: string + icon: React.ReactNode + subTabs: (BreakdownSubTab | BreakdownSubTab[])[] + primaryMetric?: string + metrics?: string[] + measure?: string + sort?: string + rowMapper?: (entry: Entry, subTabId: string) => React.ReactNode + valueMapper?: (value: number, subTabId: string) => number + transformEntries?: (entries: Entry[], subTabId: string) => Entry[] + capitalize?: string[] + getFilterLink?: ( + dimension: string, + value: string | null, + subTabId: string, + ) => LinkProps['to'] + getVersionFilterLink?: ( + parent: string | null, + version: string | null, + subTabId: string, + ) => LinkProps['to'] | string + valuesHeaderName?: string + highlightColour?: 'blue' | 'red' | 'orange' + onActiveSubTabChange?: (subTabId: string) => void + detailsExtraColumns?: PanelProps['detailsExtraColumns'] + rowTooltipRenderer?: PanelProps['rowTooltipRenderer'] + rowTooltipFollowCursor?: boolean + hidePercentageInDetails?: boolean + disableRowClick?: boolean +} + +const subTabStorageKey = (dataType: string, panelId: string) => + `v2PanelTab:${dataType}:${panelId}` + +export const BreakdownPanel = ({ + dataType, + panelId, + name, + icon, + subTabs, + primaryMetric = 'visitors', + metrics, + measure, + sort, + rowMapper, + valueMapper, + transformEntries, + capitalize, + getFilterLink: getFilterLinkProp, + getVersionFilterLink, + valuesHeaderName, + highlightColour, + onActiveSubTabChange, + detailsExtraColumns, + rowTooltipRenderer, + rowTooltipFollowCursor, + hidePercentageInDetails, + disableRowClick, +}: BreakdownPanelProps) => { + const { t } = useTranslation('common') + const { getFilterLink: getFilterLinkContext } = useViewProjectContext() + const { ref, hasBeenInView } = useInViewOnce() + + const flatSubTabs = useMemo(() => subTabs.flat(), [subTabs]) + + const [activeSubTabId, setActiveSubTabId] = useState(() => { + const stored = getItem(subTabStorageKey(dataType, panelId)) + if (stored && flatSubTabs.some((tab) => tab.id === stored)) { + return stored as string + } + return flatSubTabs[0].id + }) + + const activeSubTab = + flatSubTabs.find((tab) => tab.id === activeSubTabId) || flatSubTabs[0] + + const onSubTabChange = (tabId: string) => { + setActiveSubTabId(tabId) + setItem(subTabStorageKey(dataType, panelId), tabId) + onActiveSubTabChange?.(tabId) + } + + const isSentinel = !activeSubTab.dimension + + const resolvedSort = sort ?? `${primaryMetric}:desc` + + const query = useBreakdownDetailsQuery(dataType, { + dimension: activeSubTab.dimension || '', + metrics, + measure, + sort: resolvedSort, + enabled: hasBeenInView && !isSentinel, + }) + + const versionsQuery = useBreakdownDetailsQuery(dataType, { + dimension: activeSubTab.versionsDimension || '', + metrics, + measure, + sort: resolvedSort, + enabled: hasBeenInView && Boolean(activeSubTab.versionsDimension), + }) + + const entries = useMemo(() => { + const mapped = mapBreakdownRows( + query.data?.pages.flatMap((page) => page.data), + primaryMetric, + metrics, + ) + return transformEntries ? transformEntries(mapped, activeSubTabId) : mapped + }, [query.data, primaryMetric, metrics, transformEntries, activeSubTabId]) + + const versionData = useMemo(() => { + if (!activeSubTab.versionsDimension || !versionsQuery.data) { + return undefined + } + + return groupVersionRows( + versionsQuery.data.pages.flatMap((page) => page.data), + activeSubTab.versionsParentField || 'browser', + primaryMetric, + ) + }, [activeSubTab, versionsQuery.data, primaryMetric]) + + const total = query.data?.pages[0]?.meta.total + + const containerTabs = + flatSubTabs.length > 1 + ? subTabs.map((tab) => + Array.isArray(tab) + ? tab.map(({ id, label }) => ({ id, label })) + : { id: tab.id, label: tab.label }, + ) + : undefined + + if (isSentinel) { + return ( +
+ + {activeSubTab.render?.()} + +
+ ) + } + + // Panels below the fold only start fetching once scrolled into view, which + // leaves the query pending rather than loading — both mean "no rows yet". + const isLoading = query.isLoading || (!hasBeenInView && !query.data) + + const renderError = () => ( +
+
+ +
+ + {t('apiNotifications.somethingWentWrong')} + + +
+ ) + + return ( +
+ rowMapper(entry, activeSubTabId) : undefined + } + valueMapper={ + valueMapper + ? (value) => valueMapper(value, activeSubTabId) + : undefined + } + capitalize={capitalize?.includes(activeSubTabId)} + getFilterLink={(dimension, value) => + getFilterLinkProp + ? getFilterLinkProp(dimension, value, activeSubTabId) + : getFilterLinkContext(dimension, value) + } + versionData={versionData} + getVersionFilterLink={ + getVersionFilterLink + ? (parent, version) => + getVersionFilterLink(parent, version, activeSubTabId) + : undefined + } + valuesHeaderName={valuesHeaderName} + highlightColour={highlightColour} + detailsExtraColumns={detailsExtraColumns} + rowTooltipRenderer={rowTooltipRenderer} + rowTooltipFollowCursor={rowTooltipFollowCursor} + hidePercentageInDetails={hidePercentageInDetails} + disableRowClick={disableRowClick} + dataLoading={query.isFetching && !query.isFetchingNextPage} + isRefetching={ + query.isFetching && !query.isLoading && !query.isFetchingNextPage + } + onLoadMore={query.hasNextPage ? () => query.fetchNextPage() : undefined} + loadingMore={query.isFetchingNextPage} + /> +
+ ) +} diff --git a/web/app/pages/Project/View/v2/TrafficMap.tsx b/web/app/pages/Project/View/v2/TrafficMap.tsx new file mode 100644 index 000000000..d0656be37 --- /dev/null +++ b/web/app/pages/Project/View/v2/TrafficMap.tsx @@ -0,0 +1,62 @@ +import { Suspense, lazy, useMemo } from 'react' +import { useNavigate } from 'react-router' + +import { useBreakdownQuery } from '~/hooks/v2/useV2Queries' +import { MapLoader } from '~/pages/Project/View/components/MapLoader' +import { useViewProjectContext } from '~/pages/Project/View/ViewProject' + +import { mapBreakdownRows } from './adapters' + +const InteractiveMap = lazy( + () => import('~/pages/Project/View/components/InteractiveMap'), +) + +const MAP_CLICK_TYPE_TO_DIMENSION = { cc: 'country', rg: 'region' } as const + +export const TrafficMap = ({ isFullscreen }: { isFullscreen: boolean }) => { + const navigate = useNavigate() + const { getFilterLink, setIsMapFullscreen } = useViewProjectContext() + + const countryQuery = useBreakdownQuery('traffic', { + dimension: 'country', + limit: 100, + sort: 'visitors:desc', + }) + const regionQuery = useBreakdownQuery('traffic', { + dimension: 'region', + limit: 100, + sort: 'visitors:desc', + }) + + const countryData = useMemo( + () => mapBreakdownRows(countryQuery.data?.data), + [countryQuery.data], + ) + const regionData = useMemo( + () => mapBreakdownRows(regionQuery.data?.data), + [regionQuery.data], + ) + const total = useMemo( + () => countryData.reduce((acc, curr) => acc + curr.count, 0), + [countryData], + ) + + if (countryQuery.isLoading) { + return + } + + return ( + }> + { + navigate(getFilterLink(MAP_CLICK_TYPE_TO_DIMENSION[mapType], key)) + }} + onFullscreenToggle={setIsMapFullscreen} + isFullscreen={isFullscreen} + /> + + ) +} diff --git a/web/app/pages/Project/View/v2/adapters.ts b/web/app/pages/Project/View/v2/adapters.ts new file mode 100644 index 000000000..a3a890b1e --- /dev/null +++ b/web/app/pages/Project/View/v2/adapters.ts @@ -0,0 +1,146 @@ +import { + BreakdownRow, + MetadataRow, + TimeseriesRow, + TrafficSummaryData, + V2Filter, +} from '~/api/v2/types' +import { Entry } from '~/lib/models/Entry' +import { OverallObject } from '~/lib/models/Project' + +export const mapBreakdownRows = ( + rows: BreakdownRow[] | undefined, + primaryMetric = 'visitors', + // Metrics beyond the primary one are copied onto the entry so panels can show + // them in extra columns or row tooltips (the SEO panels lean on this). + extraMetrics?: string[], +): Entry[] => { + if (!rows) return [] + + return rows.map((row) => ({ + name: row.value, + count: Number(row[primaryMetric] ?? 0), + ...(typeof row.country === 'string' ? { cc: row.country } : {}), + ...(typeof row.region_code === 'string' ? { rgc: row.region_code } : {}), + ...Object.fromEntries( + (extraMetrics ?? []) + .filter((metric) => metric !== primaryMetric) + .map((metric) => [metric, Number(row[metric] ?? 0)]), + ), + })) +} + +export const groupVersionRows = ( + rows: BreakdownRow[] | undefined, + parentField: 'browser' | 'os', + primaryMetric = 'visitors', +): Record => { + const grouped: Record = {} + + for (const row of rows || []) { + const parent = row[parentField] + if (typeof parent !== 'string' || !parent) continue + + if (!grouped[parent]) { + grouped[parent] = [] + } + + grouped[parent].push({ + name: row.value, + count: Number(row[primaryMetric] ?? 0), + }) + } + + return grouped +} + +export const metadataToResult = (rows: MetadataRow[] | undefined) => ({ + result: rows || [], +}) + +export const hasCustomEventFilter = (filters: V2Filter[]): boolean => + filters.some( + (filter) => + filter.dimension === 'event' || filter.dimension === 'event_metadata', + ) + +export const summaryToOverall = ( + summary: TrafficSummaryData | undefined, + customEVFilterApplied = false, +): Partial => { + if (!summary) return {} + + const mapPeriod = (period: TrafficSummaryData['current']) => ({ + unique: period.visitors, + all: period.pageviews, + users: period.users, + bounceRate: period.bounce_rate, + sdur: period.session_duration, + }) + + return { + current: mapPeriod(summary.current), + previous: mapPeriod(summary.previous), + change: summary.change.pageviews, + uniqueChange: summary.change.visitors, + usersChange: summary.change.users, + bounceRateChange: summary.change.bounce_rate, + sdurChange: summary.change.session_duration, + customEVFilterApplied, + } +} + +const toChartX = (timestamp: string): string => + timestamp.slice(0, 19).replace('T', ' ') + +export interface TrafficChartData { + x: string[] + visits: number[] + uniques: number[] + sdur: number[] + bounces: number[] + concurrency: number[] + [key: string]: number[] | string[] +} + +export const pivotTrafficTimeseries = ( + rows: TimeseriesRow[] | undefined, +): TrafficChartData => { + const x: string[] = [] + const visits: number[] = [] + const uniques: number[] = [] + const sdur: number[] = [] + const bounces: number[] = [] + const concurrency: number[] = [] + + for (const row of rows || []) { + const rowUniques = Number(row.visitors ?? 0) + x.push(toChartX(row.timestamp)) + uniques.push(rowUniques) + visits.push(Number(row.pageviews ?? 0)) + sdur.push(Number(row.session_duration ?? 0)) + bounces.push((Number(row.bounce_rate ?? 0) * rowUniques) / 100) + concurrency.push(Number(row.concurrency ?? 0)) + } + + return { x, visits, uniques, sdur, bounces, concurrency } +} + +export const pivotCustomEventsTimeseries = ( + rows: TimeseriesRow[] | undefined, + events: string[], +): { x: string[]; events: Record } => { + const x: string[] = [] + const series: Record = Object.fromEntries( + events.map((event) => [event, []]), + ) + + for (const row of rows || []) { + x.push(toChartX(row.timestamp)) + for (const event of events) { + series[event].push(Number(row[event] ?? 0)) + } + } + + return { x, events: series } +} diff --git a/web/app/pages/Project/View/v2/loading.tsx b/web/app/pages/Project/View/v2/loading.tsx new file mode 100644 index 000000000..4ba496957 --- /dev/null +++ b/web/app/pages/Project/View/v2/loading.tsx @@ -0,0 +1,27 @@ +import cx from 'clsx' + +export const RefetchIndicator = ({ className }: { className?: string }) => ( +
+
+
+
+) diff --git a/web/app/pages/Project/tabs/Captcha/CaptchaView.tsx b/web/app/pages/Project/tabs/Captcha/CaptchaView.tsx index 9cc44e99c..aa11bf056 100644 --- a/web/app/pages/Project/tabs/Captcha/CaptchaView.tsx +++ b/web/app/pages/Project/tabs/Captcha/CaptchaView.tsx @@ -1,4 +1,3 @@ -import cx from 'clsx' import _isEmpty from 'lodash/isEmpty' import _map from 'lodash/map' import { @@ -6,64 +5,57 @@ import { MapPinIcon, MonitorIcon, DevicesIcon, - GlobeIcon, GaugeIcon, WarningIcon, ClockIcon, ShieldCheckIcon, } from '@phosphor-icons/react' -import { useState, useEffect, useMemo, useContext, useRef } from 'react' +import { useMemo, useRef, useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { useSearchParams } from 'react-router' -import { useCaptchaProxy } from '~/hooks/useAnalyticsProxy' -import useSize from '~/hooks/useSize' import { - chartTypes, - BROWSER_LOGO_MAP, - OS_LOGO_MAP, - OS_LOGO_MAP_DARK, -} from '~/lib/constants' + useCompareSummaryQuery, + useSummaryQuery, + useTimeseriesQuery, +} from '~/hooks/v2/useV2Queries' +import { chartTypes } from '~/lib/constants' +import { Entry } from '~/lib/models/Entry' import { MetricCard } from '~/pages/Project/tabs/Traffic/MetricCards' import { useCurrentProject } from '~/providers/CurrentProjectProvider' import { useTheme } from '~/providers/ThemeProvider' import Loader from '~/ui/Loader' -import LoadingBar from '~/ui/LoadingBar' +import { CompactNumberFlow, DurationFlow, PercentFlow } from '~/ui/NumberFlow' import { nFormatter } from '~/utils/generic' import CCRow from '../../View/components/CCRow' import DashboardHeader from '../../View/components/DashboardHeader' import Filters from '../../View/components/Filters' import ProjectViewHeaderActions from '../../View/components/ProjectViewHeaderActions' -import { Filter } from '../../View/interfaces/traffic' -import { Panel } from '../../View/Panels' -import { parseFilters } from '../../View/utils/filters' -import { - ViewProjectContext, - RefreshTriggersContext, -} from '../../View/ViewProject' -import { deviceIconMapping } from '../../View/ViewProject.helpers' +import { BreakdownPanel, BreakdownSubTab } from '../../View/v2/BreakdownPanel' +import { useViewProjectContext } from '../../View/ViewProject' +import { getDeviceRowMapper } from '../../View/ViewProject.helpers' +import { pivotCaptchaTimeseries } from './adapters' import { CaptchaChart } from './CaptchaChart' import NoCaptchaEvents from './NoCaptchaEvents' import WaitingForCaptchaEvent from './WaitingForCaptchaEvent' const PANELS_ORDER = [ - 'cc', - 'br', + 'country', + 'browser', 'os', - 'dv', + 'device', 'captcha_event', 'captcha_difficulty', 'solve_time', 'captcha_reason', -] +] as const const iconClassName = 'w-6 h-6' const panelIconMapping: Record = { - cc: , - dv: , - br: , + country: , + device: , + browser: , os: , captcha_event: , captcha_difficulty: , @@ -72,9 +64,9 @@ const panelIconMapping: Record = { } export const captchaTypeNameMapping = (t: any) => ({ - cc: t('project.mapping.cc'), - dv: t('project.mapping.dv'), - br: t('project.mapping.br'), + country: t('project.mapping.cc'), + device: t('project.mapping.dv'), + browser: t('project.mapping.br'), os: t('project.mapping.os'), captcha_event: t('project.mapping.captcha_event'), captcha_difficulty: t('project.mapping.captcha_difficulty'), @@ -86,45 +78,32 @@ interface CaptchaViewProps { projectId: string } -const CaptchaView = ({ projectId }: CaptchaViewProps) => { +const CaptchaView = (_props: CaptchaViewProps) => { const { theme } = useTheme() const { project } = useCurrentProject() - const { captchaRefreshTrigger } = useContext(RefreshTriggersContext) - const { - period, - timeBucket, - dateRange, - timeFormat, - size, - activePeriodCompare, - } = useContext(ViewProjectContext) - const [searchParams] = useSearchParams() - const isEmbedded = searchParams.get('embedded') === 'true' - const isMountedRef = useRef(true) - const captchaProxy = useCaptchaProxy() + const { timeBucket, timeFormat, filters, rotateXAxis } = + useViewProjectContext() const { t, i18n: { language }, } = useTranslation('common') - const [panelsData, setPanelsData] = useState({}) - const [chartData, setChartData] = useState<{ - x: string[] - results?: number[] - generated?: number[] - passed?: number[] - failed?: number[] - validationFailed?: number[] - replayed?: number[] - } | null>(null) - const [summaryData, setSummaryData] = useState(null) - const [summaryCompareData, setSummaryCompareData] = useState(null) - const [isPanelsDataEmpty, setIsPanelsDataEmpty] = useState(false) - const [analyticsLoading, setAnalyticsLoading] = useState(true) - const [dataLoading, setDataLoading] = useState(false) - - const rotateXAxis = useMemo(() => size.width > 0 && size.width < 500, [size]) + const hasShownContentRef = useRef(false) + + const summaryQuery = useSummaryQuery('captcha') + const compareSummaryQuery = useCompareSummaryQuery('captcha') + const timeseriesQuery = useTimeseriesQuery('captcha') + + const summary = summaryQuery.data?.data + const summaryData = summary?.current ?? null + const summaryCompareData = + compareSummaryQuery.data?.data.current ?? summary?.previous ?? null + + const chartData = useMemo( + () => pivotCaptchaTimeseries(timeseriesQuery.data?.data), + [timeseriesQuery.data], + ) const dataNames = useMemo( () => ({ @@ -138,149 +117,117 @@ const CaptchaView = ({ projectId }: CaptchaViewProps) => { [t], ) - const filters = useMemo(() => { - return parseFilters(searchParams) - }, [searchParams]) - const tnMapping = captchaTypeNameMapping(t) - const [ref] = useSize() as any - useEffect(() => { - isMountedRef.current = true - return () => { - isMountedRef.current = false - } - }, []) - - const getFormatDate = (date: Date) => { - const yyyy = date.getFullYear() - let mm: string | number = date.getMonth() + 1 - let dd: string | number = date.getDate() - if (dd < 10) dd = `0${dd}` - if (mm < 10) mm = `0${mm}` - return `${yyyy}-${mm}-${dd}` + const summaryLoaded = Boolean(summaryQuery.data) + + const isPanelsDataEmptyRaw = + summaryLoaded && !summaryData?.generated && !summaryData?.passed + + if (summaryLoaded && !isPanelsDataEmptyRaw) { + hasShownContentRef.current = true } - const loadCaptcha = async () => { - setDataLoading(true) + const isPanelsDataEmpty = isPanelsDataEmptyRaw && !hasShownContentRef.current + + // Queries keep previous data across period/filter changes, so `isLoading` only + // ever means "nothing cached to show yet" — exactly when a spinner is wanted. + const isChartLoading = summaryQuery.isLoading || timeseriesQuery.isLoading + + const panels = useMemo( + () => + _map(PANELS_ORDER, (dimension) => ({ + dimension, + name: tnMapping[dimension], + subTabs: [ + { id: dimension, label: tnMapping[dimension], dimension }, + ] as BreakdownSubTab[], + })), + // eslint-disable-next-line react-hooks/exhaustive-deps + [t], + ) - try { - let from = '' - let to = '' + const formatReason = useCallback( + (value?: string | null) => + value + ? t(`project.captchaAnalytics.reasons.${value}`, { + defaultValue: value, + }) + : 'N/A', + [t], + ) - if (dateRange) { - from = getFormatDate(dateRange[0]) - to = getFormatDate(dateRange[1]) + const formatDifficulty = useCallback( + (value?: string | null) => { + switch (Number(value)) { + case 2: + return t('project.settings.captcha.difficultyLevels.veryEasy') + case 3: + return t('project.settings.captcha.difficultyLevels.easy') + case 4: + return t('project.settings.captcha.difficultyLevels.medium') + case 5: + return t('project.settings.captcha.difficultyLevels.hard') + case 6: + return t('project.settings.captcha.difficultyLevels.veryHard') + default: + return t('project.captchaAnalytics.difficultyValue', { value }) } + }, + [t], + ) - const requestParams = { - timeBucket, - period: period === 'custom' && dateRange ? '' : period, - filters, - from: period === 'custom' && dateRange ? from : '', - to: period === 'custom' && dateRange ? to : '', - } - const compareFrom = searchParams.get('compareFrom') || '' - const compareTo = searchParams.get('compareTo') || '' - const compareEnabled = - searchParams.get('compare') === 'true' && compareFrom && compareTo - const shouldFetchCustomCompare = - compareEnabled && activePeriodCompare !== 'previous' - - const [data, compareData] = await Promise.all([ - captchaProxy.fetchCaptcha(projectId, requestParams), - shouldFetchCustomCompare - ? captchaProxy.fetchCaptcha(projectId, { - ...requestParams, - period: '', - from: compareFrom, - to: compareTo, - }) - : Promise.resolve(null), - ]) - - if (!isMountedRef.current) return - - if (_isEmpty(data)) { - setAnalyticsLoading(false) - setDataLoading(false) - setIsPanelsDataEmpty(true) - setChartData(null) - setSummaryData(null) - setSummaryCompareData(null) - return + const rowMapper = useCallback( + (entry: Entry, subTabId: string) => { + const { name: entryName, cc } = entry + + if (subTabId === 'country') { + if (cc !== undefined) { + return ( + + ) + } + return } - const { params, chart, summary, previousSummary } = data - - if (chart && chart.x) { - setChartData(chart) - } else { - setChartData(null) + if ( + subTabId === 'browser' || + subTabId === 'os' || + subTabId === 'device' + ) { + const v1Tab = + subTabId === 'browser' ? 'br' : subTabId === 'os' ? 'os' : 'dv' + const mapper = getDeviceRowMapper(v1Tab, theme, t) as + | ((entry: Entry) => React.ReactNode) + | undefined + return mapper ? mapper(entry) : entryName } - const hasData = - !_isEmpty(params) || !_isEmpty(chart?.x) || !_isEmpty(summary) - - setPanelsData({ - types: PANELS_ORDER, - data: params || {}, - }) - setIsPanelsDataEmpty(!hasData) - - setSummaryData(summary || null) - setSummaryCompareData( - shouldFetchCustomCompare - ? compareData?.summary || null - : previousSummary || null, - ) - setAnalyticsLoading(false) - setDataLoading(false) - } catch (reason) { - if (!isMountedRef.current) return - setAnalyticsLoading(false) - setDataLoading(false) - setIsPanelsDataEmpty(true) - setChartData(null) - setSummaryData(null) - setSummaryCompareData(null) - console.error('[ERROR](loadCaptcha) Loading captcha data failed:', reason) - } - } + if (subTabId === 'captcha_event') { + return t(`project.captchaAnalytics.events.${entryName}`, { + defaultValue: entryName ?? 'N/A', + }) + } - useEffect(() => { - loadCaptcha() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filters, dateRange, period, timeBucket, projectId, activePeriodCompare]) + if (subTabId === 'captcha_reason') { + return formatReason(entryName) + } - // Refresh captcha data when refresh button is clicked - useEffect(() => { - if (captchaRefreshTrigger > 0) { - loadCaptcha() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [captchaRefreshTrigger]) - - const getFilterLink = (column: string, value: string | null) => { - const isFilterActive = - filters.findIndex( - (filter) => filter.column === column && filter.filter === value, - ) >= 0 - const newSearchParams = new URLSearchParams(searchParams.toString()) - - if (isFilterActive) { - newSearchParams.delete(column, value ?? 'null') - } else { - newSearchParams.append(column, value ?? 'null') - } + if (subTabId === 'captcha_difficulty') { + return formatDifficulty(entryName) + } - return { search: newSearchParams.toString() } - } + return entryName + }, + [language, theme, t, formatReason, formatDifficulty], + ) - // Check if we have existing data - const hasExistingData = chartData !== null || !_isEmpty(panelsData.types) const formatNumber = (value: number, type: 'main' | 'badge') => - `${type === 'badge' && value > 0 ? '+' : ''}${nFormatter(value, 1)}` + type === 'badge' ? ( + `${value > 0 ? '+' : ''}${nFormatter(value, 1)}` + ) : ( + + ) const isFiniteNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value) const formatPercent = ( @@ -291,107 +238,99 @@ const CaptchaView = ({ projectId }: CaptchaViewProps) => { return 'N/A' } - return `${type === 'badge' && value > 0 ? '+' : ''}${value.toFixed(1)}%` + return type === 'badge' ? ( + `${value > 0 ? '+' : ''}${value.toFixed(1)}%` + ) : ( + + ) } - const formatSeconds = (value?: number) => - value ? `${Number(value).toFixed(2)}s` : 'N/A' const formatSecondsChange = (value: number) => `${value > 0 ? '+' : value < 0 ? '-' : ''}${Math.abs(Number(value || 0)).toFixed(2)}s` - const formatReason = (value?: string | null) => - value - ? t(`project.captchaAnalytics.reasons.${value}`, { defaultValue: value }) - : 'N/A' - const formatDifficulty = (value?: string | null) => { - switch (Number(value)) { - case 2: - return t('project.settings.captcha.difficultyLevels.veryEasy') - case 3: - return t('project.settings.captcha.difficultyLevels.easy') - case 4: - return t('project.settings.captcha.difficultyLevels.medium') - case 5: - return t('project.settings.captcha.difficultyLevels.hard') - case 6: - return t('project.settings.captcha.difficultyLevels.veryHard') - default: - return t('project.captchaAnalytics.difficultyValue', { value }) - } - } - const summaryCards = summaryData - ? [ - { - label: t('project.captchaAnalytics.generated'), - value: summaryData.generated || 0, - change: summaryCompareData - ? (summaryData.generated || 0) - (summaryCompareData.generated || 0) - : undefined, - goodChangeDirection: 'down' as const, - valueMapper: formatNumber, - }, - { - label: t('project.captchaAnalytics.passed'), - value: summaryData.passed || 0, - change: summaryCompareData - ? (summaryData.passed || 0) - (summaryCompareData.passed || 0) - : undefined, - goodChangeDirection: 'down' as const, - valueMapper: formatNumber, - }, - { - label: t('project.captchaAnalytics.passRate'), - value: summaryData.passRate, - change: summaryCompareData - ? isFiniteNumber(summaryData.passRate) && - isFiniteNumber(summaryCompareData.passRate) - ? summaryData.passRate - summaryCompareData.passRate - : undefined - : undefined, - goodChangeDirection: 'down' as const, - valueMapper: formatPercent, - }, - { - label: t('project.captchaAnalytics.medianSolve'), - value: summaryData.solveP50 || 0, - change: - summaryCompareData && - summaryData.solveP50 > 0 && - summaryCompareData.solveP50 > 0 - ? summaryData.solveP50 - summaryCompareData.solveP50 - : undefined, - goodChangeDirection: 'up' as const, - valueMapper: (value: number, type: 'main' | 'badge') => - type === 'badge' - ? formatSecondsChange(value) - : formatSeconds(value), - }, - ] - : [] + + // Built even before the summary lands: the cards read zero and roll up to the + // real values, so the row holds its height rather than appearing late. + const summaryCards = [ + { + label: t('project.captchaAnalytics.generated'), + value: summaryData?.generated || 0, + change: + summaryData && summaryCompareData + ? (summaryData.generated || 0) - (summaryCompareData.generated || 0) + : undefined, + goodChangeDirection: 'down' as const, + valueMapper: formatNumber, + }, + { + label: t('project.captchaAnalytics.passed'), + value: summaryData?.passed || 0, + change: + summaryData && summaryCompareData + ? (summaryData.passed || 0) - (summaryCompareData.passed || 0) + : undefined, + goodChangeDirection: 'down' as const, + valueMapper: formatNumber, + }, + { + label: t('project.captchaAnalytics.passRate'), + value: summaryData?.passRate as number, + change: + summaryData && + summaryCompareData && + isFiniteNumber(summaryData.passRate) && + isFiniteNumber(summaryCompareData.passRate) + ? summaryData.passRate - summaryCompareData.passRate + : undefined, + goodChangeDirection: 'down' as const, + valueMapper: formatPercent, + }, + { + label: t('project.captchaAnalytics.medianSolve'), + value: summaryData?.solveP50 || 0, + change: + summaryData && + summaryCompareData && + (summaryData.solveP50 || 0) > 0 && + (summaryCompareData.solveP50 || 0) > 0 + ? (summaryData.solveP50 || 0) - (summaryCompareData.solveP50 || 0) + : undefined, + goodChangeDirection: 'up' as const, + valueMapper: (value: number, type: 'main' | 'badge') => + type === 'badge' ? ( + formatSecondsChange(value) + ) : ( + + ), + }, + ] + + const summaryCardsRow = ( +
+ {summaryCards.map((card) => ( + + ))} +
+ ) // Show waiting state if project has no captcha data yet if (!_isEmpty(project) && !project?.isCaptchaDataExists) { return } - // Show Loader only on initial load (no existing data) - if (analyticsLoading && !hasExistingData) { - return ( -
- -
- ) - } + const headerRightContent = - if (isPanelsDataEmpty && !dataLoading) { + if (isPanelsDataEmpty) { return ( <> } + rightContent={headerRightContent} /> @@ -402,248 +341,45 @@ const CaptchaView = ({ projectId }: CaptchaViewProps) => { <> } + rightContent={headerRightContent} /> -
- {dataLoading && hasExistingData ? : null} -
- - {chartData ? ( - - {summaryCards.length ? ( -
- {summaryCards.map((card) => ( - - ))} -
- ) : null} -
- ) : null} -
- {panelsData.types - ? _map(PANELS_ORDER, (type: keyof typeof tnMapping) => { - const panelName = tnMapping[type] - const panelIcon = panelIconMapping[type] - const panelData = panelsData.data?.[type] || [] - - if (type === 'cc') { - const rowMapper = (entry: any) => { - const { name: entryName, cc } = entry - if (cc) { - return ( - - ) - } - return - } - - return ( - - ) - } - - if (type === 'dv') { - return ( - { - const { name: entryName } = entry - const icon = deviceIconMapping[entryName] - if (!icon) { - return entryName - } - return ( - <> - {icon} -   - {entryName} - - ) - }} - capitalize - activeTabId={type} - /> - ) - } - - if (type === 'br') { - const rowMapper = (entry: any) => { - const { name: entryName } = entry - // @ts-expect-error - const logoUrl = BROWSER_LOGO_MAP[entryName] - if (!logoUrl) { - return ( - <> - -   - {entryName} - - ) - } - return ( - <> - -   - {entryName} - - ) - } - - return ( - - ) - } - - if (type === 'os') { - const rowMapper = (entry: any) => { - const { name: entryName } = entry - // @ts-expect-error - const logoUrlLight = OS_LOGO_MAP[entryName] - // @ts-expect-error - const logoUrlDark = OS_LOGO_MAP_DARK[entryName] - let logoUrl = - theme === 'dark' ? logoUrlDark : logoUrlLight - logoUrl ||= logoUrlLight - - if (!logoUrl) { - return ( - <> - -   - {entryName} - - ) - } - - return ( - <> - -   - {entryName} - - ) - } - - return ( - - ) - } - - if (type === 'captcha_event') { - return ( - - t(`project.captchaAnalytics.events.${entry.name}`, { - defaultValue: entry.name, - }) - } - activeTabId={type} - /> - ) - } - - if (type === 'captcha_reason') { - return ( - formatReason(entry.name)} - activeTabId={type} - /> - ) - } - - if (type === 'captcha_difficulty') { - return ( - formatDifficulty(entry.name)} - activeTabId={type} - /> - ) - } - - return ( - - ) - }) - : null} +
+ + {isChartLoading ? ( + // Mirrors CaptchaChart's own container so the box keeps its size once + // the chart takes over from the spinner. +
+ {summaryCardsRow} +
+ +
+ ) : !_isEmpty(chartData.x) ? ( + + {summaryCardsRow} + + ) : null} +
+ {panels.map((panel) => ( + + ))}
diff --git a/web/app/pages/Project/tabs/Captcha/NoCaptchaEvents.tsx b/web/app/pages/Project/tabs/Captcha/NoCaptchaEvents.tsx index f35c85236..74cfb2cf4 100644 --- a/web/app/pages/Project/tabs/Captcha/NoCaptchaEvents.tsx +++ b/web/app/pages/Project/tabs/Captcha/NoCaptchaEvents.tsx @@ -3,15 +3,15 @@ import _isEmpty from 'lodash/isEmpty' import { memo } from 'react' import { useTranslation } from 'react-i18next' +import { V2Filter } from '~/api/v2/types' import { Text } from '~/ui/Text' import Filters from '../../View/components/Filters' -import { Filter } from '../../View/interfaces/traffic' import { captchaTypeNameMapping } from './CaptchaView' interface NoCaptchaEventsProps { - filters: Filter[] + filters: V2Filter[] } const NoCaptchaEvents = ({ filters }: NoCaptchaEventsProps) => { diff --git a/web/app/pages/Project/tabs/Captcha/adapters.ts b/web/app/pages/Project/tabs/Captcha/adapters.ts new file mode 100644 index 000000000..af521173b --- /dev/null +++ b/web/app/pages/Project/tabs/Captcha/adapters.ts @@ -0,0 +1,35 @@ +import { TimeseriesRow } from '~/api/v2/types' + +const toChartX = (timestamp: string): string => + timestamp.slice(0, 19).replace('T', ' ') + +export interface CaptchaChartData { + x: string[] + generated: number[] + passed: number[] + failed: number[] + validationFailed: number[] + replayed: number[] +} + +export const pivotCaptchaTimeseries = ( + rows: TimeseriesRow[] | undefined, +): CaptchaChartData => { + const x: string[] = [] + const generated: number[] = [] + const passed: number[] = [] + const failed: number[] = [] + const validationFailed: number[] = [] + const replayed: number[] = [] + + for (const row of rows || []) { + x.push(toChartX(row.timestamp)) + generated.push(Number(row.generated ?? 0)) + passed.push(Number(row.passed ?? 0)) + failed.push(Number(row.failed ?? 0)) + validationFailed.push(Number(row.validation_failed ?? 0)) + replayed.push(Number(row.replayed ?? 0)) + } + + return { x, generated, passed, failed, validationFailed, replayed } +} diff --git a/web/app/pages/Project/tabs/Errors/ErrorChart.tsx b/web/app/pages/Project/tabs/Errors/ErrorChart.tsx index 8a26dbdd9..c376a9fff 100644 --- a/web/app/pages/Project/tabs/Errors/ErrorChart.tsx +++ b/web/app/pages/Project/tabs/Errors/ErrorChart.tsx @@ -8,6 +8,7 @@ import type { ChartDataPointClick } from '~/pages/Project/View/utils/chartPoint' import { MetricCard } from '~/pages/Project/tabs/Traffic/MetricCards' import { MainChart } from '../../View/components/MainChart' +import { RefetchIndicator } from '../../View/v2/loading' import { getSettingsError } from '../../View/ViewProject.helpers' interface ErrorChartStat { @@ -32,6 +33,7 @@ interface ErrorChartProps { annotations?: Annotation[] stats?: ErrorChartStat[] onDataPointClick?: ChartDataPointClick + isRefetching?: boolean } export const ErrorChart = ({ @@ -45,6 +47,7 @@ export const ErrorChart = ({ annotations, stats, onDataPointClick, + isRefetching, }: ErrorChartProps) => { const { t } = useTranslation('common') @@ -117,7 +120,8 @@ export const ErrorChart = ({ if (!options && !hasStats) return null return ( -
+
+ {isRefetching ? : null} {hasStats ? (
{stats!.map((stat) => ( diff --git a/web/app/pages/Project/tabs/Errors/ErrorDetails.tsx b/web/app/pages/Project/tabs/Errors/ErrorDetails.tsx index d41160eef..972208d42 100644 --- a/web/app/pages/Project/tabs/Errors/ErrorDetails.tsx +++ b/web/app/pages/Project/tabs/Errors/ErrorDetails.tsx @@ -9,22 +9,17 @@ import { UsersIcon, CaretRightIcon, } from '@phosphor-icons/react' -import { useMemo, useState, useEffect, useCallback, useRef } from 'react' +import { useMemo, useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { Link } from '~/ui/Link' import { useLocation } from 'react-router' -import type { - ErrorAffectedSession, - ErrorDetailsResponse, -} from '~/api/api.server' -import { useErrorSessionsProxy } from '~/hooks/useAnalyticsProxy' +import { useErrorSessionsQuery } from '~/hooks/v2/useV2Queries' import { BROWSER_LOGO_MAP, OS_LOGO_MAP, OS_LOGO_MAP_DARK, } from '~/lib/constants' -import { useCurrentProject } from '~/providers/CurrentProjectProvider' import { useTheme } from '~/providers/ThemeProvider' import { Badge } from '~/ui/Badge' import Flag from '~/ui/Flag' @@ -35,16 +30,13 @@ import { Text } from '~/ui/Text' import { getRelativeDateIfPossible } from '~/utils/date' import { cn } from '~/utils/generic' +import type { ErrorDetailsInfo, ErrorSessionItem } from './types' + const STACK_PREVIEW_LINES_COUNT = 5 interface ErrorDetailsProps { - details: ErrorDetailsResponse['details'] + details: ErrorDetailsInfo chart?: React.ReactNode - period?: string - from?: string - to?: string - timeBucket?: string - projectPassword?: string } const BrowserIcon = ({ browser }: { browser: string | null }) => { @@ -148,7 +140,7 @@ const ToolbarButton = ({ ) interface SessionRowProps { - session: ErrorAffectedSession + session: ErrorSessionItem onClick?: () => void } @@ -178,10 +170,10 @@ const SessionRow = ({ session, onClick }: SessionRowProps) => { >
- {session.cc ? ( + {session.country ? (
@@ -215,40 +207,29 @@ const SessionRow = ({ session, onClick }: SessionRowProps) => { ) } -export const ErrorDetails = ({ - details, - chart, - period = '7d', - from = '', - to = '', - timeBucket = 'hour', -}: ErrorDetailsProps) => { +export const ErrorDetails = ({ details, chart }: ErrorDetailsProps) => { const { t } = useTranslation('common') - const { id } = useCurrentProject() - const { fetchErrorSessions } = useErrorSessionsProxy() const [isStackTraceExpanded, setIsStackTraceExpanded] = useState(false) const [isCopiedStack, setIsCopiedStack] = useState(false) const [isCopiedEid, setIsCopiedEid] = useState(false) const [isCopiedFile, setIsCopiedFile] = useState(false) - const [sessions, setSessions] = useState([]) - const [sessionsLoading, setSessionsLoading] = useState(null) - const [sessionsTotal, setSessionsTotal] = useState(0) - const [sessionsSkip, setSessionsSkip] = useState(0) const [isSessionsModalOpened, setIsSessionsModalOpened] = useState(false) const sessionsModalListRef = useRef(null) - const isMountedRef = useRef(true) const SESSIONS_PREVIEW_COUNT = 5 - const SESSIONS_MODAL_TAKE = 25 - useEffect(() => { - isMountedRef.current = true - return () => { - isMountedRef.current = false - } - }, []) + const sessionsQuery = useErrorSessionsQuery(details.eid) + const sessions = useMemo( + () => + (sessionsQuery.data?.pages.flatMap((page) => page.data) || + []) as ErrorSessionItem[], + [sessionsQuery.data], + ) + const sessionsLoading = sessionsQuery.isLoading + const canLoadMoreSessions = Boolean(sessionsQuery.hasNextPage) + const sessionsCountLabel = `${sessions.length.toLocaleString()}${canLoadMoreSessions ? '+' : ''}` const status: { label: string; colour: 'red' | 'yellow' | 'slate' } = useMemo(() => { @@ -265,7 +246,6 @@ export const ErrorDetails = ({ () => (details.stackTrace ? details.stackTrace.split('\n') : []), [details.stackTrace], ) - const canLoadMoreSessions = sessions.length < sessionsTotal const fileLocation = useMemo(() => { if (!details.filename) return '' @@ -279,6 +259,7 @@ export const ErrorDetails = ({ setIsCopiedStack(false) setIsCopiedEid(false) setIsCopiedFile(false) + setIsSessionsModalOpened(false) // eslint-disable-next-line react-hooks/exhaustive-deps }, [details.eid]) @@ -318,63 +299,6 @@ export const ErrorDetails = ({ } } - const loadSessions = useCallback( - async (reset = false) => { - if (sessionsLoading) return - - setSessionsLoading(true) - const skip = reset ? 0 : sessionsSkip - - try { - const result = await fetchErrorSessions(id, details.eid, { - timeBucket, - period, - from, - to, - take: reset ? SESSIONS_PREVIEW_COUNT : SESSIONS_MODAL_TAKE, - skip, - }) - - if (!isMountedRef.current) return - - if (result) { - if (reset) { - setSessions(result.sessions) - } else { - setSessions((prev) => [...prev, ...result.sessions]) - } - setSessionsTotal(result.total) - setSessionsSkip(skip + result.sessions.length) - } - } catch (reason) { - console.error('[ErrorDetails] Failed to load sessions:', reason) - } finally { - if (isMountedRef.current) { - setSessionsLoading(false) - } - } - }, - [ - id, - details.eid, - timeBucket, - period, - from, - to, - sessionsLoading, - sessionsSkip, - fetchErrorSessions, - ], - ) - - useEffect(() => { - setSessions([]) - setSessionsSkip(0) - setIsSessionsModalOpened(false) - loadSessions(true) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [details.eid, period, from, to]) - const formatStackTraceLine = (line: string) => { const atMatch = line.match(/(\s*at\s+)([^(]+)(\s*\(([^)]+)\))?/) const fileMatch = line.match(/([^:]+):(\d+):(\d+)/) @@ -586,11 +510,7 @@ export const ErrorDetails = ({ } meta={ - sessionsTotal > 0 - ? sessionsTotal.toLocaleString() - : sessionsLoading && sessions.length === 0 - ? '…' - : '0' + sessionsLoading && sessions.length === 0 ? '…' : sessionsCountLabel } > {sessionsLoading && sessions.length === 0 ? ( @@ -610,14 +530,15 @@ export const ErrorDetails = ({ ))}
- {sessionsTotal > SESSIONS_PREVIEW_COUNT ? ( + {sessions.length > SESSIONS_PREVIEW_COUNT || + canLoadMoreSessions ? ( ) : null} @@ -635,7 +556,7 @@ export const ErrorDetails = ({ {t('project.affectedSessionsList')} - {sessionsTotal.toLocaleString()} + {sessionsCountLabel} } @@ -655,9 +576,9 @@ export const ErrorDetails = ({
loadSessions()} - disabled={!!sessionsLoading} + isLoading={sessionsQuery.isFetchingNextPage} + onLoadMore={() => sessionsQuery.fetchNextPage()} + disabled={sessionsQuery.isFetching} root={sessionsModalListRef} />
diff --git a/web/app/pages/Project/tabs/Errors/ErrorsMap.tsx b/web/app/pages/Project/tabs/Errors/ErrorsMap.tsx new file mode 100644 index 000000000..12ba38c0d --- /dev/null +++ b/web/app/pages/Project/tabs/Errors/ErrorsMap.tsx @@ -0,0 +1,75 @@ +import { Suspense, lazy, useMemo } from 'react' +import { useNavigate } from 'react-router' + +import { useBreakdownQuery } from '~/hooks/v2/useV2Queries' +import { Entry } from '~/lib/models/Entry' +import { MapLoader } from '~/pages/Project/View/components/MapLoader' +import { mapBreakdownRows } from '~/pages/Project/View/v2/adapters' +import { useViewProjectContext } from '~/pages/Project/View/ViewProject' + +const InteractiveMap = lazy( + () => import('~/pages/Project/View/components/InteractiveMap'), +) + +const MAP_CLICK_TYPE_TO_DIMENSION = { cc: 'country', rg: 'region' } as const + +interface ErrorsMapProps { + staticCountryData?: Entry[] + staticRegionData?: Entry[] +} + +export const ErrorsMap = ({ + staticCountryData, + staticRegionData, +}: ErrorsMapProps) => { + const navigate = useNavigate() + const { getFilterLink } = useViewProjectContext() + const isStatic = Boolean(staticCountryData) + + const countryQuery = useBreakdownQuery('errors', { + dimension: 'country', + limit: 100, + sort: 'occurrences:desc', + enabled: !isStatic, + }) + const regionQuery = useBreakdownQuery('errors', { + dimension: 'region', + limit: 100, + sort: 'occurrences:desc', + enabled: !isStatic, + }) + + const countryData = useMemo( + () => + staticCountryData ?? + mapBreakdownRows(countryQuery.data?.data, 'occurrences'), + [staticCountryData, countryQuery.data], + ) + const regionData = useMemo( + () => + staticRegionData ?? + mapBreakdownRows(regionQuery.data?.data, 'occurrences'), + [staticRegionData, regionQuery.data], + ) + const total = useMemo( + () => countryData.reduce((acc, curr) => acc + curr.count, 0), + [countryData], + ) + + if (!isStatic && countryQuery.isLoading) { + return + } + + return ( + }> + { + navigate(getFilterLink(MAP_CLICK_TYPE_TO_DIMENSION[mapType], key)) + }} + /> + + ) +} diff --git a/web/app/pages/Project/tabs/Errors/ErrorsView.tsx b/web/app/pages/Project/tabs/Errors/ErrorsView.tsx index 274e92ceb..3356385de 100644 --- a/web/app/pages/Project/tabs/Errors/ErrorsView.tsx +++ b/web/app/pages/Project/tabs/Errors/ErrorsView.tsx @@ -1,9 +1,9 @@ +import { useQueryClient } from '@tanstack/react-query' import type { ChartOptions } from 'billboard.js' import { area, bar } from 'billboard.js' import cx from 'clsx' import * as d3 from 'd3' import dayjs from 'dayjs' -import _debounce from 'lodash/debounce' import _isEmpty from 'lodash/isEmpty' import _map from 'lodash/map' import _size from 'lodash/size' @@ -15,39 +15,20 @@ import { WarningIcon, UserIcon, } from '@phosphor-icons/react' -import { - useState, - useEffect, - useMemo, - useRef, - useCallback, - lazy, - Suspense, - use, -} from 'react' -import { useIsPresent } from 'motion/react' +import { useState, useEffect, useMemo, useRef, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { Link } from '~/ui/Link' -import { - useLocation, - useSearchParams, - useNavigate, - useFetcher, - useLoaderData, - useRevalidator, - type LinkProps, -} from 'react-router' +import { useLocation, useSearchParams, useFetcher } from 'react-router' import { ClientOnly } from 'remix-utils/client-only' -import { MapLoader } from '~/pages/Project/View/components/MapLoader' import { toast } from 'sonner' -import type { - ErrorsResponse, - ErrorDetailsResponse, - ErrorOverviewResponse, -} from '~/api/api.server' -import { useErrorsProxy } from '~/hooks/useAnalyticsProxy' +import type { ErrorListItem } from '~/api/v2/types' +import { + useErrorDetailsQuery, + useErrorsListQuery, + useErrorsOverviewQuery, +} from '~/hooks/v2/useV2Queries' import { TimeFormat, tbsFormatMapper, @@ -55,14 +36,18 @@ import { tbsFormatMapperTooltip, tbsFormatMapperTooltip24h, chartTypes, - ERROR_PANELS_ORDER, } from '~/lib/constants' -import { CountryEntry, Entry } from '~/lib/models/Entry' -import { SwetrixError } from '~/lib/models/Project' +import { Entry } from '~/lib/models/Entry' import { ErrorChart } from '~/pages/Project/tabs/Errors/ErrorChart' import { ErrorDetails } from '~/pages/Project/tabs/Errors/ErrorDetails' +import { ErrorsMap } from '~/pages/Project/tabs/Errors/ErrorsMap' import NoErrorDetails from '~/pages/Project/tabs/Errors/NoErrorDetails' +import type { + ErrorDetailsData, + ErrorsOverviewData, +} from '~/pages/Project/tabs/Errors/types' import WaitingForAnError from '~/pages/Project/tabs/Errors/WaitingForAnError' +import PageLinkRow from '~/pages/Project/tabs/Traffic/PageLinkRow' import { SessionsDrawer } from '~/pages/Project/tabs/Traffic/SessionsDrawer' import CCRow from '~/pages/Project/View/components/CCRow' import DashboardHeader from '~/pages/Project/View/components/DashboardHeader' @@ -70,14 +55,12 @@ import Filters from '~/pages/Project/View/components/Filters' import NoEvents from '~/pages/Project/View/components/NoEvents' import ProjectViewHeaderActions from '~/pages/Project/View/components/ProjectViewHeaderActions' import TabErrorBoundary from '~/pages/Project/View/components/TabErrorBoundary' -import { Panel, MetadataPanel } from '~/pages/Project/View/Panels' +import { MetadataPanel, Panel } from '~/pages/Project/View/Panels' import { ERROR_FILTERS_MAPPING } from '~/pages/Project/View/utils/filters' +import { BreakdownSubTab } from '~/pages/Project/View/v2/BreakdownPanel' +import { RefetchIndicator } from '~/pages/Project/View/v2/loading' +import { useViewProjectContext } from '~/pages/Project/View/ViewProject' import { - useViewProjectContext, - useRefreshTriggers, -} from '~/pages/Project/View/ViewProject' -import { - getFormatDate, typeNameMapping, panelIconMapping, getDeviceRowMapper, @@ -89,31 +72,20 @@ import { getChartPointWindow, type ChartDataPointClick, } from '~/pages/Project/View/utils/chartPoint' -import { - useCurrentProject, - useProjectPassword, -} from '~/providers/CurrentProjectProvider' +import { useCurrentProject } from '~/providers/CurrentProjectProvider' import { useTheme } from '~/providers/ThemeProvider' -import type { - ProjectViewActionData, - ProjectLoaderData, -} from '~/routes/projects.$id' +import type { ProjectViewActionData } from '~/routes/projects.$id' import { Badge } from '~/ui/Badge' import BillboardChart from '~/ui/BillboardChart' import Checkbox from '~/ui/Checkbox' import Dropdown from '~/ui/Dropdown' import InfiniteScrollTrigger from '~/ui/InfiniteScrollTrigger' import Loader from '~/ui/Loader' -import LoadingBar from '~/ui/LoadingBar' import { Text } from '~/ui/Text' import Tooltip from '~/ui/Tooltip' import { getRelativeDateIfPossible } from '~/utils/date' +import { CompactNumberFlow, PercentFlow } from '~/ui/NumberFlow' import { getLocaleDisplayName, nFormatter } from '~/utils/generic' -import { LoaderView } from '../../View/components/LoaderView' - -const InteractiveMap = lazy( - () => import('~/pages/Project/View/components/InteractiveMap'), -) const calculateOptimalTicks = ( data: number[], @@ -323,9 +295,173 @@ const getErrorTrendsChartSettings = ( } } +const PER_ERROR_SUBTAB_TO_V1: Record = { + country: 'cc', + region: 'rg', + city: 'ct', + locale: 'lc', + page: 'pg', + host: 'host', + browser: 'br', + os: 'os', + device: 'dv', + isp: 'isp', + organization: 'og', + user_type: 'ut', + connection_type: 'ctp', +} + +interface PerErrorPanelsProps { + params: ErrorDetailsData['params'] + locationSubTabs: BreakdownSubTab[] + pagesSubTabs: BreakdownSubTab[] + devicesSubTabs: BreakdownSubTab[] + networkSubTabs: BreakdownSubTab[] + locationRowMapper: (entry: Entry, subTabId: string) => React.ReactNode + pagesRowMapper: (entry: Entry, subTabId: string) => React.ReactNode + devicesRowMapper: (entry: Entry, subTabId: string) => React.ReactNode + networkRowMapper: (entry: Entry, subTabId: string) => React.ReactNode + isRefetching: boolean +} + +const PerErrorPanels = ({ + params, + locationSubTabs, + pagesSubTabs, + devicesSubTabs, + networkSubTabs, + locationRowMapper, + pagesRowMapper, + devicesRowMapper, + networkRowMapper, + isRefetching, +}: PerErrorPanelsProps) => { + const { t } = useTranslation('common') + const { getFilterLink, getVersionFilterLink } = useViewProjectContext() + + const [activeTabs, setActiveTabs] = useState({ + location: 'country', + pages: 'page', + devices: 'browser', + network: 'isp', + }) + + const setPanelTab = (panel: keyof typeof activeTabs, tab: string) => + setActiveTabs((prev) => ({ ...prev, [panel]: tab })) + + const toEntries = (subTabId: string): Entry[] => + (params?.[PER_ERROR_SUBTAB_TO_V1[subTabId]] || []) as Entry[] + + const versionData = useMemo(() => { + const browserVersions: Record = {} + const osVersions: Record = {} + + for (const entry of (params?.brv || []) as any[]) { + const { br, name, count } = entry + if (!br) continue + ;(browserVersions[br] ||= []).push({ name, count }) + } + for (const entry of (params?.osv || []) as any[]) { + const { os, name, count } = entry + if (!os) continue + ;(osVersions[os] ||= []).push({ name, count }) + } + + return { browserVersions, osVersions } + }, [params]) + + const panels: { + panelKey: keyof typeof activeTabs + name: string + icon: React.ReactNode + subTabs: BreakdownSubTab[] + rowMapper: (entry: Entry, subTabId: string) => React.ReactNode + }[] = [ + { + panelKey: 'location', + name: t('project.location'), + icon: panelIconMapping.country, + subTabs: locationSubTabs, + rowMapper: locationRowMapper, + }, + { + panelKey: 'pages', + name: t('project.pages'), + icon: panelIconMapping.page, + subTabs: pagesSubTabs, + rowMapper: pagesRowMapper, + }, + { + panelKey: 'devices', + name: t('project.devices'), + icon: panelIconMapping.os, + subTabs: devicesSubTabs, + rowMapper: devicesRowMapper, + }, + { + panelKey: 'network', + name: t('project.network'), + icon: panelIconMapping.isp, + subTabs: networkSubTabs, + rowMapper: networkRowMapper, + }, + ] + + return ( + <> + {panels.map(({ panelKey, name, icon, subTabs, rowMapper }) => { + const activeTabId = activeTabs[panelKey] + + return ( + ({ id, label }))} + onTabChange={(tab) => setPanelTab(panelKey, tab)} + activeTabId={activeTabId} + data={toEntries(activeTabId)} + rowMapper={(entry) => rowMapper(entry, activeTabId)} + getFilterLink={getFilterLink} + highlightColour='red' + valuesHeaderName={t('project.occurrences')} + isRefetching={isRefetching} + customRenderer={ + activeTabId === 'map' + ? () => ( + + ) + : undefined + } + capitalize={activeTabId === 'device'} + versionData={ + activeTabId === 'browser' + ? versionData.browserVersions + : activeTabId === 'os' + ? versionData.osVersions + : undefined + } + getVersionFilterLink={(parent, version) => + getVersionFilterLink( + parent, + version, + activeTabId === 'browser' ? 'browser' : 'os', + ) + } + /> + ) + })} + + ) +} + interface StatCardProps { icon: React.ReactNode - value: string | number + value: React.ReactNode label: string } @@ -346,7 +482,7 @@ const StatCard = ({ icon, value, label }: StatCardProps) => ( ) interface ErrorItemProps { - error: SwetrixError + error: ErrorListItem } const ErrorItem = ({ error }: ErrorItemProps) => { @@ -517,86 +653,24 @@ const ErrorItem = ({ error }: ErrorItemProps) => { ) } -const ERRORS_TAKE = 30 - -interface DeferredErrorsData { - errorsData: ErrorsResponse | null - errorDetails: ErrorDetailsResponse | null - errorOverview: ErrorOverviewResponse | null -} - -function ErrorsDataResolver({ - children, -}: { - children: (data: DeferredErrorsData) => React.ReactNode -}) { - const isPresent = useIsPresent() - const previousDataRef = useRef(null) - const { - errorsData: errorsDataPromise, - errorDetails: errorDetailsPromise, - errorOverview: errorOverviewPromise, - } = useLoaderData() - const shouldUsePreviousData = !isPresent && !!previousDataRef.current - - const errorsData = shouldUsePreviousData - ? previousDataRef.current!.errorsData - : errorsDataPromise - ? use(errorsDataPromise) - : null - const errorDetails = shouldUsePreviousData - ? previousDataRef.current!.errorDetails - : errorDetailsPromise - ? use(errorDetailsPromise) - : null - const errorOverview = shouldUsePreviousData - ? previousDataRef.current!.errorOverview - : errorOverviewPromise - ? use(errorOverviewPromise) - : null - - const deferredData = useMemo( - () => ({ errorsData, errorDetails, errorOverview }), - [errorsData, errorDetails, errorOverview], - ) - - if (isPresent) { - previousDataRef.current = deferredData - } - - return <>{children(deferredData)} -} - function ErrorsViewWrapper() { const [searchParams] = useSearchParams() - const { errorsRefreshTrigger } = useRefreshTriggers() - const resetKey = `errors:${searchParams.toString()}:${errorsRefreshTrigger}` + const resetKey = `errors:${searchParams.toString()}` return ( - }> - - {(deferredData) => } - - + ) } -interface ErrorsViewInnerProps { - deferredData: DeferredErrorsData -} - -const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { +const ErrorsViewInner = () => { const { id, projectPath, allowedToManage, project } = useCurrentProject() - const projectPassword = useProjectPassword(id) - const revalidator = useRevalidator() - const errorsProxy = useErrorsProxy() - const { errorsRefreshTrigger } = useRefreshTriggers() - const { timeBucket, timeFormat, period, dateRange, timezone, filters, size } = + const queryClient = useQueryClient() + const { timeBucket, timeFormat, timezone, filters, size } = useViewProjectContext() const { t, @@ -604,48 +678,17 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { } = useTranslation('common') const { theme } = useTheme() const [searchParams] = useSearchParams() - const isEmbedded = searchParams.get('embedded') === 'true' - const navigate = useNavigate() const errorStatusFetcher = useFetcher() const lastHandledStatusData = useRef(null) - const pendingStatusUpdate = useRef<'resolved' | 'active' | null>(null) - const from = dateRange ? getFormatDate(dateRange[0]) : '' - const to = dateRange ? getFormatDate(dateRange[1]) : '' const tnMapping = typeNameMapping(t) const rotateXAxis = useMemo(() => size.width > 0 && size.width < 500, [size]) - // Initialize state from deferred data - const initialDataProcessed = useRef(false) - const errorsProxyRequestRef = useRef<{ - mode: 'append' | 'replace' - limit?: number - } | null>(null) - const preserveErrorsRevalidationRef = useRef(false) - const preserveErrorsRevalidationStartedRef = useRef(false) - const [errorOptions, setErrorOptions] = useState>({ [ERROR_FILTERS_MAPPING.showResolved]: false, }) + const showResolved = errorOptions[ERROR_FILTERS_MAPPING.showResolved] - // Overview from loader - const overview = deferredData.errorOverview - const overviewLoading = revalidator.state === 'loading' - - const isMountedRef = useRef(true) - - const [errorsLoading, setErrorsLoading] = useState(() => - deferredData.errorsData ? false : null, - ) - const [errors, setErrors] = useState( - () => deferredData.errorsData?.errors || [], - ) - const [errorsSkip, setErrorsSkip] = useState( - () => deferredData.errorsData?.errors?.length || 0, - ) - const [canLoadMoreErrors, setCanLoadMoreErrors] = useState( - () => (deferredData.errorsData?.errors?.length || 0) >= ERRORS_TAKE, - ) const [sessionsDrawer, setSessionsDrawer] = useState<{ from: string to: string @@ -655,35 +698,23 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { const activeEID = useMemo(() => searchParams.get('eid'), [searchParams]) - // Error details from loader - const activeError = useMemo(() => { - if (deferredData.errorDetails) { - return { - details: deferredData.errorDetails.details, - chart: deferredData.errorDetails.chart, - params: deferredData.errorDetails.params, - metadata: deferredData.errorDetails.metadata, - timeBucket: deferredData.errorDetails.timeBucket, - } - } - return null - }, [deferredData.errorDetails]) - - const errorLoading = activeEID ? revalidator.state === 'loading' : false - - const errorStatusUpdating = errorStatusFetcher.state !== 'idle' - - const [errorsActiveTabs, setErrorsActiveTabs] = useState<{ - location: 'cc' | 'rg' | 'ct' | 'lc' | 'map' - page: 'pg' | 'host' - device: 'br' | 'os' | 'dv' - network: 'isp' | 'og' | 'ut' | 'ctp' - }>({ - location: 'cc', - page: 'pg', - device: 'br', - network: 'isp', + const errorsListQuery = useErrorsListQuery({ + showResolved, + enabled: !activeEID, }) + const overviewQuery = useErrorsOverviewQuery({ enabled: !activeEID }) + const detailsQuery = useErrorDetailsQuery(activeEID) + + const errors = useMemo( + () => + (errorsListQuery.data?.pages.flatMap((page) => page.data) || + []) as ErrorListItem[], + [errorsListQuery.data], + ) + const overview = overviewQuery.data?.data as ErrorsOverviewData | undefined + const activeError = activeEID + ? (detailsQuery.data?.data as ErrorDetailsData | undefined) + : undefined const pureSearchParams = useMemo(() => { const newSearchParams = new URLSearchParams(searchParams.toString()) @@ -691,142 +722,7 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { return newSearchParams.toString() }, [searchParams]) - useEffect(() => { - isMountedRef.current = true - return () => { - isMountedRef.current = false - } - }, []) - - // Process deferred data on mount - useEffect(() => { - if (initialDataProcessed.current) return - initialDataProcessed.current = true - - if (deferredData.errorsData) { - const errorsList = deferredData.errorsData.errors || [] - setErrors(errorsList) - setErrorsSkip(ERRORS_TAKE) - setCanLoadMoreErrors(errorsList.length >= ERRORS_TAKE) - } else { - setErrors([]) - setCanLoadMoreErrors(false) - } - setErrorsLoading(false) - }, [deferredData]) - - // Sync state when revalidation completes with new data - useEffect(() => { - if (!initialDataProcessed.current) return - if (revalidator.state === 'idle') { - if (preserveErrorsRevalidationRef.current) { - if (preserveErrorsRevalidationStartedRef.current) { - preserveErrorsRevalidationRef.current = false - preserveErrorsRevalidationStartedRef.current = false - setErrorsLoading(false) - } - return - } - - if (deferredData.errorsData) { - const errorsList = deferredData.errorsData.errors || [] - setErrors(errorsList) - setErrorsSkip(ERRORS_TAKE) - setCanLoadMoreErrors(errorsList.length >= ERRORS_TAKE) - } else { - setErrors([]) - setCanLoadMoreErrors(false) - } - setErrorsLoading(false) - } else if (revalidator.state === 'loading') { - if (preserveErrorsRevalidationRef.current) { - preserveErrorsRevalidationStartedRef.current = true - } - setErrorsLoading(true) - } - }, [revalidator.state, deferredData]) - - // Load more errors via proxy - const loadMoreErrors = useCallback(() => { - if (errorsLoading || errorsProxy.isLoading) return - - errorsProxyRequestRef.current = { mode: 'append' } - errorsProxy.fetchErrors(id, { - timeBucket, - period: period === 'custom' ? '' : period, - from: from || undefined, - to: to || undefined, - timezone: timezone || '', - filters, - take: ERRORS_TAKE, - skip: errorsSkip, - options: errorOptions, - }) - }, [ - id, - timeBucket, - period, - from, - to, - timezone, - filters, - errorsSkip, - errorOptions, - errorsLoading, - errorsProxy, - ]) - - // Handle proxy response for pagination - useEffect(() => { - if (revalidator.state === 'loading') return - - if (errorsProxy.data && !errorsProxy.isLoading) { - const proxyRequest = errorsProxyRequestRef.current - if (!proxyRequest) { - return - } - - const newErrors = errorsProxy.data.errors || [] - if (proxyRequest.mode === 'replace') { - const visibleErrors = newErrors.slice(0, proxyRequest.limit) - setErrors(visibleErrors) - setErrorsSkip(visibleErrors.length) - setCanLoadMoreErrors(newErrors.length > (proxyRequest.limit || 0)) - } else { - setErrors((prev) => [...prev, ...newErrors]) - setErrorsSkip((prev) => prev + newErrors.length) - setCanLoadMoreErrors(newErrors.length >= ERRORS_TAKE) - } - setErrorsLoading(false) - errorsProxyRequestRef.current = null - } - if (errorsProxy.error) { - toast.error(errorsProxy.error) - setErrorsLoading(false) - errorsProxyRequestRef.current = null - } - }, [ - errorsProxy.data, - errorsProxy.error, - errorsProxy.isLoading, - revalidator.state, - ]) - - const updateStatusInErrors = useCallback( - (status: 'active' | 'resolved') => { - if (!activeError?.details?.eid) return - - setErrors((prevErrors) => { - return prevErrors.map((error) => { - if (error.eid === activeError.details.eid) { - return { ...error, status } - } - return error - }) - }) - }, - [activeError?.details?.eid], - ) + const errorStatusUpdating = errorStatusFetcher.state !== 'idle' // Handle error status update response useEffect(() => { @@ -837,25 +733,18 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { const { intent, success, error } = errorStatusFetcher.data if (intent === 'update-error-status') { - if (success && pendingStatusUpdate.current) { - updateStatusInErrors(pendingStatusUpdate.current) - if (activeEID) { - revalidator.revalidate() - } + if (success) { + queryClient.invalidateQueries({ queryKey: ['v2'] }) toast.success(t('apiNotifications.errorStatusUpdated')) - pendingStatusUpdate.current = null } else if (error) { toast.error(error) - pendingStatusUpdate.current = null } } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [errorStatusFetcher.state, errorStatusFetcher.data, t, activeEID]) + }, [errorStatusFetcher.state, errorStatusFetcher.data, t, queryClient]) const markErrorAsResolved = () => { if (errorStatusUpdating || !activeEID || !activeError?.details?.eid) return - pendingStatusUpdate.current = 'resolved' errorStatusFetcher.submit( { intent: 'update-error-status', eid: activeEID, status: 'resolved' }, { method: 'POST', action: projectPath }, @@ -865,21 +754,15 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { const markErrorAsActive = () => { if (errorStatusUpdating || !activeEID || !activeError?.details?.eid) return - pendingStatusUpdate.current = 'active' errorStatusFetcher.submit( { intent: 'update-error-status', eid: activeEID, status: 'active' }, { method: 'POST', action: projectPath }, ) } - const switchActiveErrorFilter = useMemo( - () => - _debounce((pairID: string) => { - setErrorOptions((prev) => ({ ...prev, [pairID]: !prev[pairID] })) - setErrorsSkip(0) - }, 0), - [], - ) + const switchActiveErrorFilter = useCallback((pairID: string) => { + setErrorOptions((prev) => ({ ...prev, [pairID]: !prev[pairID] })) + }, []) const errorFilters = useMemo(() => { return [ @@ -891,19 +774,21 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { ] }, [t, errorOptions]) + const overviewTimeBucket = overview?.timeBucket || timeBucket + const handleOverviewDataPointClick = useCallback( (d: { x: Date; index: number; xValue?: string }) => { setSessionsDrawer( getChartPointWindow({ x: d.x, xValue: d.xValue, - timeBucket, + timeBucket: overviewTimeBucket, timezone, timeFormat, }), ) }, - [timeBucket, timeFormat, timezone], + [overviewTimeBucket, timeFormat, timezone], ) const handleActiveErrorDataPointClick = useCallback( @@ -930,41 +815,12 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { ], ) - useEffect(() => { - if (errorsRefreshTrigger > 0) { - if (!activeEID) { - if (errorsProxy.isLoading) { - return - } - - const limit = Math.max(errors.length, ERRORS_TAKE) - errorsProxyRequestRef.current = { mode: 'replace', limit } - preserveErrorsRevalidationRef.current = true - setErrorsLoading(true) - errorsProxy.fetchErrors(id, { - timeBucket, - period: period === 'custom' ? '' : period, - from: from || undefined, - to: to || undefined, - timezone: timezone || '', - filters, - take: limit + 1, - skip: 0, - options: errorOptions, - }) - } - - revalidator.revalidate() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [errorsRefreshTrigger]) - const chartOptions = useMemo(() => { if (!overview?.chart || !overview.chart.x || overview.chart.x.length === 0) return null return getErrorTrendsChartSettings( overview.chart, - timeBucket, + overviewTimeBucket, timeFormat, chartTypes.line, { @@ -974,9 +830,16 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { handleOverviewDataPointClick, t('project.exploreSessions'), ) - }, [overview?.chart, timeBucket, timeFormat, handleOverviewDataPointClick, t]) + }, [ + overview?.chart, + overviewTimeBucket, + timeFormat, + handleOverviewDataPointClick, + t, + ]) - const hasErrorsRaw = !_isEmpty(errors) || overview?.stats?.totalErrors + const hasErrorsRaw = + !_isEmpty(errors) || Boolean(overview?.stats?.totalErrors) // Track if we've ever shown actual content to prevent NoEvents flash during exit animation const hasShownContentRef = useRef(false) @@ -988,81 +851,181 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { // Don't show NoEvents if we've previously shown content (prevents flash during tab switch) const hasErrors = hasErrorsRaw || hasShownContentRef.current - const getFilterLink = useCallback( - (column: string, value: string | null): LinkProps['to'] => { - const isFilterActive = - filters.findIndex( - (filter) => filter.column === column && filter.filter === value, - ) >= 0 - const newSearchParams = new URLSearchParams(searchParams.toString()) - - if (isFilterActive) { - newSearchParams.delete(column) - } else { - if (value === null) { - newSearchParams.set(column, 'null') - } else { - newSearchParams.set(column, value) - } - } + const isInitialLoading = + (overviewQuery.isLoading || errorsListQuery.isLoading) && + !hasShownContentRef.current - return { search: newSearchParams.toString() } - }, - [filters, searchParams], + const isRefetching = + (overviewQuery.isFetching && !overviewQuery.isLoading) || + (errorsListQuery.isFetching && + !errorsListQuery.isLoading && + !errorsListQuery.isFetchingNextPage) + + const dataNames = useMemo( + () => ({ + occurrences: t('project.totalErrors'), + affectedUsers: t('project.affectedUsers'), + }), + [t], + ) + + const locationSubTabs = useMemo( + () => [ + { id: 'country', label: t('project.mapping.cc'), dimension: 'country' }, + { id: 'region', label: t('project.mapping.rg'), dimension: 'region' }, + { id: 'city', label: t('project.mapping.ct'), dimension: 'city' }, + { id: 'locale', label: t('project.mapping.lc'), dimension: 'locale' }, + { + id: 'map', + label: t('project.mapping.map'), + render: () => , + }, + ], + [t], ) - const getVersionFilterLink = useCallback( - (parent: string | null, version: string | null, panelType: 'br' | 'os') => { - const filterParams = new URLSearchParams(searchParams.toString()) + const pagesSubTabs = useMemo( + () => [ + { id: 'page', label: t('project.mapping.pg'), dimension: 'page' }, + { id: 'host', label: t('project.mapping.host'), dimension: 'host' }, + ], + [t], + ) + + const devicesSubTabs = useMemo( + () => [ + { + id: 'browser', + label: t('project.mapping.br'), + dimension: 'browser', + versionsDimension: 'browser_version' as const, + versionsParentField: 'browser' as const, + }, + { + id: 'os', + label: t('project.mapping.os'), + dimension: 'os', + versionsDimension: 'os_version' as const, + versionsParentField: 'os' as const, + }, + { id: 'device', label: t('project.mapping.dv'), dimension: 'device' }, + ], + [t], + ) - if (panelType === 'br') { - filterParams.set('br', parent ?? 'null') - filterParams.set('brv', version ?? 'null') - } else if (panelType === 'os') { - filterParams.set('os', parent ?? 'null') - filterParams.set('osv', version ?? 'null') + const networkSubTabs = useMemo( + () => [ + { id: 'isp', label: t('project.mapping.isp'), dimension: 'isp' }, + { + id: 'organization', + label: t('project.mapping.og'), + dimension: 'organization', + }, + { + id: 'user_type', + label: t('project.mapping.ut'), + dimension: 'user_type', + }, + { + id: 'connection_type', + label: t('project.mapping.ctp'), + dimension: 'connection_type', + }, + ], + [t], + ) + + const locationRowMapper = useCallback( + (entry: Entry, subTabId: string) => { + const { name: entryName, cc } = entry + + if (subTabId === 'locale') { + if (entryName === null) { + return + } + + const entryNameArray = entryName.split('-') + const displayName = getLocaleDisplayName(entryName, language) + + return ( + + ) } - return `?${filterParams.toString()}` + if (cc !== undefined) { + return ( + + ) + } + + return }, - [searchParams], + [language], ) - const createVersionDataMapping = useMemo(() => { - const browserDataSource = activeError?.params?.brv - const osDataSource = activeError?.params?.osv + const pagesRowMapper = useCallback( + (entry: Entry, subTabId: string) => { + const { name: entryName } = entry - const browserVersions: { [key: string]: Entry[] } = {} - const osVersions: { [key: string]: Entry[] } = {} + if (!entryName) { + return ( + + {subTabId === 'host' + ? t('project.unknownHost') + : t('common.notSet')} + + ) + } - if (browserDataSource) { - browserDataSource.forEach((entry: any) => { - const { br, name, count } = entry - if (!browserVersions[br]) { - browserVersions[br] = [] - } - browserVersions[br].push({ name, count }) - }) - } + let decodedUri = entryName - if (osDataSource) { - osDataSource.forEach((entry: any) => { - const { os, name, count } = entry - if (!osVersions[os]) { - osVersions[os] = [] - } - osVersions[os].push({ name, count }) - }) - } + try { + decodedUri = decodeURIComponent(entryName) + } catch { + // ignore + } - return { browserVersions, osVersions } - }, [activeError?.params?.brv, activeError?.params?.osv]) + if (subTabId === 'page' && project?.websiteUrl) { + return ( + + ) + } - const dataNames = useMemo( - () => ({ - occurrences: t('project.totalErrors'), - affectedUsers: t('project.affectedUsers'), - }), + return decodedUri + }, + [t, project?.websiteUrl], + ) + + const devicesRowMapper = useCallback( + (entry: Entry, subTabId: string) => { + const v1Tab = + subTabId === 'browser' ? 'br' : subTabId === 'os' ? 'os' : 'dv' + const mapper = getDeviceRowMapper(v1Tab, theme, t) as + | ((entry: Entry) => React.ReactNode) + | undefined + return mapper ? mapper(entry) : entry.name + }, + [theme, t], + ) + + const networkRowMapper = useCallback( + (entry: Entry, subTabId: string) => { + const { name: entryName } = entry + if (!entryName) { + return {t('common.notSet')} + } + if (subTabId === 'user_type') { + return getUsageTypeLabel(entryName, t) + } + if (subTabId === 'connection_type') { + return getConnectionTypeLabel(entryName, t) + } + return entryName + }, [t], ) @@ -1084,6 +1047,9 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { } if (activeEID) { + const detailsLoading = detailsQuery.isLoading + const detailsRefetching = detailsQuery.isFetching && !detailsQuery.isLoading + const resolveButton = allowedToManage && activeError && @@ -1096,7 +1062,7 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { 'group relative rounded-md border border-transparent bg-transparent px-3 py-1.5 text-sm font-medium text-gray-700 transition-all ring-inset hover:border-gray-300 hover:bg-white focus:z-10 focus:ring-1 focus:ring-slate-900 focus:outline-hidden dark:text-gray-50 hover:dark:border-slate-700/80 dark:hover:bg-slate-900 dark:focus:ring-slate-300', { 'cursor-not-allowed opacity-50': - errorLoading && !errorStatusUpdating, + detailsRefetching && !errorStatusUpdating, 'animate-pulse cursor-not-allowed': errorStatusUpdating, }, )} @@ -1114,7 +1080,7 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { 'group relative rounded-md border border-transparent bg-transparent px-3 py-1.5 text-sm font-medium text-gray-700 transition-all ring-inset hover:border-gray-300 hover:bg-white focus:z-10 focus:ring-1 focus:ring-slate-900 focus:outline-hidden dark:text-gray-50 hover:dark:border-slate-700/80 dark:hover:bg-slate-900 dark:focus:ring-slate-300', { 'cursor-not-allowed opacity-50': - errorLoading && !errorStatusUpdating, + detailsRefetching && !errorStatusUpdating, 'animate-pulse cursor-not-allowed': errorStatusUpdating, }, )} @@ -1125,9 +1091,6 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { return (
- {errorLoading && activeError ? : null} - {errorsLoading && !_isEmpty(errors) ? : null} - { - {activeError?.details ? ( - - } - /> + {detailsLoading ? ( + ) : null} -
- {!_isEmpty(activeError?.params) - ? _map(ERROR_PANELS_ORDER, (type: keyof typeof tnMapping) => { - if (type === 'location') { - const locationTabs = [ - { id: 'cc', label: t('project.mapping.cc') }, - { id: 'rg', label: t('project.mapping.rg') }, - { id: 'ct', label: t('project.mapping.ct') }, - { id: 'lc', label: t('project.mapping.lc') }, - { id: 'map', label: 'Map' }, - ] - - const rowMapper = (entry: CountryEntry) => { - const { name: entryName, cc } = entry - - if (errorsActiveTabs.location === 'lc') { - if (entryName === null) { - return - } - const entryNameArray = entryName.split('-') - const displayName = getLocaleDisplayName( - entryName, - language, - ) - - return ( - + - ) - } - - if (cc !== undefined) { - return ( - - ) - } - - return - } - - return ( - - setErrorsActiveTabs({ - ...errorsActiveTabs, - location: tab as 'cc' | 'rg' | 'ct' | 'lc' | 'map', - }) - } - activeTabId={errorsActiveTabs.location} - data={ - activeError?.params?.[errorsActiveTabs.location] || [] - } - rowMapper={rowMapper} - customRenderer={ - errorsActiveTabs.location === 'map' - ? () => { - const countryData = activeError?.params?.cc || [] - const regionData = activeError?.params?.rg || [] - const total = countryData.reduce( - (acc, curr) => acc + curr.count, - 0, - ) - - return ( - }> - { - const link = getFilterLink(type, key) - navigate(link) - }} - /> - - ) - } - : undefined - } - valuesHeaderName={t('project.occurrences')} - highlightColour='red' - /> - ) - } - - if (type === 'devices') { - const deviceTabs = [ - { id: 'br', label: t('project.mapping.br') }, - { id: 'os', label: t('project.mapping.os') }, - { id: 'dv', label: t('project.mapping.dv') }, - ] - - return ( - - setErrorsActiveTabs({ - ...errorsActiveTabs, - device: tab as 'br' | 'os' | 'dv', - }) - } - activeTabId={errorsActiveTabs.device} - data={ - activeError?.params?.[errorsActiveTabs.device] || [] - } - rowMapper={getDeviceRowMapper( - errorsActiveTabs.device, - theme, - t, - )} - capitalize={errorsActiveTabs.device === 'dv'} - versionData={ - errorsActiveTabs.device === 'br' - ? createVersionDataMapping.browserVersions - : errorsActiveTabs.device === 'os' - ? createVersionDataMapping.osVersions - : undefined - } - getVersionFilterLink={(parent, version) => - getVersionFilterLink( - parent, - version, - errorsActiveTabs.device === 'br' ? 'br' : 'os', - ) - } - valuesHeaderName={t('project.occurrences')} - highlightColour='red' - /> - ) - } - - if (type === 'network') { - const networkTabs = [ - { id: 'isp', label: t('project.mapping.isp') }, - { id: 'og', label: t('project.mapping.og') }, - { id: 'ut', label: t('project.mapping.ut') }, - { id: 'ctp', label: t('project.mapping.ctp') }, - ] - - const activeNetworkTab = errorsActiveTabs.network - - return ( - - setErrorsActiveTabs({ - ...errorsActiveTabs, - network: tab as 'isp' | 'og' | 'ut' | 'ctp', - }) - } - activeTabId={activeNetworkTab} - data={activeError?.params?.[activeNetworkTab] || []} - rowMapper={({ name: entryName }) => { - if (!entryName) { - return ( - {t('common.notSet')} - ) - } - if (activeNetworkTab === 'ut') { - return getUsageTypeLabel(entryName, t) - } - if (activeNetworkTab === 'ctp') { - return getConnectionTypeLabel(entryName, t) - } - return entryName - }} - valuesHeaderName={t('project.occurrences')} - highlightColour='red' - /> - ) - } - - if (type === 'pg') { - const pageTabs = [ - { id: 'pg', label: t('project.mapping.pg') }, - { id: 'host', label: t('project.mapping.host') }, - ] - - return ( - { - if (!entryName) { - return ( - - {errorsActiveTabs.page === 'pg' - ? t('common.notSet') - : t('project.unknownHost')} - - ) - } - - let decodedUri = entryName as string - - try { - decodedUri = decodeURIComponent(entryName) - } catch { - // do nothing - } - - return decodedUri - }} - name={t('project.pages')} - tabs={pageTabs} - onTabChange={(tab) => - setErrorsActiveTabs({ - ...errorsActiveTabs, - page: tab as 'pg' | 'host', - }) - } - activeTabId={errorsActiveTabs.page} - data={activeError?.params?.[errorsActiveTabs.page] || []} - valuesHeaderName={t('project.occurrences')} - highlightColour='red' - /> - ) - } - - return null - }) - : null} - {activeError?.metadata ? ( - - ) : null} -
+ ), + }, + { + key: 'firstSeen', + label: t('dashboard.firstSeen'), + value: + getRelativeDateIfPossible( + activeError.details.first_seen, + language, + ) || '-', + // A relative date is a sentence, not a figure — at the + // numeric card size it dwarfs the rest of the row. + valueClassName: 'text-xl sm:text-2xl', + }, + { + key: 'lastSeen', + label: t('dashboard.lastSeen'), + value: + getRelativeDateIfPossible( + activeError.details.last_seen, + language, + ) || '-', + valueClassName: 'text-xl sm:text-2xl', + }, + ]} + /> + } + /> +
+ ) : null} - {_isEmpty(activeError) && errorLoading ? : null} + {activeError?.details ? ( +
+ + {activeError?.metadata ? ( + + ) : null} +
+ ) : null} - {!errorLoading && _isEmpty(activeError) ? : null} + {!detailsLoading && !activeError?.details ? : null} setSessionsDrawer(null)} @@ -1463,24 +1206,6 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { ) } - // List view - Initial loading state - if ( - (overviewLoading === null || overviewLoading) && - !overview && - _isEmpty(errors) - ) { - return ( -
- -
- ) - } - const filtersDropdown = hasErrors ? ( { /> ) : null + if (isInitialLoading) { + return ( +
+ + } + /> + +
+ ) + } + return (
{ } /> - {(overviewLoading || errorsLoading) && (overview || !_isEmpty(errors)) ? ( - - ) : null} - {hasErrors ? : null} - {!hasErrors && errorsLoading === false && overviewLoading === false ? ( + {!hasErrors && !overviewQuery.isLoading && !errorsListQuery.isLoading ? ( ) : null} @@ -1538,7 +1278,8 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { <>
{chartOptions && overview?.chart ? ( -
+
+ {isRefetching ? : null}
) : null} @@ -1546,28 +1287,41 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => {
} - value={nFormatter(overview?.stats?.totalErrors || 0, 1)} + value={ + + } label={t('project.totalErrors')} /> } - value={`${overview?.stats?.errorRate || 0}%`} + value={} label={t('project.errorRate')} /> } - value={nFormatter(overview?.stats?.affectedUsers || 0, 1)} + value={ + + } label={t('project.affectedUsers')} /> } - value={nFormatter(overview?.stats?.affectedSessions || 0, 1)} + value={ + + } label={t('project.affectedSessions')} />
-
+
+ {isRefetching ? : null} {t('project.recentErrors')} @@ -1588,10 +1342,10 @@ const ErrorsViewInner = ({ deferredData }: ErrorsViewInnerProps) => { errorsListQuery.fetchNextPage()} + disabled={errorsListQuery.isFetching} />
diff --git a/web/app/pages/Project/tabs/Errors/types.ts b/web/app/pages/Project/tabs/Errors/types.ts new file mode 100644 index 000000000..2ec9ec085 --- /dev/null +++ b/web/app/pages/Project/tabs/Errors/types.ts @@ -0,0 +1,65 @@ +interface ErrorsChartData { + x: string[] + occurrences: number[] + affectedUsers: number[] +} + +export interface ErrorsOverviewData { + stats: { + totalErrors: number + uniqueErrors: number + affectedSessions: number + affectedUsers: number + errorRate: number + } + mostFrequentError: { + eid: string + name: string + message: string | null + count: number + usersAffected: number + lastSeen: string + } | null + chart: ErrorsChartData + timeBucket: string +} + +export interface ErrorDetailsInfo { + eid: string + name: string + message: string | null + filename: string | null + colno: number | null + lineno: number | null + stackTrace: string | null + count: number + status: 'active' | 'regressed' | 'resolved' | null + first_seen: string + last_seen: string + users?: number +} + +interface ErrorMetadataRow { + key: string + value: string + count: number +} + +export interface ErrorDetailsData { + details: ErrorDetailsInfo + metadata: ErrorMetadataRow[] + params: Record + chart: ErrorsChartData + timeBucket: string +} + +export interface ErrorSessionItem { + psid: string + profileId: string | null + country: string | null + browser: string | null + os: string | null + firstErrorAt: string + lastErrorAt: string + errorCount: number +} diff --git a/web/app/pages/Project/tabs/Experiments/ExperimentsView.tsx b/web/app/pages/Project/tabs/Experiments/ExperimentsView.tsx index 01f535c6e..444a9ded7 100644 --- a/web/app/pages/Project/tabs/Experiments/ExperimentsView.tsx +++ b/web/app/pages/Project/tabs/Experiments/ExperimentsView.tsx @@ -23,10 +23,10 @@ import { useFetcher, useLocation, useSearchParams } from 'react-router' import { toast } from 'sonner' import type { - AnalyticsFilter, ExperimentResults as ExperimentResultsSummary, ExperimentVariantResult, } from '~/api/api.server' +import type { V2Filter } from '~/api/v2/types' import { DOCS_URL } from '~/lib/constants' import DashboardHeader from '~/pages/Project/View/components/DashboardHeader' import { @@ -117,7 +117,7 @@ interface ExperimentRowProps { from: string to: string timezone?: string - filters: AnalyticsFilter[] + filters: V2Filter[] } const normaliseProbability = (value: number) => { @@ -166,7 +166,7 @@ const ExperimentWinningRail = ({ from: string to: string timezone?: string - filters: AnalyticsFilter[] + filters: V2Filter[] }) => { const { t } = useTranslation() const { diff --git a/web/app/pages/Project/tabs/Funnels/FunnelsList.tsx b/web/app/pages/Project/tabs/Funnels/FunnelsList.tsx index 498474ecd..9a19864af 100644 --- a/web/app/pages/Project/tabs/Funnels/FunnelsList.tsx +++ b/web/app/pages/Project/tabs/Funnels/FunnelsList.tsx @@ -10,34 +10,30 @@ import { ArrowRightIcon, CaretDownIcon, } from '@phosphor-icons/react' -import { - Suspense, - use, - useEffect, - useMemo, - useRef, - type ReactNode, -} from 'react' +import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' -import type { FunnelDataResponse } from '~/api/api.server' -import type { Funnel } from '~/lib/models/Project' +import type { AnalyticsFunnel, Funnel } from '~/lib/models/Project' import { FunnelChart } from '~/pages/Project/tabs/Funnels/FunnelChart' +import { RefetchIndicator } from '~/pages/Project/View/v2/loading' import { useAuth } from '~/providers/AuthProvider' import Button from '~/ui/Button' -import Spin from '~/ui/icons/Spin' +import Loader from '~/ui/Loader' import { Text } from '~/ui/Text' import { nLocaleFormatter } from '~/utils/generic' +export interface FunnelData { + funnel: AnalyticsFunnel[] + totalPageviews: number +} + interface FunnelsListProps { funnels?: Funnel[] activeFunnelId: string | null - funnelDataPromise?: Promise + funnelData: FunnelData | null + funnelDataLoading: boolean + funnelDataRefetching: boolean onToggleFunnel: (id: string) => void - onFunnelDataResolved: ( - funnelId: string, - funnelData: FunnelDataResponse | null, - ) => void onBarClick: (stepIndex: number) => void openFunnelSettings: (funnel?: Funnel) => void deleteFunnel: (id: string) => void @@ -48,12 +44,10 @@ interface FunnelsListProps { interface FunnelCardProps { funnel: Funnel isExpanded: boolean - funnelDataPromise?: Promise + funnelData: FunnelData | null + funnelDataLoading: boolean + funnelDataRefetching: boolean onToggleFunnel: (id: string) => void - onFunnelDataResolved: ( - funnelId: string, - funnelData: FunnelDataResponse | null, - ) => void onBarClick: (stepIndex: number) => void openFunnelSettings: (funnel: Funnel) => void deleteFunnel: (id: string) => void @@ -67,7 +61,7 @@ interface AddFunnelProps { const STEPS_MAX_HEIGHT = 42 -const getFunnelSummary = (funnelData?: FunnelDataResponse | null) => { +const getFunnelSummary = (funnelData?: FunnelData | null) => { if (!funnelData || _isEmpty(funnelData.funnel)) { return null } @@ -87,35 +81,6 @@ const getFunnelSummary = (funnelData?: FunnelDataResponse | null) => { } } -const FunnelDataResolver = ({ - funnelId, - funnelDataPromise, - onFunnelDataResolved, - children, -}: { - funnelId: string - funnelDataPromise?: Promise - onFunnelDataResolved: ( - funnelId: string, - funnelData: FunnelDataResponse | null, - ) => void - children: (funnelData: FunnelDataResponse | null) => ReactNode -}) => { - const funnelData = funnelDataPromise ? use(funnelDataPromise) : null - - useEffect(() => { - onFunnelDataResolved(funnelId, funnelData) - }, [funnelData, funnelId, onFunnelDataResolved]) - - return <>{children(funnelData)} -} - -const FunnelChartLoading = () => ( -
- -
-) - const FunnelSteps = ({ steps }: { steps: string[] }) => { const containerRef = useRef(null) @@ -165,7 +130,7 @@ const FunnelExpandedChart = ({ funnelData, onBarClick, }: { - funnelData: FunnelDataResponse | null + funnelData: FunnelData | null onBarClick: (stepIndex: number) => void }) => { const { t } = useTranslation('common') @@ -220,9 +185,10 @@ const FunnelExpandedChart = ({ const FunnelCard = ({ funnel, isExpanded, - funnelDataPromise, + funnelData, + funnelDataLoading, + funnelDataRefetching, onToggleFunnel, - onFunnelDataResolved, onBarClick, openFunnelSettings, deleteFunnel, @@ -233,6 +199,7 @@ const FunnelCard = ({ return (
  • + {isExpanded && funnelDataRefetching ? : null}
    ) : null} @@ -360,9 +317,10 @@ const AddFunnel = ({ openFunnelSettings }: AddFunnelProps) => { const FunnelsList = ({ funnels, activeFunnelId, - funnelDataPromise, + funnelData, + funnelDataLoading, + funnelDataRefetching, onToggleFunnel, - onFunnelDataResolved, onBarClick, openFunnelSettings, deleteFunnel, @@ -378,9 +336,10 @@ const FunnelsList = ({ key={funnel.id} funnel={funnel} isExpanded={activeFunnelId === funnel.id} - funnelDataPromise={funnelDataPromise} + funnelData={funnelData} + funnelDataLoading={funnelDataLoading} + funnelDataRefetching={funnelDataRefetching} onToggleFunnel={onToggleFunnel} - onFunnelDataResolved={onFunnelDataResolved} onBarClick={onBarClick} deleteFunnel={deleteFunnel} openFunnelSettings={openFunnelSettings} diff --git a/web/app/pages/Project/tabs/Funnels/FunnelsView.tsx b/web/app/pages/Project/tabs/Funnels/FunnelsView.tsx index 482f0749b..ea1790eeb 100644 --- a/web/app/pages/Project/tabs/Funnels/FunnelsView.tsx +++ b/web/app/pages/Project/tabs/Funnels/FunnelsView.tsx @@ -1,3 +1,4 @@ +import { useQueryClient } from '@tanstack/react-query' import _filter from 'lodash/filter' import _find from 'lodash/find' import _includes from 'lodash/includes' @@ -5,39 +6,29 @@ import _isEmpty from 'lodash/isEmpty' import { FunnelIcon, PlusIcon } from '@phosphor-icons/react' import { useState, useEffect, useMemo, useCallback, useRef } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { - useSearchParams, - useFetcher, - useLoaderData, - useRevalidator, -} from 'react-router' +import { useSearchParams, useFetcher } from 'react-router' import { toast } from 'sonner' -import type { FunnelDataResponse } from '~/api/api.server' +import { useFunnelQuery } from '~/hooks/v2/useV2Queries' import { DOCS_URL, FUNNELS_PERIOD_PAIRS } from '~/lib/constants' -import type { Funnel } from '~/lib/models/Project' +import type { AnalyticsFunnel, Funnel } from '~/lib/models/Project' import NewFunnel from '~/modals/NewFunnel' -import FunnelsList from '~/pages/Project/tabs/Funnels/FunnelsList' +import FunnelsList, { + type FunnelData, +} from '~/pages/Project/tabs/Funnels/FunnelsList' import DashboardHeader from '~/pages/Project/View/components/DashboardHeader' import Filters from '~/pages/Project/View/components/Filters' import ProjectViewHeaderActions from '~/pages/Project/View/components/ProjectViewHeaderActions' import TabErrorBoundary from '~/pages/Project/View/components/TabErrorBoundary' -import { - useViewProjectContext, - useRefreshTriggers, -} from '~/pages/Project/View/ViewProject' +import { useViewProjectContext } from '~/pages/Project/View/ViewProject' import { useAuth } from '~/providers/AuthProvider' import { useCurrentProject, useProjectPassword, } from '~/providers/CurrentProjectProvider' -import type { - ProjectLoaderData, - ProjectViewActionData, -} from '~/routes/projects.$id' +import type { ProjectViewActionData } from '~/routes/projects.$id' import Button from '~/ui/Button' import { Link } from '~/ui/Link' -import LoadingBar from '~/ui/LoadingBar' import { Text } from '~/ui/Text' import routes from '~/utils/routes' @@ -50,26 +41,21 @@ interface FunnelsViewProps { const FUNNELS_DOCS_URL = `${DOCS_URL}/analytics-dashboard/funnels` const FunnelsView = ({ tnMapping }: FunnelsViewProps) => { - const { funnelData: funnelDataPromise } = useLoaderData() const { id, projectPath, project, mergeProject, allowedToManage } = useCurrentProject() const projectPassword = useProjectPassword(id) - const revalidator = useRevalidator() const { isAuthenticated } = useAuth() - const { funnelsRefreshTrigger } = useRefreshTriggers() const { periodPairs, period, timezone, timeFormat, filters } = useViewProjectContext() const { t } = useTranslation('common') const [searchParams, setSearchParams] = useSearchParams() + const queryClient = useQueryClient() const fetcher = useFetcher() const lastHandledData = useRef(null) const deletedFunnelId = useRef(null) - const lastRefreshTrigger = useRef(funnelsRefreshTrigger) const [isNewFunnelOpened, setIsNewFunnelOpened] = useState(false) const [funnelToEdit, setFunnelToEdit] = useState() - const [funnelAnalytics, setFunnelAnalytics] = - useState(null) const [sessionsDrawer, setSessionsDrawer] = useState<{ stepIndex: number label: string @@ -96,15 +82,41 @@ const FunnelsView = ({ tnMapping }: FunnelsViewProps) => { return _find(project.funnels, (funnel) => funnel.id === funnelId) || null }, [searchParams, project]) + const funnelQuery = useFunnelQuery(activeFunnel?.id ?? null) + + const displayedFunnelIdRef = useRef(null) + if (funnelQuery.data && !funnelQuery.isPlaceholderData) { + displayedFunnelIdRef.current = activeFunnel?.id ?? null + } + const isStaleFunnelData = + funnelQuery.isPlaceholderData && + displayedFunnelIdRef.current !== (activeFunnel?.id ?? null) + + const funnelAnalytics = useMemo(() => { + const data = funnelQuery.data?.data + + if (!data) { + return null + } + + return { + funnel: (data.steps || []) as AnalyticsFunnel[], + totalPageviews: Number(data.totalPageviews || 0), + } + }, [funnelQuery.data]) + + const displayedFunnelAnalytics = isStaleFunnelData ? null : funnelAnalytics + const funnelDataLoading = funnelQuery.isLoading || isStaleFunnelData + const funnelDataRefetching = + funnelQuery.isFetching && !funnelDataLoading && Boolean(funnelQuery.data) + const funnelActionLoading = fetcher.state !== 'idle' - const analyticsLoading = revalidator.state === 'loading' const hasFunnels = !_isEmpty(project?.funnels) - const resetKey = `funnels:${searchParams.toString()}:${funnelsRefreshTrigger}` + const resetKey = `funnels:${searchParams.toString()}` const clearActiveFunnel = useCallback(() => { const nextSearchParams = new URLSearchParams(searchParams.toString()) nextSearchParams.delete('funnelId') - setFunnelAnalytics(null) setSessionsDrawer(null) setSearchParams(nextSearchParams) }, [searchParams, setSearchParams]) @@ -119,7 +131,6 @@ const FunnelsView = ({ tnMapping }: FunnelsViewProps) => { nextSearchParams.set('funnelId', funnelId) } - setFunnelAnalytics(null) setSessionsDrawer(null) setSearchParams(nextSearchParams) }, @@ -136,17 +147,7 @@ const FunnelsView = ({ tnMapping }: FunnelsViewProps) => { setFunnelToEdit(undefined) }, []) - const handleFunnelDataResolved = useCallback( - (funnelId: string, funnelData: FunnelDataResponse | null) => { - if (activeFunnel?.id === funnelId) { - setFunnelAnalytics(funnelData) - } - }, - [activeFunnel?.id], - ) - useEffect(() => { - setFunnelAnalytics(null) setSessionsDrawer(null) }, [activeFunnel?.id]) @@ -173,10 +174,7 @@ const FunnelsView = ({ tnMapping }: FunnelsViewProps) => { { method: 'POST', action: projectPath }, ) - const updatedFunnelId = (fetcher.data.data as { id?: string })?.id - if (updatedFunnelId && activeFunnel?.id === updatedFunnelId) { - revalidator.revalidate() - } + queryClient.invalidateQueries({ queryKey: ['v2', id, 'funnel'] }) } else if (intent === 'delete-funnel') { toast.success(t('apiNotifications.funnelDeleted')) fetcher.submit( @@ -205,9 +203,9 @@ const FunnelsView = ({ tnMapping }: FunnelsViewProps) => { projectPath, fetcher, activeFunnel?.id, - revalidator, clearActiveFunnel, mergeProject, + queryClient, ]) const onFunnelCreate = (name: string, steps: string[]) => { @@ -240,24 +238,17 @@ const FunnelsView = ({ tnMapping }: FunnelsViewProps) => { const handleBarClick = useCallback( (stepIndex: number) => { - if (!funnelAnalytics?.funnel?.[stepIndex]) return + if (!displayedFunnelAnalytics?.funnel?.[stepIndex]) return - const step = funnelAnalytics.funnel[stepIndex] + const step = displayedFunnelAnalytics.funnel[stepIndex] setSessionsDrawer({ stepIndex, label: step.value, }) }, - [funnelAnalytics], + [displayedFunnelAnalytics], ) - useEffect(() => { - if (funnelsRefreshTrigger > lastRefreshTrigger.current) { - lastRefreshTrigger.current = funnelsRefreshTrigger - revalidator.revalidate() - } - }, [funnelsRefreshTrigger, revalidator]) - return ( { rightContent={} /> - {activeFunnel && analyticsLoading && funnelAnalytics ? ( - - ) : null} {hasFunnels ? ( { dropoff={Boolean(sessionsDrawer?.dropoff)} showDropoffToggle={ !!sessionsDrawer && - sessionsDrawer.stepIndex < (funnelAnalytics?.funnel?.length || 0) - 1 + sessionsDrawer.stepIndex < + (displayedFunnelAnalytics?.funnel?.length || 0) - 1 } onDropoffChange={(dropoff) => setSessionsDrawer((current) => @@ -377,8 +367,10 @@ const FunnelsView = ({ tnMapping }: FunnelsViewProps) => { totalCount={ sessionsDrawer ? sessionsDrawer.dropoff - ? funnelAnalytics?.funnel?.[sessionsDrawer.stepIndex + 1]?.dropoff - : funnelAnalytics?.funnel?.[sessionsDrawer.stepIndex]?.events + ? displayedFunnelAnalytics?.funnel?.[sessionsDrawer.stepIndex + 1] + ?.dropoff + : displayedFunnelAnalytics?.funnel?.[sessionsDrawer.stepIndex] + ?.events : undefined } /> diff --git a/web/app/pages/Project/tabs/Goals/GoalSettingsModal.tsx b/web/app/pages/Project/tabs/Goals/GoalSettingsModal.tsx index 4f516cf48..df44a7296 100644 --- a/web/app/pages/Project/tabs/Goals/GoalSettingsModal.tsx +++ b/web/app/pages/Project/tabs/Goals/GoalSettingsModal.tsx @@ -23,7 +23,8 @@ import Input from '~/ui/Input' import Loader from '~/ui/Loader' import Select from '~/ui/Select' import { Text } from '~/ui/Text' -import type { Filter as FilterType } from '../../View/interfaces/traffic' +import type { V2Filter, V2FilterOperator } from '~/api/v2/types' +import { ALL_VALID_DIMENSIONS, V1_TO_V2_DIMENSION } from '~/lib/v2Dimensions' import FilterRowsEditor from '../../View/components/FilterRowsEditor' const GOAL_TYPES = [ @@ -47,134 +48,146 @@ const CONDITION_RELATIONS: { value: GoalConditionRelation; label: string }[] = [ ] const GOAL_CONDITION_FILTER_OPTIONS = [ - 'pg', - 'ev', + 'page', + 'event', 'host', - 'cc', - 'dv', - 'br', + 'country', + 'device', + 'browser', 'os', - 'ref', - 'so', - 'me', - 'ca', - 'te', - 'co', - 'tag:key', - 'ev:key', + 'referrer', + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_term', + 'utm_content', + 'page_property', + 'event_metadata', ] const createConditionId = () => globalThis.crypto?.randomUUID?.() || Math.random().toString(36).slice(2) -const filterToConditionOperator = ({ - isExclusive, - isContains, -}: FilterType): GoalCondition['operator'] => { - if (isContains && isExclusive) return 'not_contains' - if (isContains) return 'contains' - if (isExclusive) return 'not_equals' +const filterToConditionOperator = ( + operator: V2FilterOperator, +): GoalCondition['operator'] => { + if (operator === 'contains_not') return 'not_contains' + if (operator === 'contains') return 'contains' + if (operator === 'is_not') return 'not_equals' return 'equals' } -const conditionOperatorToFilterFlags = ( +const conditionOperatorToV2Operator = ( operator: GoalCondition['operator'], -) => ({ - isExclusive: operator === 'not_equals' || operator === 'not_contains', - isContains: operator === 'contains' || operator === 'not_contains', -}) +): V2FilterOperator => { + if (operator === 'not_contains') return 'contains_not' + if (operator === 'contains') return 'contains' + if (operator === 'not_equals') return 'is_not' + return 'is' +} + +const getMetadataConditionDimension = (eventType: GoalCondition['eventType']) => + eventType === 'custom_event' ? 'event_metadata' : 'page_property' -const getMetadataConditionPrefix = (eventType: GoalCondition['eventType']) => - eventType === 'custom_event' ? 'ev:key' : 'tag:key' +const filterToCondition = (filter: V2Filter): GoalCondition | null => { + const value = typeof filter.value === 'string' ? filter.value : '' -const filterToCondition = (filter: FilterType): GoalCondition | null => { - if (!filter.column || !filter.filter) return null + if (!filter.dimension || !value) return null - if (filter.column === 'ev') { + if (filter.dimension === 'event') { return { id: createConditionId(), eventType: 'custom_event', field: 'event_name', - operator: filterToConditionOperator(filter), - value: filter.filter, - } - } - - if (filter.column === 'ev:key' || filter.column === 'tag:key') { - return { - id: createConditionId(), - eventType: filter.column === 'ev:key' ? 'custom_event' : 'pageview', - field: 'metadata', - operator: filter.isExclusive ? 'not_exists' : 'exists', - metadataKey: filter.filter, + operator: filterToConditionOperator(filter.operator), + value, } } if ( - filter.column.startsWith('ev:key:') || - filter.column.startsWith('tag:key:') + filter.dimension === 'event_metadata' || + filter.dimension === 'page_property' ) { + const eventType = + filter.dimension === 'event_metadata' ? 'custom_event' : 'pageview' + + if (!filter.key) { + return { + id: createConditionId(), + eventType, + field: 'metadata', + operator: filter.operator === 'is_not' ? 'not_exists' : 'exists', + metadataKey: value, + } + } + return { id: createConditionId(), - eventType: filter.column.startsWith('ev:key:') - ? 'custom_event' - : 'pageview', + eventType, field: 'metadata', - operator: filterToConditionOperator(filter), - metadataKey: filter.column - .replace(/^ev:key:/, '') - .replace(/^tag:key:/, ''), - value: filter.filter, + operator: filterToConditionOperator(filter.operator), + metadataKey: filter.key, + value, } } return { id: createConditionId(), - eventType: filter.column === 'pg' ? 'pageview' : 'any', - field: filter.column, - operator: filterToConditionOperator(filter), - value: filter.filter, + eventType: filter.dimension === 'page' ? 'pageview' : 'any', + field: filter.dimension, + operator: filterToConditionOperator(filter.operator), + value, } } -const conditionToFilter = (condition: GoalCondition): FilterType | null => { +const conditionToFilter = (condition: GoalCondition): V2Filter | null => { if (condition.field === 'metadata') { if (!condition.metadataKey) return null - const prefix = getMetadataConditionPrefix(condition.eventType) + const dimension = getMetadataConditionDimension(condition.eventType) if ( condition.operator === 'exists' || condition.operator === 'not_exists' ) { return { - column: prefix, - filter: condition.metadataKey, - isExclusive: condition.operator === 'not_exists', - isContains: false, + dimension, + operator: condition.operator === 'not_exists' ? 'is_not' : 'is', + value: condition.metadataKey, } } if (!condition.value) return null - const flags = conditionOperatorToFilterFlags(condition.operator) return { - column: `${prefix}:${condition.metadataKey}`, - filter: condition.value, - ...flags, + dimension, + key: condition.metadataKey, + operator: conditionOperatorToV2Operator(condition.operator), + value: condition.value, } } if (!condition.value) return null - const flags = conditionOperatorToFilterFlags(condition.operator) + + if (condition.field === 'event' || condition.field === 'event_name') { + return { + dimension: 'event', + operator: conditionOperatorToV2Operator(condition.operator), + value: condition.value, + } + } + + const operator = conditionOperatorToV2Operator(condition.operator) + + const dimension = V1_TO_V2_DIMENSION[condition.field] ?? condition.field + if (!ALL_VALID_DIMENSIONS.includes(dimension)) { + return null + } return { - column: - condition.field === 'event' || condition.field === 'event_name' - ? 'ev' - : condition.field, - filter: condition.value, - ...flags, + dimension, + operator, + value: condition.value, } } @@ -215,9 +228,9 @@ const GoalSettingsModal = ({ const [conditionsRelation, setConditionsRelation] = useState('AND') const [conditionInitialFilters, setConditionInitialFilters] = useState< - FilterType[] + V2Filter[] >([]) - const [conditionFilters, setConditionFilters] = useState([]) + const [conditionFilters, setConditionFilters] = useState([]) const [unmappedConditions, setUnmappedConditions] = useState( [], ) @@ -225,7 +238,7 @@ const GoalSettingsModal = ({ const isSaving = fetcher.state === 'submitting' - const resetConditionFilters = useCallback((filters: FilterType[] = []) => { + const resetConditionFilters = useCallback((filters: V2Filter[] = []) => { setConditionInitialFilters(filters) setConditionFilters(filters) setUnmappedConditions([]) @@ -260,7 +273,7 @@ const GoalSettingsModal = ({ goal.conditions?.conditions?.length ? 'conditions' : 'simple', ) setConditionsRelation(goal.conditions?.relation || 'AND') - const mappedFilters: FilterType[] = [] + const mappedFilters: V2Filter[] = [] const nextUnmappedConditions: GoalCondition[] = [] const loadedConditions = goal.conditions?.conditions || [] @@ -331,6 +344,23 @@ const GoalSettingsModal = ({ e.preventDefault() processedRef.current = null + const mappedConditions: GoalCondition[] = [] + let hasUnmappableCondition = false + + conditionFilters.forEach((filter) => { + const condition = filterToCondition(filter) + if (condition) { + mappedConditions.push(condition) + } else { + hasUnmappableCondition = true + } + }) + + if (definitionMode === 'conditions' && hasUnmappableCondition) { + toast.error(t('goals.conditionValueRequired')) + return + } + const formData = new FormData() formData.set('intent', isNew ? 'create-goal' : 'update-goal') formData.set('name', name) @@ -344,9 +374,6 @@ const GoalSettingsModal = ({ ), ) - const mappedConditions = conditionFilters - .map(filterToCondition) - .filter((condition): condition is GoalCondition => Boolean(condition)) const goalConditions: GoalConditions | null = definitionMode === 'conditions' && (mappedConditions.length > 0 || unmappedConditions.length > 0) diff --git a/web/app/pages/Project/tabs/Goals/GoalsView.tsx b/web/app/pages/Project/tabs/Goals/GoalsView.tsx index 8194b1652..f1605600b 100644 --- a/web/app/pages/Project/tabs/Goals/GoalsView.tsx +++ b/web/app/pages/Project/tabs/Goals/GoalsView.tsx @@ -77,6 +77,7 @@ import Spin from '~/ui/icons/Spin' import InfiniteScrollTrigger from '~/ui/InfiniteScrollTrigger' import LoadingBar from '~/ui/LoadingBar' import Modal from '~/ui/Modal' +import { DurationFlow } from '~/ui/NumberFlow' import ProgressRing from '~/ui/ProgressRing' import StatusPage from '~/ui/StatusPage' import { Text } from '~/ui/Text' @@ -682,7 +683,11 @@ const GoalRow = ({ weight='bold' className='leading-none whitespace-nowrap' > - {formatDuration(metric?.median)} + {hasTiming ? ( + + ) : ( + formatDuration(metric?.median) + )}
    diff --git a/web/app/pages/Project/tabs/Performance/PerformanceMap.tsx b/web/app/pages/Project/tabs/Performance/PerformanceMap.tsx new file mode 100644 index 000000000..d16de3cda --- /dev/null +++ b/web/app/pages/Project/tabs/Performance/PerformanceMap.tsx @@ -0,0 +1,71 @@ +import { Suspense, lazy, useMemo } from 'react' +import { useNavigate } from 'react-router' + +import { useBreakdownQuery } from '~/hooks/v2/useV2Queries' +import { MapLoader } from '~/pages/Project/View/components/MapLoader' +import { mapBreakdownRows } from '~/pages/Project/View/v2/adapters' +import { useViewProjectContext } from '~/pages/Project/View/ViewProject' + +const InteractiveMap = lazy( + () => import('~/pages/Project/View/components/InteractiveMap'), +) + +const MAP_CLICK_TYPE_TO_DIMENSION = { cc: 'country', rg: 'region' } as const + +export const PerformanceMap = ({ + isFullscreen, + measure, +}: { + isFullscreen: boolean + measure?: string +}) => { + const navigate = useNavigate() + const { getFilterLink, setIsMapFullscreen } = useViewProjectContext() + + const countryQuery = useBreakdownQuery('performance', { + dimension: 'country', + metrics: ['load_time'], + measure, + limit: 100, + sort: 'load_time:desc', + }) + const regionQuery = useBreakdownQuery('performance', { + dimension: 'region', + metrics: ['load_time'], + measure, + limit: 100, + sort: 'load_time:desc', + }) + + const countryData = useMemo( + () => mapBreakdownRows(countryQuery.data?.data, 'load_time'), + [countryQuery.data], + ) + const regionData = useMemo( + () => mapBreakdownRows(regionQuery.data?.data, 'load_time'), + [regionQuery.data], + ) + const total = useMemo( + () => countryData.reduce((acc, curr) => acc + curr.count, 0), + [countryData], + ) + + if (countryQuery.isLoading) { + return + } + + return ( + }> + { + navigate(getFilterLink(MAP_CLICK_TYPE_TO_DIMENSION[mapType], key)) + }} + onFullscreenToggle={setIsMapFullscreen} + isFullscreen={isFullscreen} + /> + + ) +} diff --git a/web/app/pages/Project/tabs/Performance/PerformanceView.tsx b/web/app/pages/Project/tabs/Performance/PerformanceView.tsx index b00d5db8e..c0972cd6c 100644 --- a/web/app/pages/Project/tabs/Performance/PerformanceView.tsx +++ b/web/app/pages/Project/tabs/Performance/PerformanceView.tsx @@ -1,38 +1,27 @@ import cx from 'clsx' import dayjs from 'dayjs' import _isEmpty from 'lodash/isEmpty' -import _keys from 'lodash/keys' -import _map from 'lodash/map' import { EyeIcon, PercentIcon } from '@phosphor-icons/react' -import { - useState, - useEffect, - useMemo, - useRef, - useCallback, - lazy, - Suspense, - use, -} from 'react' +import React, { useState, useMemo, useRef, useCallback } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' +import { useSearchParams } from 'react-router' + import { - useNavigate, - useSearchParams, - useLoaderData, - useRevalidator, -} from 'react-router' - -import type { - PerformanceDataResponse, - PerformanceOverallObject, -} from '~/api/api.server' + useCompareSummaryQuery, + useCompareTimeseriesQuery, + useSummaryQuery, + useTimeseriesQuery, +} from '~/hooks/v2/useV2Queries' import { useAnnotations } from '~/hooks/useAnnotations' -import { PERFORMANCE_PANELS_ORDER } from '~/lib/constants' -import { CountryEntry } from '~/lib/models/Entry' -import { OverallPerformanceObject } from '~/lib/models/Project' +import { Entry } from '~/lib/models/Entry' import AnnotationModal from '~/modals/AnnotationModal' import { PerformanceChart } from '~/pages/Project/tabs/Performance/PerformanceChart' +import { PerformanceMap } from '~/pages/Project/tabs/Performance/PerformanceMap' +import { + perfSummaryToOverall, + pivotPerformanceTimeseries, +} from '~/pages/Project/tabs/Performance/perfAdapters' import { PerformanceMetricCards } from '~/pages/Project/tabs/Traffic/MetricCards' import { SessionsDrawer } from '~/pages/Project/tabs/Traffic/SessionsDrawer' import PageLinkRow from '~/pages/Project/tabs/Traffic/PageLinkRow' @@ -40,15 +29,15 @@ import CCRow from '~/pages/Project/View/components/CCRow' import { ChartContextMenu } from '~/pages/Project/View/components/ChartContextMenu' import DashboardHeader from '~/pages/Project/View/components/DashboardHeader' import Filters from '~/pages/Project/View/components/Filters' -import { MapLoader } from '~/pages/Project/View/components/MapLoader' import NoEvents from '~/pages/Project/View/components/NoEvents' import ProjectViewHeaderActions from '~/pages/Project/View/components/ProjectViewHeaderActions' import TabErrorBoundary from '~/pages/Project/View/components/TabErrorBoundary' -import { Panel } from '~/pages/Project/View/Panels' import { - useViewProjectContext, - useRefreshTriggers, -} from '~/pages/Project/View/ViewProject' + BreakdownPanel, + BreakdownSubTab, +} from '~/pages/Project/View/v2/BreakdownPanel' +import { RefetchIndicator } from '~/pages/Project/View/v2/loading' +import { useViewProjectContext } from '~/pages/Project/View/ViewProject' import { panelIconMapping, CHART_METRICS_MAPPING_PERF, @@ -60,96 +49,32 @@ import { import { getChartPointWindow } from '~/pages/Project/View/utils/chartPoint' import { useCurrentProject } from '~/providers/CurrentProjectProvider' import { useTheme } from '~/providers/ThemeProvider' -import type { ProjectLoaderData } from '~/routes/projects.$id' import Dropdown from '~/ui/Dropdown' -import LoadingBar from '~/ui/LoadingBar' +import Loader from '~/ui/Loader' import { getStringFromTime, getTimeFromSeconds } from '~/utils/generic' -import { LoaderView } from '../../View/components/LoaderView' import { ChartTypeSwitcher } from '../../View/components/ChartTypeSwitcher' import WaitingForAnEvent from '../../View/components/WaitingForAnEvent' -const InteractiveMap = lazy( - () => import('~/pages/Project/View/components/InteractiveMap'), -) - interface PerformanceViewProps { tnMapping: Record } -interface DeferredPerfData { - perfData: PerformanceDataResponse | null - perfOverallStats: Record | null - perfCompareData: PerformanceDataResponse | null - perfOverallCompareStats: Record | null -} - -function PerfDataResolver({ - children, -}: { - children: (data: DeferredPerfData) => React.ReactNode -}) { - const { - perfData: perfDataPromise, - perfOverallStats: perfOverallStatsPromise, - perfCompareData: perfCompareDataPromise, - perfOverallCompareStats: perfOverallCompareStatsPromise, - } = useLoaderData() - - const perfData = perfDataPromise ? use(perfDataPromise) : null - const perfOverallStats = perfOverallStatsPromise - ? use(perfOverallStatsPromise) - : null - const perfCompareData = perfCompareDataPromise - ? use(perfCompareDataPromise) - : null - const perfOverallCompareStats = perfOverallCompareStatsPromise - ? use(perfOverallCompareStatsPromise) - : null - - return ( - <> - {children({ - perfData, - perfOverallStats, - perfCompareData, - perfOverallCompareStats, - })} - - ) -} - function PerformanceViewWrapper(props: PerformanceViewProps) { const [searchParams] = useSearchParams() - const { performanceRefreshTrigger } = useRefreshTriggers() - const resetKey = `performance:${searchParams.toString()}:${performanceRefreshTrigger}` + const resetKey = `performance:${searchParams.toString()}` return ( - }> - - {(deferredData) => ( - - )} - - + ) } -interface PerformanceViewInnerProps extends PerformanceViewProps { - deferredData: DeferredPerfData -} - -const PerformanceViewInner = ({ - tnMapping, - deferredData, -}: PerformanceViewInnerProps) => { +const PerformanceViewInner = ({ tnMapping }: PerformanceViewProps) => { const { id, project, allowedToManage } = useCurrentProject() - const revalidator = useRevalidator() - const { performanceRefreshTrigger } = useRefreshTriggers() const { timezone, filters, @@ -163,10 +88,8 @@ const PerformanceViewInner = ({ onMainChartZoom, shouldEnableZoom, // Filter functions from context - getFilterLink, getVersionFilterLink, isMapFullscreen, - setIsMapFullscreen, fullscreenMapRef, } = useViewProjectContext() @@ -191,68 +114,16 @@ const PerformanceViewInner = ({ i18n: { language }, } = useTranslation('common') const { theme } = useTheme() - const navigate = useNavigate() const [searchParams, setSearchParams] = useSearchParams() - // State for data - initialized from loader, can be overridden by client-side fetches - const [dataLoading, setDataLoading] = useState(false) - const [panelsData, setPanelsData] = useState(() => { - if (deferredData.perfData?.params) { - return { - types: _keys(deferredData.perfData.params), - data: deferredData.perfData.params, - } - } - return { data: {} } - }) - const [chartData, setChartData] = useState( - () => deferredData.perfData?.chart || {}, - ) - const [overall, setOverall] = useState>( - () => { - if (deferredData.perfOverallStats && id) { - return deferredData.perfOverallStats[id] || {} - } - return {} - }, - ) const [sessionsDrawer, setSessionsDrawer] = useState<{ from: string to: string label: string } | null>(null) - // Track if we've ever shown actual content to prevent NoEvents flash during exit animation const hasShownContentRef = useRef(false) - const isPanelsDataEmptyRaw = _isEmpty(panelsData.data) - - // Track when we've shown content to prevent NoEvents flash during exit animation - if (!isPanelsDataEmptyRaw) { - hasShownContentRef.current = true - } - - // Don't show NoEvents if we've previously shown content (prevents flash during tab switch) - const isPanelsDataEmpty = isPanelsDataEmptyRaw && !hasShownContentRef.current - - // Sync state when loader provides new data (e.g., after URL changes) - useEffect(() => { - if (revalidator.state === 'idle' && deferredData.perfData) { - const { chart, params } = deferredData.perfData - setChartData(chart || {}) - setPanelsData({ - types: _keys(params), - data: params, - }) - if (deferredData.perfOverallStats && id) { - setOverall(deferredData.perfOverallStats[id] || {}) - } - setDataLoading(false) - } else if (revalidator.state === 'loading') { - setDataLoading(true) - } - }, [revalidator.state, deferredData, id]) - // Get chart metrics and measure from URL params (SSR-friendly) const activeChartMetrics = searchParams.get('perfMetric') || CHART_METRICS_MAPPING_PERF.timing @@ -272,12 +143,76 @@ const PerformanceViewInner = ({ setSearchParams(newParams) } - const overallCompare: Partial = useMemo(() => { - if (deferredData.perfOverallCompareStats && id) { - return deferredData.perfOverallCompareStats[id] || {} - } - return {} - }, [deferredData.perfOverallCompareStats, id]) + const effectiveMeasure = + activeChartMetrics === CHART_METRICS_MAPPING_PERF.quantiles + ? CHART_MEASURES_MAPPING_PERF.quantiles + : activeMeasure + const panelsMeasure = + effectiveMeasure === CHART_MEASURES_MAPPING_PERF.quantiles + ? CHART_MEASURES_MAPPING_PERF.median + : activeMeasure + + const summaryQuery = useSummaryQuery('performance', { + measure: activeMeasure, + }) + const compareSummaryQuery = useCompareSummaryQuery('performance', { + measure: activeMeasure, + }) + const timeseriesQuery = useTimeseriesQuery('performance', { + measure: effectiveMeasure, + }) + const compareTimeseriesQuery = useCompareTimeseriesQuery('performance', { + measure: effectiveMeasure, + }) + + const overall = useMemo( + () => perfSummaryToOverall(summaryQuery.data?.data), + [summaryQuery.data], + ) + const overallCompare = useMemo( + () => perfSummaryToOverall(compareSummaryQuery.data?.data), + [compareSummaryQuery.data], + ) + + const chartData = useMemo( + () => + pivotPerformanceTimeseries(timeseriesQuery.data?.data, effectiveMeasure), + [timeseriesQuery.data, effectiveMeasure], + ) + const chartDataCompare = useMemo( + () => + compareTimeseriesQuery.data + ? pivotPerformanceTimeseries( + compareTimeseriesQuery.data.data, + effectiveMeasure, + ) + : undefined, + [compareTimeseriesQuery.data, effectiveMeasure], + ) + + const summaryLoaded = Boolean(summaryQuery.data) + + const isPanelsDataEmptyRaw = + summaryLoaded && + !overall.current?.frontend && + !overall.current?.network && + !overall.current?.backend + + if (summaryLoaded && !isPanelsDataEmptyRaw) { + hasShownContentRef.current = true + } + + const isPanelsDataEmpty = isPanelsDataEmptyRaw && !hasShownContentRef.current + + // Queries keep previous data across period/filter changes, so `isLoading` only + // ever means "nothing cached to show yet" — exactly when a spinner is wanted. + const isChartLoading = summaryQuery.isLoading || timeseriesQuery.isLoading + + // The counterpart: stale data is on screen while a refresh is in flight, so + // the panel gets the same progress bar the breakdown panels use. + const isChartRefetching = + (summaryQuery.isFetching && !summaryQuery.isLoading) || + (timeseriesQuery.isFetching && !timeseriesQuery.isLoading) const handleDataPointClick = useCallback( (d: { x: Date; index: number; xValue?: string }) => { @@ -294,14 +229,9 @@ const PerformanceViewInner = ({ [timeBucket, timeFormat, timezone], ) - const chartDataCompare: any = useMemo( - () => deferredData.perfCompareData?.chart || {}, - [deferredData.perfCompareData], - ) - // Filter annotations to only those within the chart's visible x-axis range const filteredAnnotations = useMemo(() => { - const xAxis = (chartData as { x?: string[] })?.x + const xAxis = chartData?.x if (!annotations?.length || !xAxis?.length) return annotations || [] const rangeStart = dayjs(xAxis[0]).startOf('day') @@ -316,19 +246,6 @@ const PerformanceViewInner = ({ }) }, [annotations, chartData]) - // Panel active tabs - const [activeTabs, setActiveTabs] = useState<{ - location: 'cc' | 'rg' | 'ct' | 'map' - page: 'pg' | 'host' - device: 'br' | 'os' | 'dv' - network: 'isp' | 'og' | 'ut' | 'ctp' - }>({ - location: 'cc', - page: 'pg', - device: 'br', - network: 'isp', - }) - const chartMetrics = useMemo( () => [ { @@ -411,47 +328,161 @@ const PerformanceViewInner = ({ [t], ) - // Version data mapping for browser/OS versions - const createVersionDataMapping = useMemo(() => { - const browserDataSource = panelsData.data?.brv - - const browserVersions: { - [key: string]: { name: string; count: number }[] - } = {} - - if (browserDataSource) { - browserDataSource.forEach((entry: any) => { - const { br, name, count } = entry - if (!browserVersions[br]) { - browserVersions[br] = [] - } - browserVersions[br].push({ name, count }) - }) - } - - return { browserVersions } - }, [panelsData.data?.brv]) - - // Handle refresh trigger - useEffect(() => { - if (performanceRefreshTrigger > 0) { - revalidator.revalidate() - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [performanceRefreshTrigger]) + const locationSubTabs = useMemo( + () => [ + { id: 'country', label: t('project.mapping.cc'), dimension: 'country' }, + { id: 'region', label: t('project.mapping.rg'), dimension: 'region' }, + { id: 'city', label: t('project.mapping.ct'), dimension: 'city' }, + { + id: 'map', + label: t('project.mapping.map'), + render: () => ( + + ), + }, + ], + [t, panelsMeasure], + ) + + const pagesSubTabs = useMemo( + () => [ + { id: 'page', label: t('project.mapping.pg'), dimension: 'page' }, + { id: 'host', label: t('project.mapping.host'), dimension: 'host' }, + ], + [t], + ) + + const devicesSubTabs = useMemo( + () => [ + { + id: 'browser', + label: t('project.mapping.br'), + dimension: 'browser', + versionsDimension: 'browser_version' as const, + versionsParentField: 'browser' as const, + }, + { id: 'device', label: t('project.mapping.dv'), dimension: 'device' }, + ], + [t], + ) + + const networkSubTabs = useMemo<(BreakdownSubTab | BreakdownSubTab[])[]>( + () => [ + [ + { id: 'isp', label: t('project.mapping.isp'), dimension: 'isp' }, + { + id: 'organization', + label: t('project.mapping.og'), + dimension: 'organization', + }, + { + id: 'user_type', + label: t('project.mapping.ut'), + dimension: 'user_type', + }, + { + id: 'connection_type', + label: t('project.mapping.ctp'), + dimension: 'connection_type', + }, + ], + ], + [t], + ) + + const locationRowMapper = useCallback( + (entry: Entry) => { + const { name: entryName, cc } = entry + + if (cc !== undefined) { + return ( + + ) + } + + return + }, + [language], + ) + + const pagesRowMapper = useCallback( + (entry: Entry, subTabId: string) => { + const { name: entryName } = entry + + if (!entryName) { + return ( + + {subTabId === 'host' + ? t('project.unknownHost') + : t('common.notSet')} + + ) + } + + let decodedUri = entryName + + try { + decodedUri = decodeURIComponent(entryName) + } catch { + // ignore + } + + if (subTabId === 'page' && project?.websiteUrl) { + return ( + + ) + } + + return decodedUri + }, + [t, project?.websiteUrl], + ) + + const devicesRowMapper = useCallback( + (entry: Entry, subTabId: string) => { + const v1Tab = subTabId === 'browser' ? 'br' : 'dv' + const mapper = getDeviceRowMapper(v1Tab, theme, t) as + | ((entry: Entry) => React.ReactNode) + | undefined + return mapper ? mapper(entry) : entry.name + }, + [theme, t], + ) + + const networkRowMapper = useCallback( + (entry: Entry, subTabId: string) => { + const { name: entryName } = entry + if (!entryName) { + return {t('common.notSet')} + } + if (subTabId === 'user_type') { + return getUsageTypeLabel(entryName, t) + } + if (subTabId === 'connection_type') { + return getConnectionTypeLabel(entryName, t) + } + return entryName + }, + [t], + ) + + const loadTimeValueMapper = useCallback( + (value: number) => + getStringFromTime(getTimeFromSeconds(value), true) as unknown as number, + [], + ) // Show waiting state if project has no traffic data yet if (!project?.isDataExists) { return } - // Show no events if data is empty + const headerRightContent = + if (isPanelsDataEmpty) { return ( <> - } - /> + ) @@ -459,53 +490,21 @@ const PerformanceViewInner = ({ // Fullscreen map view - takes over the entire content area if (isMapFullscreen && fullscreenMapRef.current) { - const countryData = panelsData.data?.cc || [] - const regionData = panelsData.data?.rg || [] - const total = countryData.reduce( - (acc: number, curr: any) => acc + curr.count, - 0, - ) - return createPortal( - -
    -
    - - Loading map... - -
    -
    - } - > - { - const link = getFilterLink(type, key) - navigate(link) - }} - onFullscreenToggle={setIsMapFullscreen} - isFullscreen={true} - /> -
    , + , fullscreenMapRef.current, ) } return ( <> - } - /> - {dataLoading && !isPanelsDataEmpty ? : null} +
    {!isPanelsDataEmpty ? ( ) : null}
    + {isChartRefetching ? : null}
    - {!_isEmpty(overall) ? ( - - ) : null} - {activeChartMetrics && !_isEmpty(chartData) ? ( + {/* Always rendered: the cards read zero until the summary lands and + then roll up to it, so the row holds its height instead of + appearing late and shoving the chart and panels down. */} + + {isChartLoading ? ( + // Same box as PerformanceChart below (incl. its mobile mt-5) so the + // chart drops straight into the spinner's place. +
    + +
    + ) : activeChartMetrics && !_isEmpty(chartData.x) ? (
    handleChartContextMenu(e, chartData?.x)} + onContextMenu={(e) => handleChartContextMenu(e, chartData.x)} className='relative' >
    - {!_isEmpty(panelsData.types) - ? _map(PERFORMANCE_PANELS_ORDER, (type: keyof typeof tnMapping) => { - if (type === 'location') { - const locationTabs = [ - { id: 'cc', label: t('project.mapping.cc') }, - { id: 'rg', label: t('project.mapping.rg') }, - { id: 'ct', label: t('project.mapping.ct') }, - { id: 'map', label: 'Map' }, - ] - - const rowMapper = (entry: CountryEntry) => { - const { name: entryName, cc } = entry - - if (cc !== undefined) { - return ( - - ) - } - - return - } - - return ( - - setActiveTabs({ - ...activeTabs, - location: tab as 'cc' | 'rg' | 'ct' | 'map', - }) - } - activeTabId={activeTabs.location} - data={panelsData.data[activeTabs.location]} - rowMapper={rowMapper} - // @ts-expect-error - valueMapper={(value) => - getStringFromTime(getTimeFromSeconds(value), true) - } - customRenderer={ - activeTabs.location === 'map' - ? () => { - const countryData = panelsData.data?.cc || [] - const regionData = panelsData.data?.rg || [] - const total = countryData.reduce( - (acc: number, curr: any) => acc + curr.count, - 0, - ) - - return ( - }> - { - const link = getFilterLink(type, key) - navigate(link) - }} - onFullscreenToggle={setIsMapFullscreen} - isFullscreen={false} - /> - - ) - } - : undefined - } - valuesHeaderName={t('project.loadTime')} - highlightColour='orange' - /> - ) - } - - if (type === 'devices') { - const deviceTabs = [ - { id: 'br', label: t('project.mapping.br') }, - { id: 'dv', label: t('project.mapping.dv') }, - ] - - return ( - - setActiveTabs({ - ...activeTabs, - device: tab as 'br' | 'dv', - }) - } - activeTabId={activeTabs.device} - data={panelsData.data[activeTabs.device]} - rowMapper={getDeviceRowMapper( - activeTabs.device, - theme, - t, - )} - capitalize={activeTabs.device === 'dv'} - versionData={ - activeTabs.device === 'br' - ? createVersionDataMapping.browserVersions - : undefined - } - getVersionFilterLink={(parent, version) => - getVersionFilterLink(parent, version, 'br') - } - // @ts-expect-error - valueMapper={(value) => - getStringFromTime(getTimeFromSeconds(value), true) - } - valuesHeaderName={t('project.loadTime')} - highlightColour='orange' - /> - ) - } - - if (type === 'network') { - const networkTabs = [ - [ - { id: 'isp', label: t('project.mapping.isp') }, - { id: 'og', label: t('project.mapping.og') }, - { id: 'ut', label: t('project.mapping.ut') }, - { id: 'ctp', label: t('project.mapping.ctp') }, - ], - ] - - const activeNetworkTab = activeTabs.network - - return ( - - setActiveTabs({ - ...activeTabs, - network: tab as 'isp' | 'og' | 'ut' | 'ctp', - }) - } - activeTabId={activeNetworkTab} - data={panelsData.data[activeNetworkTab]} - rowMapper={({ name: entryName }) => { - if (!entryName) { - return ( - {t('common.notSet')} - ) - } - if (activeNetworkTab === 'ut') { - return getUsageTypeLabel(entryName, t) - } - if (activeNetworkTab === 'ctp') { - return getConnectionTypeLabel(entryName, t) - } - return entryName - }} - // @ts-expect-error - valueMapper={(value) => - getStringFromTime(getTimeFromSeconds(value), true) - } - valuesHeaderName={t('project.loadTime')} - highlightColour='orange' - /> - ) - } - - if (type === 'pg') { - const pageTabs = [ - { id: 'pg', label: t('project.mapping.pg') }, - { - id: 'host', - label: t('project.mapping.host'), - }, - ] - - return ( - { - if (!entryName) { - return ( - - {activeTabs.page === 'pg' - ? t('common.notSet') - : t('project.unknownHost')} - - ) - } - - let decodedUri = entryName as string - - try { - decodedUri = decodeURIComponent(entryName) - } catch { - // do nothing - } - - // For page paths (pg tab), show with clickable link - if (activeTabs.page === 'pg' && project?.websiteUrl) { - return ( - - ) - } - - return decodedUri - }} - name={t('project.pages')} - tabs={pageTabs} - onTabChange={(tab) => - setActiveTabs({ - ...activeTabs, - page: tab as 'pg' | 'host', - }) - } - activeTabId={activeTabs.page} - data={panelsData.data[activeTabs.page]} - // @ts-expect-error - valueMapper={(value) => - getStringFromTime(getTimeFromSeconds(value), true) - } - valuesHeaderName={t('project.loadTime')} - highlightColour='orange' - /> - ) - } - - return null - }) - : null} + + + + getVersionFilterLink(parent, version, 'browser') + } + valuesHeaderName={t('project.loadTime')} + /> +
    => { + if (!summary) return {} + + return { + current: summary.current, + previous: summary.previous, + frontendChange: summary.change.frontend, + backendChange: summary.change.backend, + networkChange: summary.change.network, + } +} + +const toChartX = (timestamp: string): string => + timestamp.slice(0, 19).replace('T', ' ') + +export const pivotPerformanceTimeseries = ( + rows: TimeseriesRow[] | undefined, + measure: string, +): Record => { + const x: string[] = [] + + if (measure === 'quantiles') { + const p50: number[] = [] + const p75: number[] = [] + const p95: number[] = [] + + for (const row of rows || []) { + x.push(toChartX(row.timestamp)) + p50.push(Number(row.p50 ?? 0)) + p75.push(Number(row.p75 ?? 0)) + p95.push(Number(row.p95 ?? 0)) + } + + return { x, p50, p75, p95 } as unknown as Record + } + + const dns: number[] = [] + const tls: number[] = [] + const conn: number[] = [] + const response: number[] = [] + const render: number[] = [] + const domLoad: number[] = [] + const ttfb: number[] = [] + + for (const row of rows || []) { + x.push(toChartX(row.timestamp)) + dns.push(Number(row.dns ?? 0)) + tls.push(Number(row.tls ?? 0)) + conn.push(Number(row.connection ?? 0)) + response.push(Number(row.response ?? 0)) + render.push(Number(row.render ?? 0)) + domLoad.push(Number(row.dom_load ?? 0)) + ttfb.push(Number(row.ttfb ?? 0)) + } + + return { + x, + dns, + tls, + conn, + response, + render, + domLoad, + ttfb, + } as unknown as Record +} diff --git a/web/app/pages/Project/tabs/Profiles/ProfileDetails.tsx b/web/app/pages/Project/tabs/Profiles/ProfileDetails.tsx index 5187780b7..23b0dce53 100644 --- a/web/app/pages/Project/tabs/Profiles/ProfileDetails.tsx +++ b/web/app/pages/Project/tabs/Profiles/ProfileDetails.tsx @@ -65,11 +65,11 @@ interface PageflowItem { currency?: string } -interface ProfileSession { +export interface ProfileSession { psid: string - cc: string | null + country: string | null os: string | null - br: string | null + browser: string | null pageviews: number customEvents: number errors: number @@ -79,7 +79,7 @@ interface ProfileSession { sessionStart?: string lastActivity?: string isLive?: 1 | 0 | boolean - sdur?: number + duration?: number | null pages?: PageflowItem[] } @@ -155,8 +155,8 @@ const formatVersionLabel = (name: string | null, version: string | null) => { } const getSessionDuration = (session: ProfileSession) => { - if (session.sdur != null) { - return session.sdur + if (session.duration != null) { + return session.duration } const interactionPages = (session.pages || []).filter( @@ -431,13 +431,15 @@ const ProfileSessionFlow = ({ }, ] : []), - ...(session.br + ...(session.browser ? [ { key: 'browser', - label: session.br, - tooltip: session.br, - icon: , + label: session.browser, + tooltip: session.browser, + icon: ( + + ), }, ] : []), @@ -620,14 +622,17 @@ export const ProfileDetails = ({ const statusTooltip = details.lastSeen ? t('project.lastSeenAgo', { time: lastSeenAgo }) : t('project.unknown') - const countryName = details.cc - ? countries.getName(details.cc, language) || details.cc + const countryName = details.country + ? countries.getName(details.country, language) || details.country : null const locationSummary = - [countryName, details.rg, details.ct].filter(Boolean).join(', ') || + [countryName, details.region, details.city].filter(Boolean).join(', ') || t('project.unknown') - const osTooltipLabel = formatVersionLabel(details.os, details.osv) - const browserTooltipLabel = formatVersionLabel(details.br, details.brv) + const osTooltipLabel = formatVersionLabel(details.os, details.os_version) + const browserTooltipLabel = formatVersionLabel( + details.browser, + details.browser_version, + ) const profilePlatformParts: PlatformPart[] = [ ...(details.os ? [ @@ -639,13 +644,13 @@ export const ProfileDetails = ({ }, ] : []), - ...(details.br + ...(details.browser ? [ { key: 'browser', - label: details.br, - tooltip: browserTooltipLabel || details.br, - icon: , + label: details.browser, + tooltip: browserTooltipLabel || details.browser, + icon: , }, ] : []), @@ -803,10 +808,10 @@ export const ProfileDetails = ({ colour='primary' className='flex h-6 items-center justify-center' > - {details.cc ? ( + {details.country ? (