{ "version": 3, "sources": ["../../../app/hooks/instrumentation/useImpressionLedger/useImpressionLedger.ts", "../../../app/hooks/instrumentation/useImpressionLedger/ImpressionLedger.ts", "../../../app/hooks/instrumentation/useImpressionLedger/useLookImpressionPayload.ts", "../../../app/analytics/look/looks.ts", "../../../app/hooks/instrumentation/useImpressionLedger/useProductImpressionPayloadMap.ts", "../../../app/hooks/instrumentation/usePageViewTracker.ts", "../../../app/analytics/page/page.ts", "../../../app/hooks/instrumentation/usePageDurationTracker.ts", "../../../app/hooks/instrumentation/useSessionDurationTracker.ts", "../../../app/hooks/instrumentation/utils.ts"], "sourcesContent": ["import { useEffect, useRef } from 'react';\nimport ImpressionLedger from './ImpressionLedger';\n\nconst useImpressionLedger = (eventName: string, source: string) => {\n const ledgerRef = useRef(\n new ImpressionLedger(eventName, source)\n );\n\n // mount ref to avoid re-initialization of ledger on first render.\n const isMountRef = useRef(true);\n\n useEffect(() => {\n const prevLedger = ledgerRef.current;\n if (!isMountRef.current) {\n ledgerRef.current = new ImpressionLedger(eventName, source);\n }\n isMountRef.current = false;\n return () => {\n prevLedger?.flush();\n };\n }, [eventName, source, ledgerRef, isMountRef]);\n\n return ledgerRef.current;\n};\n\nexport default useImpressionLedger;\n", "import { track } from '~/analytics/utils';\n\nimport { isEmpty, isNil } from 'lodash-es';\nimport type { ImpressionLedgerEntry } from './useImpressionLedger.types';\n\n\nconst IMPRESSION_CUTOFF_TIME = 500; // in ms\n\nconst hasImpressedEnough = (entry: ImpressionLedgerEntry) => {\n const { inViewAt } = entry;\n if (isNil(inViewAt)) {\n return false;\n }\n const diff = Date.now() - inViewAt;\n return diff >= IMPRESSION_CUTOFF_TIME;\n};\n\nclass ImpressionLedger {\n private readonly ledger = new Map();\n\n // eslint-disable-next-line no-useless-constructor\n constructor(\n private readonly eventName: string,\n private readonly source?: string\n ) { }\n\n // Track existing impression for an entry if needed.\n private trackExistingImpression(id: string, inView: boolean) {\n const prevEntry = this.ledger.get(id);\n if (!prevEntry) {\n // no previous impression to track.\n return;\n }\n\n if (inView || prevEntry.isTracked) {\n // item is still in view port or may already tracked.\n return;\n }\n\n if (!hasImpressedEnough(prevEntry)) {\n // looks like cutoff time is not met.\n return;\n }\n\n track(this.eventName, {\n source: this.source,\n items: [\n { page: prevEntry.page, index: prevEntry.index, ...prevEntry.payload },\n ],\n });\n\n // mark impression as tracked to avoid noise.\n this.ledger.set(id, { ...prevEntry, isTracked: true });\n }\n\n public setEntry(id: string, entry: ImpressionLedgerEntry) {\n this.trackExistingImpression(id, entry.inView);\n\n const prevEntry = this.ledger.get(id);\n if (!prevEntry) {\n const inViewAt = entry.inView ? Date.now() : null;\n this.ledger.set(id, { ...entry, inViewAt });\n return this;\n }\n\n if (prevEntry.isTracked) {\n // already tracked. Don't do anything.\n return this;\n }\n\n if (entry.inView && prevEntry.inView) {\n // item is already in view no need to do anything.\n return this;\n }\n\n // If item has entered in the view port, update inViewAt as needed.\n const hasEntered = entry.inView && !prevEntry.inView;\n const inViewAt = hasEntered ? Date.now() : prevEntry.inViewAt;\n this.ledger.set(id, { ...prevEntry, ...entry, inViewAt });\n return this;\n }\n\n // Flushes impressions by sending them to mixpanel.\n public flush() {\n const items: object[] = [];\n\n for (const [_, entry] of this.ledger) {\n if (entry.isTracked) {\n continue;\n }\n if (!hasImpressedEnough(entry)) {\n continue;\n }\n items.push({ page: entry.page, index: entry.index, ...entry.payload });\n }\n\n if (isEmpty(items)) {\n return;\n }\n track(this.eventName, { source: this.source, items });\n this.ledger.clear();\n }\n}\n\nexport default ImpressionLedger;\n", "import { useMemo } from 'react';\nimport { toLookImpressionPayload } from '~/analytics/look/looks';\n\nimport type { CrossSellLook, Look, LookTypesense } from '~/typings/models';\nimport type { ImpressionLedgerEntry } from './useImpressionLedger.types';\n\n\nconst useLookImpressionPayloadMap = (\n looks: (LookTypesense | Look | CrossSellLook)[]\n) =>\n useMemo(() => {\n const map = new Map>();\n\n looks.forEach((look, index) => {\n const payload = toLookImpressionPayload({\n index,\n look,\n });\n const entry = { index, payload };\n map.set(`${look.id}`, entry);\n });\n\n return map;\n }, [looks]);\n\nexport default useLookImpressionPayloadMap;\n", "import type { Look } from '~/typings/models';\n\ninterface ToLookImpressionPayloadArgs extends Record {\n look: Pick;\n}\n\nconst toLookImpressionPayload = ({\n look,\n index,\n ...rest\n}: ToLookImpressionPayloadArgs) => {\n const { slug: lookSlug } = look;\n return {\n index,\n lookSlug,\n rest,\n };\n};\n\nexport { toLookImpressionPayload };\n", "import { useMemo } from 'react';\nimport { toProductImpressionPayload } from '~/analytics/product';\n\nimport type { Product } from '~/typings/models';\nimport type { ImpressionLedgerEntry } from './useImpressionLedger.types';\n\n\nconst useProductImpressionPayloadMap = (\n products: Product[],\n searchParams?: URLSearchParams,\n perPage?: number\n) =>\n useMemo(() => {\n const map = new Map>();\n\n products?.forEach((product, index) => {\n let page = Math.ceil(index / (perPage ?? Infinity));\n page = page !== 0 ? page : 1;\n const payload = toProductImpressionPayload({\n index,\n product,\n searchParams: Object.fromEntries(searchParams?.entries() || []),\n });\n const entry = { index, page, payload };\n map.set(`${product.id}`, entry);\n });\n\n return map;\n }, [products, searchParams, perPage]);\n\nexport default useProductImpressionPayloadMap;\n", "import { useLocation, useParams } from '@remix-run/react';\nimport { useEffect } from 'react';\nimport { trackPageView } from '~/analytics/page';\nimport { usePrevious } from '~/hooks/common';\n\nimport type { Location } from '@remix-run/react';\nimport type { Maybe } from '~/typings/utils';\n\n\nconst usePageViewTracker = () => {\n const location = useLocation();\n const params = useParams();\n const paramsRef = usePrevious(params);\n\n // There's no way to get previous url reliable. We use ref to remember previous\n // location and construct it manually when it is available.\n // https://stackoverflow.com/questions/3528324/how-to-get-the-previous-url-in-javascript\n const prevLocationRef = usePrevious>(null);\n\n useEffect(() => {\n paramsRef.current = params;\n }, [params, paramsRef]);\n\n useEffect(() => {\n const payload = { pageParams: paramsRef.current };\n trackPageView(payload);\n prevLocationRef.current = location;\n }, [location, prevLocationRef, paramsRef]);\n};\n\nexport default usePageViewTracker;\n", "import { Analytics } from '@tectonic/athena-web';\nimport { toAnalyticsProperties, track } from '~/analytics/utils';\n\nimport type { AnalyticsEventPayload } from '~/analytics/utils';\nimport type { PageEventNames } from './event';\n\n\nconst trackPageView = (payload: AnalyticsEventPayload) => {\n const props = toAnalyticsProperties(payload);\n Analytics.trackPageView(props);\n};\n\nconst trackPageEvent = (\n event: PageEventNames,\n payload: AnalyticsEventPayload\n) => track(event, payload);\n\nexport { trackPageEvent, trackPageView };\n", "import { useLocation, useParams } from '@remix-run/react';\nimport { Analytics } from '@tectonic/athena-web';\nimport { useCallback, useEffect, useRef } from 'react';\nimport { PageEventNames, trackPageEvent } from '~/analytics/page';\nimport { usePrevious } from '~/hooks/common';\nimport { useOnWindowScroll } from '~/hooks/window';\nimport { getScrollHeight } from '~/utils/dom';\nimport { percent } from '~/utils/math';\n\nconst getScrollPercentage = () => {\n // https://css-tricks.com/how-i-put-the-scroll-percentage-in-the-browser-title-bar/\n const scrollTop = window.scrollY;\n const scrollHeight = getScrollHeight();\n const winHeight = window.innerHeight;\n return percent(scrollTop, scrollHeight - winHeight);\n};\n\n// Track the page duration along with scroll depth.\nconst usePageDurationTracker = () => {\n const scrollDepthRef = useRef(0);\n\n const location = useLocation();\n const params = useParams();\n const paramsRef = usePrevious(params);\n\n const onScroll = useCallback(() => {\n const percent = getScrollPercentage();\n scrollDepthRef.current = Math.max(percent, scrollDepthRef.current);\n }, [scrollDepthRef]);\n\n useOnWindowScroll(onScroll);\n\n useEffect(() => {\n Analytics.timeEvent(PageEventNames.PAGE_DURATION);\n const pageParams = paramsRef.current;\n return () => {\n const payload = {\n pageParams,\n scrollDepth: scrollDepthRef.current,\n };\n trackPageEvent(PageEventNames.PAGE_DURATION, payload);\n scrollDepthRef.current = 0;\n };\n }, [paramsRef, location, scrollDepthRef]);\n};\n\nexport default usePageDurationTracker;\n", "import { useLocation } from '@remix-run/react';\nimport { Analytics, AnalyticsEventNames } from '@tectonic/athena-web';\nimport { useEffect } from 'react';\nimport { toLocationPath } from './utils';\nimport { track } from '~/analytics/utils';\nimport { usePrevious } from '~/hooks/common';\n\nimport type { Location } from '@remix-run/react';\n\n// Track the page duration along with scroll depth.\nconst useSessionDurationTracker = () => {\n const location = useLocation();\n const initialLocationRef = usePrevious(location);\n useEffect(() => {\n Analytics.timeEvent(AnalyticsEventNames.SESSION_DURATION);\n const landingUrl = toLocationPath(initialLocationRef.current);\n\n window.addEventListener('pagehide', () => {\n try {\n const payload = { landingUrl };\n track(\n AnalyticsEventNames.SESSION_DURATION,\n payload,\n false,\n 'sendBeacon'\n );\n } catch (error) {}\n });\n }, [initialLocationRef]);\n};\n\nexport default useSessionDurationTracker;\n", "import type { Location } from '@remix-run/react';\nimport type { Maybe } from '~/typings/utils';\n\nconst toLocationPath = (location: Maybe) => {\n if (!location) {\n return '';\n }\n const { search, pathname } = location;\n return `${pathname}${search}`;\n};\n\nexport { toLocationPath };\n"], "mappings": "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", "names": ["import_react", "IMPRESSION_CUTOFF_TIME", "hasImpressedEnough", "entry", "inViewAt", "isNil_default", "ImpressionLedger", "eventName", "source", "id", "inView", "prevEntry", "track", "items", "_", "isEmpty_default", "ImpressionLedger_default", "useImpressionLedger", "eventName", "source", "ledgerRef", "ImpressionLedger_default", "isMountRef", "prevLedger", "useImpressionLedger_default", "import_react", "toLookImpressionPayload", "look", "index", "rest", "lookSlug", "useLookImpressionPayloadMap", "looks", "map", "look", "index", "payload", "toLookImpressionPayload", "entry", "useLookImpressionPayload_default", "import_react", "useProductImpressionPayloadMap", "products", "searchParams", "perPage", "map", "product", "index", "page", "payload", "toProductImpressionPayload", "entry", "useProductImpressionPayloadMap_default", "import_react", "trackPageView", "payload", "props", "toAnalyticsProperties", "ri", "trackPageEvent", "event", "track", "usePageViewTracker", "location", "useLocation", "params", "useParams", "paramsRef", "usePrevious_default", "prevLocationRef", "payload", "trackPageView", "usePageViewTracker_default", "import_react", "getScrollPercentage", "scrollTop", "scrollHeight", "getScrollHeight", "winHeight", "percent", "usePageDurationTracker", "scrollDepthRef", "location", "useLocation", "params", "useParams", "paramsRef", "usePrevious_default", "onScroll", "useOnWindowScroll_default", "ri", "pageParams", "payload", "trackPageEvent", "usePageDurationTracker_default", "import_react", "toLocationPath", "location", "search", "pathname", "useSessionDurationTracker", "location", "useLocation", "initialLocationRef", "usePrevious_default", "ri", "e", "landingUrl", "toLocationPath", "payload", "track", "useSessionDurationTracker_default"] }