├── README.md ├── .prettierignore ├── apps ├── web │ ├── app │ │ ├── routes │ │ │ ├── docs │ │ │ │ ├── documents │ │ │ │ │ ├── changelog.mdoc │ │ │ │ │ ├── reference │ │ │ │ │ │ ├── web-analytics.mdoc │ │ │ │ │ │ └── requests.mdoc │ │ │ │ │ ├── partials │ │ │ │ │ │ ├── metronome-init.partial.mdoc │ │ │ │ │ │ └── metronome-installation.partial.mdoc │ │ │ │ │ └── about │ │ │ │ │ │ ├── pricing.mdoc │ │ │ │ │ │ └── support.mdoc │ │ │ │ ├── helpers │ │ │ │ │ ├── index.ts │ │ │ │ │ └── toSlug.ts │ │ │ │ ├── components │ │ │ │ │ ├── DocsHeader │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Markdoc │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── Code │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── Code.tsx │ │ │ │ │ │ │ ├── Link │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ ├── Fence │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ ├── Image │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── Image.tsx │ │ │ │ │ │ │ ├── Button │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── Button.tsx │ │ │ │ │ │ │ ├── Heading │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ ├── Strong │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── Strong.tsx │ │ │ │ │ │ │ ├── Paragraph │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── Paragraph.tsx │ │ │ │ │ │ │ ├── List │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── ListItem.tsx │ │ │ │ │ │ │ │ └── List.tsx │ │ │ │ │ │ │ ├── InstallationTargets │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── InstallationTargets.tsx │ │ │ │ │ │ │ ├── HorizontalRule │ │ │ │ │ │ │ │ └── HorizontalRule.tsx │ │ │ │ │ │ │ ├── Table │ │ │ │ │ │ │ │ ├── Tr.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── THead.tsx │ │ │ │ │ │ │ │ ├── TBody.tsx │ │ │ │ │ │ │ │ ├── Table.tsx │ │ │ │ │ │ │ │ ├── Th.tsx │ │ │ │ │ │ │ │ └── Td.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── Alert │ │ │ │ │ │ │ │ └── Alert.tsx │ │ │ │ │ │ ├── hooks │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── useMarkdoc │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── TableOfContents │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Sidebar │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── SidebarContainer.tsx │ │ │ │ │ │ ├── SheetCloseContainer.tsx │ │ │ │ │ │ ├── provider.ts │ │ │ │ │ │ └── Sidebar.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── hooks │ │ │ │ │ └── useDocsLoaderData │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── useDocsLoaderData.ts │ │ │ │ ├── getters │ │ │ │ │ ├── index.ts │ │ │ │ │ └── getDocumentMeta.ts │ │ │ │ └── constants.ts │ │ │ ├── teams │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useTeamLoaderData │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── useTeamLoaderData.ts │ │ │ │ ├── components │ │ │ │ │ ├── TeamsHeader │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── TeamsHeader.tsx │ │ │ │ │ ├── ProjectSelector │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── components │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── NewProjectDialog │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ └── routes │ │ │ │ │ └── projects │ │ │ │ │ ├── components │ │ │ │ │ ├── Metric │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Section │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── Section.Title.tsx │ │ │ │ │ │ └── Section.tsx │ │ │ │ │ ├── Navigation │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── VersionNotification │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── index.ts │ │ │ │ │ ├── routes │ │ │ │ │ ├── overview │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── ActionsSection │ │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ │ ├── Chart │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── Errors │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── Duration │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ └── Invocations │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ ├── LoadersSection │ │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ │ ├── Chart │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── Errors │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── Duration │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ └── Invocations │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ ├── RequestsSection │ │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ │ ├── Chart │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── Duration │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── Requests │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── DataRequests │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── DocumentRequests │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ ├── WebVitalsSection │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── components │ │ │ │ │ │ │ │ │ └── WebVitalsCard │ │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ │ └── components │ │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ │ └── WebVitalBar │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ ├── WebAnalyticsSection │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ │ ├── BounceRate │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── TotalPageviews │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── TotalSessions │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── VisitorsRightNow │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── SessionMedianDuration │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ └── WebAnalyticsSection.tsx │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── hooks │ │ │ │ │ │ │ ├── useOverviewEventData │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── useOverviewEventData.ts │ │ │ │ │ │ │ ├── useOverviewLoaderData │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── useOverviewLoaderData.ts │ │ │ │ │ │ │ ├── useIsNavigatingOverview │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── useIsNavigatingOverview.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── settings │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── DangerZoneForm │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ │ └── DeleteProject │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ └── DangerZoneForm.tsx │ │ │ │ │ │ │ ├── InformationForm │ │ │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ │ │ ├── Usage │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ └── RotateApiKey │ │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ ├── VisibilityForm │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ └── GeneralSettingsForm │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── hooks │ │ │ │ │ │ │ ├── useSettingsEventData │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── useSettingsEventData.ts │ │ │ │ │ │ │ ├── useSettingsLoaderData │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── useSettingsLoaderData.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── web-analytics │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── DevicesSection │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── components │ │ │ │ │ │ │ │ │ ├── OsTabContent │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ └── BrowsersTabContent │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ ├── RoutesSection │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── components │ │ │ │ │ │ │ │ │ ├── RoutesSectionUrlsTabContent │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ └── RoutesSectionPathsTabContent │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ ├── LocationsSection │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── components │ │ │ │ │ │ │ │ │ ├── CitiesTabContent │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ └── CountriesTabContent │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ ├── ReferrersSection │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── ReferrersTable │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ └── ReferrersSection.tsx │ │ │ │ │ │ │ ├── GeneralWebAnalyticsSection │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── components │ │ │ │ │ │ │ │ │ ├── ViewsTabContent │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── ViewsTabTrigger │ │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ │ │ ├── SessionsTabContent │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── SessionsTabTrigger │ │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ │ │ ├── BounceRateTabContent │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── BounceRateTabTrigger │ │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ │ │ ├── VisitorsChartTabContent │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── UniqueVisitorsTabTrigger │ │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ │ │ ├── VisitorsRightNowTabTrigger │ │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ │ │ ├── MedianSessionTimeTabContent │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ │ │ ├── MedianSessionTimeTabTrigger │ │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── hooks │ │ │ │ │ │ │ ├── useWebAnalyticsEventData │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── useWebAnalyticsEventData.ts │ │ │ │ │ │ │ ├── useWebAnalyticsLoaderData │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── useWebAnalyticsLoaderData.ts │ │ │ │ │ │ │ ├── useIsNavigatingWebAnalytics │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── useIsNavigatingWebAnalytics.ts │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── web-vitals │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── WebVitalsSection │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── components │ │ │ │ │ │ │ │ │ └── WebVitalsCard │ │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ │ └── components │ │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ │ └── WebVitalBar │ │ │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ │ └── WebVitalsByRouteSection │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ └── data │ │ │ │ │ │ │ │ ├── tasks.json │ │ │ │ │ │ │ │ └── schema.tsx │ │ │ │ │ │ └── hooks │ │ │ │ │ │ │ ├── useIsNavigatingWebVitals.ts │ │ │ │ │ │ │ ├── useWebVitalsEventData.ts │ │ │ │ │ │ │ └── useWebVitalsLoaderData.ts │ │ │ │ │ └── errors │ │ │ │ │ │ └── components │ │ │ │ │ │ └── ErrorsList.tsx │ │ │ │ │ └── hooks │ │ │ │ │ ├── useTeamProjectLoaderData │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useTeamProjectLoaderData.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useTeamProjectEventData.ts │ │ │ ├── resources │ │ │ │ └── healthcheck.route.ts │ │ │ ├── shared │ │ │ │ └── hooks │ │ │ │ │ ├── useIsNavigatingSharedProject.ts │ │ │ │ │ ├── useSharedProjectEventData.ts │ │ │ │ │ └── useSharedProjectLoaderData.ts │ │ │ ├── authentication │ │ │ │ ├── authentication.logout.route.tsx │ │ │ │ └── authentication.success.route.tsx │ │ │ ├── metrics │ │ │ │ └── metrics.route.ts │ │ │ └── notifications │ │ │ │ └── notifications.$notificationId.route.tsx │ │ ├── modules │ │ │ ├── index.ts │ │ │ └── nanoid.ts │ │ ├── components │ │ │ ├── Ping │ │ │ │ ├── index.ts │ │ │ │ └── Ping.tsx │ │ │ ├── Badge │ │ │ │ └── index.ts │ │ │ ├── Brand │ │ │ │ ├── index.ts │ │ │ │ ├── images │ │ │ │ │ └── logo-white.svg │ │ │ │ └── Brand.stories.tsx │ │ │ ├── Header │ │ │ │ └── index.ts │ │ │ ├── Input │ │ │ │ └── index.ts │ │ │ ├── Label │ │ │ │ ├── index.ts │ │ │ │ └── Label.tsx │ │ │ ├── Switch │ │ │ │ └── index.ts │ │ │ ├── Heading │ │ │ │ └── index.ts │ │ │ ├── Spinner │ │ │ │ ├── index.ts │ │ │ │ └── Spinner.tsx │ │ │ ├── Calendar │ │ │ │ └── index.ts │ │ │ ├── Checkbox │ │ │ │ └── index.ts │ │ │ ├── Container │ │ │ │ └── index.ts │ │ │ ├── Separator │ │ │ │ └── index.ts │ │ │ ├── UserMenu │ │ │ │ └── index.ts │ │ │ ├── AspectRatio │ │ │ │ ├── index.ts │ │ │ │ └── AspectRatio.tsx │ │ │ ├── Button │ │ │ │ └── index.ts │ │ │ ├── RouteDisplay │ │ │ │ ├── index.ts │ │ │ │ └── RouteDisplay.stories.tsx │ │ │ ├── BarStackChart │ │ │ │ └── index.ts │ │ │ ├── UrlRouteDisplay │ │ │ │ ├── index.ts │ │ │ │ └── UrlRouteDisplay.stories.tsx │ │ │ ├── TableWithBarChart │ │ │ │ └── index.ts │ │ │ ├── Notifications │ │ │ │ ├── index.ts │ │ │ │ └── NotificationsOutlet.tsx │ │ │ ├── utils.ts │ │ │ ├── ScrollArea │ │ │ │ └── index.ts │ │ │ ├── Alert │ │ │ │ └── index.ts │ │ │ ├── Breadcrumb │ │ │ │ ├── index.ts │ │ │ │ └── BreadcrumbOutlet.tsx │ │ │ ├── Avatar │ │ │ │ └── index.ts │ │ │ ├── Popover │ │ │ │ └── index.ts │ │ │ ├── Tooltip │ │ │ │ └── index.ts │ │ │ ├── Tabs │ │ │ │ └── index.ts │ │ │ ├── Icon │ │ │ │ ├── Check.tsx │ │ │ │ ├── ChevronRight.tsx │ │ │ │ ├── LoaderTwo.tsx │ │ │ │ ├── Menu.tsx │ │ │ │ ├── X.tsx │ │ │ │ ├── Search.tsx │ │ │ │ ├── Filter.tsx │ │ │ │ ├── ArrowNarrowLeft.tsx │ │ │ │ ├── ArrowNarrowRight.tsx │ │ │ │ ├── BrandSafari.tsx │ │ │ │ ├── Refresh.tsx │ │ │ │ ├── BrandOpera.tsx │ │ │ │ ├── Eye.tsx │ │ │ │ ├── RouteSquareTwo.tsx │ │ │ │ ├── Heartbeat.tsx │ │ │ │ ├── Sun.tsx │ │ │ │ ├── SquareRoundedPlus.tsx │ │ │ │ ├── InfoSquareRounded.tsx │ │ │ │ ├── Alarm.tsx │ │ │ │ ├── CircleFilled.tsx │ │ │ │ ├── Home.tsx │ │ │ │ ├── SquareFilled.tsx │ │ │ │ ├── TerminalTwo.tsx │ │ │ │ ├── BuildingLighthouse.tsx │ │ │ │ ├── AlertSquareRoundedOutline.tsx │ │ │ │ ├── Clipboard.tsx │ │ │ │ ├── DeviceDesktop.tsx │ │ │ │ ├── Quote.tsx │ │ │ │ ├── BrandWindows.tsx │ │ │ │ ├── World.tsx │ │ │ │ ├── Rocket.tsx │ │ │ │ ├── EyeClosed.tsx │ │ │ │ ├── ClipboardCheck.tsx │ │ │ │ ├── FileText.tsx │ │ │ │ ├── TriangleInvertedFilled.tsx │ │ │ │ ├── TriangleFilled.tsx │ │ │ │ ├── BrandChrome.tsx │ │ │ │ ├── MoodSadDizzy.tsx │ │ │ │ ├── RotateTwo.tsx │ │ │ │ ├── Calendar.tsx │ │ │ │ ├── UserScan.tsx │ │ │ │ ├── Github.tsx │ │ │ │ ├── BrandAndroid.tsx │ │ │ │ ├── BrandApple.tsx │ │ │ │ ├── ClipboardCopy.tsx │ │ │ │ ├── SettingsTwo.tsx │ │ │ │ ├── SquaresFilled.tsx │ │ │ │ ├── HeartFilled.tsx │ │ │ │ ├── TimelineEventExclamation.tsx │ │ │ │ ├── Key.tsx │ │ │ │ ├── BrandEdge.tsx │ │ │ │ ├── Bug.tsx │ │ │ │ ├── CaretDownFilled.tsx │ │ │ │ ├── Svg.tsx │ │ │ │ └── BrandUbuntu.tsx │ │ │ ├── Card │ │ │ │ ├── index.ts │ │ │ │ └── Card.stories.tsx │ │ │ ├── HoverCard │ │ │ │ └── index.ts │ │ │ ├── Select │ │ │ │ └── index.ts │ │ │ ├── Table │ │ │ │ └── index.ts │ │ │ ├── Dialog │ │ │ │ └── index.ts │ │ │ └── Form │ │ │ │ └── index.ts │ │ ├── filters │ │ │ ├── components │ │ │ │ ├── Filter │ │ │ │ │ └── index.ts │ │ │ │ └── Filters │ │ │ │ │ └── index.ts │ │ │ ├── filters │ │ │ │ ├── interval │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── interval.types.ts │ │ │ │ │ └── interval.server.ts │ │ │ │ ├── date-range │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── components │ │ │ │ │ │ ├── CustomDateRange │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── CustomDateRangeLabel │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── date-range.types.ts │ │ │ │ └── index.ts │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ ├── useFiltersContext │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useFiltersContext.ts │ │ │ │ └── useFilterActiveOption │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useFilterActiveOption.tsx │ │ │ ├── helpers │ │ │ │ └── index.ts │ │ │ ├── InvalidFilterValueError.ts │ │ │ └── index.ts │ │ ├── handlers │ │ │ ├── index.ts │ │ │ ├── handle.ts │ │ │ └── helpers.ts │ │ ├── hooks │ │ │ ├── usePrevious │ │ │ │ ├── index.ts │ │ │ │ └── usePrevious.ts │ │ │ ├── useTinyKeys │ │ │ │ ├── index.ts │ │ │ │ └── useTinyKeys.ts │ │ │ ├── useMediaQuery │ │ │ │ └── index.ts │ │ │ ├── useTimezoneId │ │ │ │ ├── index.ts │ │ │ │ └── useTimezoneId.ts │ │ │ ├── useIsNavigating │ │ │ │ └── index.ts │ │ │ ├── useTimeZoneSync │ │ │ │ ├── index.ts │ │ │ │ └── useTimeZoneSync.ts │ │ │ ├── useEventRouteData │ │ │ │ └── index.ts │ │ │ ├── useRootLoaderData │ │ │ │ ├── index.ts │ │ │ │ └── useRootLoaderData.ts │ │ │ ├── useWindowVisibility │ │ │ │ └── index.ts │ │ │ ├── useIsomorphicLayoutEffect.ts │ │ │ ├── useEventData.ts │ │ │ └── index.ts │ │ ├── events │ │ │ ├── hooks │ │ │ │ ├── index.ts │ │ │ │ └── useEventContext │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useEventContext.ts │ │ │ ├── EventProvider │ │ │ │ ├── index.ts │ │ │ │ └── EventContext.ts │ │ │ ├── index.ts │ │ │ └── events.ts │ │ ├── responses │ │ │ ├── success.ts │ │ │ ├── failure.ts │ │ │ ├── empty.ts │ │ │ ├── notFound.ts │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── countryFlag.ts │ │ │ ├── waitFor.ts │ │ │ ├── index.ts │ │ │ ├── inRoute.ts │ │ │ ├── formatTime.ts │ │ │ ├── isRangeWithinToday.ts │ │ │ ├── formatNumber.ts │ │ │ └── checkForProjectClientUpdates.ts │ │ └── entry.client.tsx │ ├── .storybook │ │ ├── manager-head.html │ │ ├── mocks │ │ │ ├── modules │ │ │ │ └── @remix-run │ │ │ │ │ └── react │ │ │ │ │ ├── useSubmit.ts │ │ │ │ │ ├── useNavigate.ts │ │ │ │ │ ├── Link.tsx │ │ │ │ │ ├── useFetcher.ts │ │ │ │ │ └── useNavigation.ts │ │ │ └── index.ts │ │ ├── preview-body.html │ │ └── preview.tsx │ ├── .gitignore │ ├── public │ │ ├── favicon.ico │ │ └── images │ │ │ ├── social-preview.png │ │ │ ├── doc-index-splash.png │ │ │ ├── favicon-alternate.png │ │ │ ├── docs-docker-do-reserve-ip.jpg │ │ │ ├── favicon-alternate.svg │ │ │ └── favicon.svg │ ├── .eslintrc.json │ ├── postcss.config.js │ ├── .babelrc.json │ ├── components.json │ └── remix.env.d.ts ├── cron │ ├── .eslintrc.json │ ├── src │ │ ├── jobs │ │ │ └── rotate-salt.ts │ │ └── index.ts │ ├── tsconfig.json │ └── package.json ├── producer │ ├── .eslintrc.json │ ├── src │ │ ├── index.test.ts │ │ └── mocks │ │ │ └── index.ts │ ├── scripts │ │ ├── esbuild.watch.mjs │ │ └── esbuild.mjs │ ├── tsconfig.json │ └── package.json └── workers │ ├── .eslintrc.json │ ├── src │ ├── events.ts │ ├── aggregations.ts │ └── index.ts │ ├── scripts │ ├── esbuild.watch.mjs │ └── esbuild.mjs │ ├── tsconfig.json │ └── package.json ├── packages ├── cache.server │ ├── src │ │ ├── index.ts │ │ ├── index.test.ts │ │ └── modules │ │ │ └── ioredis.ts │ ├── .eslintrc.json │ ├── scripts │ │ ├── cache-clear.d.mts │ │ ├── cache-clear.d.mts.map │ │ ├── esbuild.watch.mjs │ │ ├── esbuild.mjs │ │ └── cache-clear.mts │ └── tsconfig.json ├── env.server │ ├── src │ │ └── index.ts │ ├── .eslintrc.json │ ├── scripts │ │ ├── esbuild.watch.mjs │ │ └── esbuild.mjs │ └── tsconfig.json ├── queues.server │ ├── src │ │ ├── index.ts │ │ └── ioredis.ts │ ├── .eslintrc.json │ ├── scripts │ │ ├── esbuild.watch.mjs │ │ └── esbuild.mjs │ └── tsconfig.json └── db.server │ ├── src │ ├── models │ │ ├── webVitals │ │ │ └── index.ts │ │ ├── sourcemaps │ │ │ └── sourcemaps.types.ts │ │ ├── spans │ │ │ └── spans.types.ts │ │ ├── events │ │ │ └── events.types.ts │ │ └── actions.ts │ ├── schema │ │ └── index.ts │ ├── modules │ │ ├── slugify.ts │ │ ├── deviceDetector.ts │ │ └── clickhouse.ts │ ├── utils │ │ ├── buildJsonObject.ts │ │ ├── prettyPrintZodError.ts │ │ ├── timeZones.ts │ │ ├── device.ts │ │ ├── url.ts │ │ └── referrer.ts │ └── legacy │ │ └── legacySpan.ts │ ├── drizzle │ └── migrations │ │ ├── 0023_third_firelord.sql │ │ ├── 0003_great_darkstar.sql │ │ ├── 0011_omniscient_vermin.sql │ │ ├── 0004_common_zombie.sql │ │ ├── 0013_broad_speed.sql │ │ ├── 0034_calm_microchip.sql │ │ ├── 0035_yummy_ben_parker.sql │ │ ├── 0015_majestic_stellaris.sql │ │ ├── 0016_curved_the_spike.sql │ │ ├── 0020_conscious_surge.sql │ │ ├── 0039_purple_iron_monger.sql │ │ ├── 0043_medical_master_chief.sql │ │ ├── 0036_sturdy_eddie_brock.sql │ │ ├── 0038_chilly_ultragirl.sql │ │ ├── 0037_dazzling_mikhail_rasputin.sql │ │ ├── 0010_little_morgan_stark.sql │ │ ├── 0002_cloudy_doomsday.sql │ │ ├── 0008_nebulous_talkback.sql │ │ ├── 0042_melodic_zarda.sql │ │ ├── 0012_busy_shockwave.sql │ │ ├── 0014_fearless_blazing_skull.sql │ │ ├── 0000_lucky_justice.sql │ │ ├── 0018_loose_bulldozer.sql │ │ ├── 0029_previous_the_liberteens.sql │ │ ├── 0006_sudden_kate_bishop.sql │ │ ├── 0007_perpetual_spectrum.sql │ │ ├── 0022_bent_living_mummy.sql │ │ ├── 0025_spicy_sentinel.sql │ │ ├── 0032_cool_vampiro.sql │ │ ├── 0005_zippy_shaman.sql │ │ ├── 0019_massive_thunderbird.sql │ │ ├── 0040_stiff_captain_britain.sql │ │ ├── 0041_aberrant_meltdown.sql │ │ ├── 0027_freezing_jean_grey.sql │ │ ├── 0033_little_wrecker.sql │ │ ├── 0030_warm_jazinda.sql │ │ └── 0009_volatile_mach_iv.sql │ ├── .eslintrc.json │ ├── scripts │ ├── esbuild.watch.mjs │ └── esbuild.mjs │ ├── clickhouse │ ├── template.js │ ├── clickhouse.js │ └── migrations │ │ └── 1705987423878-create-events-table.js │ ├── drizzle.config.ts │ └── tsconfig.json ├── pnpm-workspace.yaml ├── .vscode ├── extensions.json └── settings.json ├── .prettierrc ├── .eslintignore ├── scripts └── setup.mjs ├── .gitignore ├── .env.example └── Dockerfile /README.md: -------------------------------------------------------------------------------- 1 | ### Metronome 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/documents/changelog.mdoc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/app/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { nanoid } from './nanoid'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/Ping/index.ts: -------------------------------------------------------------------------------- 1 | export { Ping } from './Ping'; 2 | -------------------------------------------------------------------------------- /packages/cache.server/src/index.ts: -------------------------------------------------------------------------------- 1 | export { cache } from './cache'; 2 | -------------------------------------------------------------------------------- /apps/cron/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json" 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/app/components/Badge/index.ts: -------------------------------------------------------------------------------- 1 | export { Badge } from './Badge'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/Brand/index.ts: -------------------------------------------------------------------------------- 1 | export { Brand } from './Brand'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { Header } from './Header'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/Input/index.ts: -------------------------------------------------------------------------------- 1 | export { Input } from './Input'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/Label/index.ts: -------------------------------------------------------------------------------- 1 | export { Label } from './Label'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/Switch/index.ts: -------------------------------------------------------------------------------- 1 | export { Switch } from './Switch'; 2 | -------------------------------------------------------------------------------- /packages/env.server/src/index.ts: -------------------------------------------------------------------------------- 1 | export { env, Environment } from './env'; 2 | -------------------------------------------------------------------------------- /packages/queues.server/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as queues from './queues'; 2 | -------------------------------------------------------------------------------- /apps/producer/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json" 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/.storybook/manager-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/web/app/components/Heading/index.ts: -------------------------------------------------------------------------------- 1 | export { Heading } from './Heading'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/Spinner/index.ts: -------------------------------------------------------------------------------- 1 | export { Spinner } from './Spinner'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export { toSlug } from './toSlug'; 2 | -------------------------------------------------------------------------------- /apps/workers/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json" 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/app/components/Calendar/index.ts: -------------------------------------------------------------------------------- 1 | export { Calendar } from './Calendar'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/Checkbox/index.ts: -------------------------------------------------------------------------------- 1 | export { Checkbox } from './Checkbox'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/Container/index.ts: -------------------------------------------------------------------------------- 1 | export { Container } from './Container'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/Separator/index.ts: -------------------------------------------------------------------------------- 1 | export { Separator } from './Separator'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/UserMenu/index.ts: -------------------------------------------------------------------------------- 1 | export { UserMenu } from './UserMenu'; 2 | -------------------------------------------------------------------------------- /apps/web/app/filters/components/Filter/index.ts: -------------------------------------------------------------------------------- 1 | export { Filter } from './Filter'; 2 | -------------------------------------------------------------------------------- /apps/web/app/handlers/index.ts: -------------------------------------------------------------------------------- 1 | export { createHandler, handle } from './handle'; 2 | -------------------------------------------------------------------------------- /apps/web/app/hooks/usePrevious/index.ts: -------------------------------------------------------------------------------- 1 | export { usePrevious } from "./usePrevious"; 2 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useTinyKeys/index.ts: -------------------------------------------------------------------------------- 1 | export { useTinyKeys } from './useTinyKeys'; 2 | -------------------------------------------------------------------------------- /packages/cache.server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/env.server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json" 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | /.cache 4 | /build 5 | /public/build 6 | .env 7 | -------------------------------------------------------------------------------- /apps/web/app/components/AspectRatio/index.ts: -------------------------------------------------------------------------------- 1 | export { AspectRatio } from './AspectRatio'; 2 | -------------------------------------------------------------------------------- /apps/web/app/events/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useEventContext } from './useEventContext'; 2 | -------------------------------------------------------------------------------- /apps/web/app/filters/components/Filters/index.ts: -------------------------------------------------------------------------------- 1 | export { Filters } from './Filters'; 2 | -------------------------------------------------------------------------------- /apps/web/app/filters/filters/interval/index.ts: -------------------------------------------------------------------------------- 1 | export { interval } from './interval'; 2 | -------------------------------------------------------------------------------- /packages/queues.server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | - "client/*" 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["stripe.markdoc-language-support"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/app/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { Button, buttonVariants } from './Button'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/RouteDisplay/index.ts: -------------------------------------------------------------------------------- 1 | export { RouteDisplay } from './RouteDisplay'; 2 | -------------------------------------------------------------------------------- /apps/web/app/filters/filters/date-range/index.ts: -------------------------------------------------------------------------------- 1 | export { dateRange } from './date-range'; 2 | -------------------------------------------------------------------------------- /apps/web/app/filters/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useFiltersContext } from './useFiltersContext'; 2 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useMediaQuery/index.ts: -------------------------------------------------------------------------------- 1 | export { useMediaQuery } from './useMediaQuery'; 2 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useTimezoneId/index.ts: -------------------------------------------------------------------------------- 1 | export { useTimezoneId } from './useTimezoneId'; 2 | -------------------------------------------------------------------------------- /packages/db.server/src/models/webVitals/index.ts: -------------------------------------------------------------------------------- 1 | export * as webVitals from './webVitals'; 2 | -------------------------------------------------------------------------------- /apps/web/app/components/BarStackChart/index.ts: -------------------------------------------------------------------------------- 1 | export { BarStackChart } from './BarStackChart'; 2 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useIsNavigating/index.ts: -------------------------------------------------------------------------------- 1 | export { useIsNavigating } from './useIsNavigating'; 2 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useTimeZoneSync/index.ts: -------------------------------------------------------------------------------- 1 | export { useTimeZoneSync } from './useTimeZoneSync'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/DocsHeader/index.ts: -------------------------------------------------------------------------------- 1 | export { DocsHeader } from "./DocsHeader"; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Code/index.ts: -------------------------------------------------------------------------------- 1 | export { Code } from './Code'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Link/index.ts: -------------------------------------------------------------------------------- 1 | export { Link } from './Link'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useTeamLoaderData } from './useTeamLoaderData'; 2 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0023_third_firelord.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "usage" RENAME TO "usages"; -------------------------------------------------------------------------------- /apps/web/app/components/UrlRouteDisplay/index.ts: -------------------------------------------------------------------------------- 1 | export { UrlRouteDisplay } from './UrlRouteDisplay'; 2 | -------------------------------------------------------------------------------- /apps/web/app/events/hooks/useEventContext/index.ts: -------------------------------------------------------------------------------- 1 | export { useEventContext } from './useEventContext'; 2 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useEventRouteData/index.ts: -------------------------------------------------------------------------------- 1 | export { useEventRouteData } from './useEventRouteData'; 2 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useRootLoaderData/index.ts: -------------------------------------------------------------------------------- 1 | export { useRootLoaderData } from './useRootLoaderData'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Fence/index.ts: -------------------------------------------------------------------------------- 1 | export { Fence } from './Fence'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Image/index.ts: -------------------------------------------------------------------------------- 1 | export { Image } from "./Image"; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useMarkdoc } from './useMarkdoc'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/components/TeamsHeader/index.ts: -------------------------------------------------------------------------------- 1 | export { TeamsHeader } from './TeamsHeader'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/components/Metric/index.ts: -------------------------------------------------------------------------------- 1 | export { Metric } from './Metric'; 2 | -------------------------------------------------------------------------------- /packages/cache.server/scripts/cache-clear.d.mts: -------------------------------------------------------------------------------- 1 | export {}; 2 | //# sourceMappingURL=cache-clear.d.mts.map -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0003_great_darkstar.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users" ADD COLUMN "avatar" text; -------------------------------------------------------------------------------- /packages/db.server/src/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from './aggregations'; 2 | export * from './schema'; 3 | -------------------------------------------------------------------------------- /apps/web/app/components/TableWithBarChart/index.ts: -------------------------------------------------------------------------------- 1 | export { TableWithBarChart } from './TableWithBarChart'; 2 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useWindowVisibility/index.ts: -------------------------------------------------------------------------------- 1 | export { useWindowVisibility } from './useWindowVisibility'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { Button } from './Button'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Heading/index.ts: -------------------------------------------------------------------------------- 1 | export { Heading } from "./Heading"; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Strong/index.ts: -------------------------------------------------------------------------------- 1 | export { Strong } from './Strong'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/components/Section/index.ts: -------------------------------------------------------------------------------- 1 | export { Section } from './Section'; 2 | -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metronome-sh/metronome/HEAD/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0011_omniscient_vermin.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects" ADD COLUMN "runtime" text; -------------------------------------------------------------------------------- /apps/web/app/filters/hooks/useFiltersContext/index.ts: -------------------------------------------------------------------------------- 1 | export { useFiltersContext } from './useFiltersContext'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Paragraph/index.ts: -------------------------------------------------------------------------------- 1 | export { Paragraph } from "./Paragraph"; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/hooks/useMarkdoc/index.ts: -------------------------------------------------------------------------------- 1 | export { useMarkdoc } from "./useMarkdoc"; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './hooks'; 3 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/TableOfContents/index.ts: -------------------------------------------------------------------------------- 1 | export { TableOfContents } from './TableOfContents'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/hooks/useDocsLoaderData/index.ts: -------------------------------------------------------------------------------- 1 | export { useDocsLoaderData } from './useDocsLoaderData'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/components/ProjectSelector/index.ts: -------------------------------------------------------------------------------- 1 | export { ProjectSelector } from './ProjectSelector'; 2 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0004_common_zombie.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users" ADD COLUMN "strategy_user_id" text; -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0013_broad_speed.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects" ALTER COLUMN "name" SET NOT NULL; -------------------------------------------------------------------------------- /apps/web/app/routes/teams/hooks/useTeamLoaderData/index.ts: -------------------------------------------------------------------------------- 1 | export { useTeamLoaderData } from './useTeamLoaderData'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/components/Navigation/index.ts: -------------------------------------------------------------------------------- 1 | export { Navigation } from './Navigation'; 2 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0034_calm_microchip.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects" ALTER COLUMN "is_new" SET DEFAULT true; -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0035_yummy_ben_parker.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects" ALTER COLUMN "is_new" SET NOT NULL; -------------------------------------------------------------------------------- /apps/web/app/filters/hooks/useFilterActiveOption/index.ts: -------------------------------------------------------------------------------- 1 | export { useFilterActiveOption } from './useFilterActiveOption'; 2 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0015_majestic_stellaris.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects" ALTER COLUMN "api_key" SET NOT NULL; -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0016_curved_the_spike.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects" ALTER COLUMN "deleted" SET DEFAULT false; -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0020_conscious_surge.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "usage" ALTER COLUMN "timestamp" SET DEFAULT now(); -------------------------------------------------------------------------------- /apps/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../../.eslintrc.json", 4 | "plugin:storybook/recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/app/filters/filters/date-range/components/CustomDateRange/index.ts: -------------------------------------------------------------------------------- 1 | export { CustomDateRange } from './CustomDateRange'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/components/ProjectSelector/components/index.ts: -------------------------------------------------------------------------------- 1 | export { NewProjectDialog } from './NewProjectDialog'; 2 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0039_purple_iron_monger.sql: -------------------------------------------------------------------------------- 1 | CREATE INDEX idx_users_settings ON users USING GIN (settings); 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/resources/healthcheck.route.ts: -------------------------------------------------------------------------------- 1 | export function loader() { 2 | return new Response(null, { status: 200 }); 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/public/images/social-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metronome-sh/metronome/HEAD/apps/web/public/images/social-preview.png -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0043_medical_master_chief.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects" ADD COLUMN "is_using_vite" boolean DEFAULT false; -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2 7 | } 8 | -------------------------------------------------------------------------------- /apps/producer/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | 3 | test('index.ts', () => { 4 | expect(true).toBe(true); 5 | }); 6 | -------------------------------------------------------------------------------- /apps/web/app/filters/filters/date-range/components/CustomDateRangeLabel/index.ts: -------------------------------------------------------------------------------- 1 | export { CustomDateRangeLabel } from './CustomDateRangeLabel'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/List/index.ts: -------------------------------------------------------------------------------- 1 | export { List } from './List'; 2 | export { ListItem } from './ListItem'; 3 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/components/ProjectSelector/components/NewProjectDialog/index.ts: -------------------------------------------------------------------------------- 1 | export { NewProjectDialog } from './NewProjectDialog'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/components/VersionNotification/index.ts: -------------------------------------------------------------------------------- 1 | export { VersionNotification } from './VersionNotification'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/ActionsSection/components/Chart/index.ts: -------------------------------------------------------------------------------- 1 | export { Chart } from './Chart'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/ActionsSection/index.ts: -------------------------------------------------------------------------------- 1 | export { ActionsSection } from './ActionsSection'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/LoadersSection/components/Chart/index.ts: -------------------------------------------------------------------------------- 1 | export { Chart } from './Chart'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/LoadersSection/index.ts: -------------------------------------------------------------------------------- 1 | export { LoadersSection } from './LoadersSection'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/RequestsSection/components/Chart/index.ts: -------------------------------------------------------------------------------- 1 | export { Chart } from './Chart'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/components/DangerZoneForm/index.ts: -------------------------------------------------------------------------------- 1 | export { DangerZoneForm } from './DangerZoneForm'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/components/InformationForm/components/Usage/index.ts: -------------------------------------------------------------------------------- 1 | export { Usage } from './Usage'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/components/VisibilityForm/index.ts: -------------------------------------------------------------------------------- 1 | export { VisibilityForm } from './VisibilityForm'; 2 | -------------------------------------------------------------------------------- /apps/web/public/images/doc-index-splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metronome-sh/metronome/HEAD/apps/web/public/images/doc-index-splash.png -------------------------------------------------------------------------------- /apps/web/public/images/favicon-alternate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metronome-sh/metronome/HEAD/apps/web/public/images/favicon-alternate.png -------------------------------------------------------------------------------- /apps/web/app/routes/docs/helpers/toSlug.ts: -------------------------------------------------------------------------------- 1 | export function toSlug(text: string) { 2 | return text.toLowerCase().replace(/\s|\.|;/g, '-'); 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ProjectSelector } from './ProjectSelector'; 2 | export { TeamsHeader } from './TeamsHeader'; 3 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/ActionsSection/components/Errors/index.ts: -------------------------------------------------------------------------------- 1 | export { Errors } from './Errors'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/LoadersSection/components/Errors/index.ts: -------------------------------------------------------------------------------- 1 | export { Errors } from './Errors'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/RequestsSection/index.ts: -------------------------------------------------------------------------------- 1 | export { RequestsSection } from './RequestsSection'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebVitalsSection/index.ts: -------------------------------------------------------------------------------- 1 | export { WebVitalsSection } from './WebVitalsSection'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/components/InformationForm/index.ts: -------------------------------------------------------------------------------- 1 | export { InformationForm } from './InformationForm'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/DevicesSection/index.ts: -------------------------------------------------------------------------------- 1 | export { DevicesSection } from './DevicesSection'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/RoutesSection/index.ts: -------------------------------------------------------------------------------- 1 | export { RoutesSection } from './RoutesSection'; 2 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0036_sturdy_eddie_brock.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "teams" ADD COLUMN "settings" jsonb DEFAULT '{"subscription":null}'::jsonb; -------------------------------------------------------------------------------- /apps/web/app/responses/success.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@remix-run/node'; 2 | 3 | export function success() { 4 | return json({ success: true }); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/hooks/useTeamProjectLoaderData/index.ts: -------------------------------------------------------------------------------- 1 | export { useTeamProjectLoaderData } from './useTeamProjectLoaderData'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/ActionsSection/components/Duration/index.ts: -------------------------------------------------------------------------------- 1 | export { Duration } from './Duration'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/LoadersSection/components/Duration/index.ts: -------------------------------------------------------------------------------- 1 | export { Duration } from './Duration'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/RequestsSection/components/Duration/index.ts: -------------------------------------------------------------------------------- 1 | export { Duration } from './Duration'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/RequestsSection/components/Requests/index.ts: -------------------------------------------------------------------------------- 1 | export { Requests } from './Requests'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/LocationsSection/index.ts: -------------------------------------------------------------------------------- 1 | export { LocationsSection } from './LocationsSection'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/ReferrersSection/index.ts: -------------------------------------------------------------------------------- 1 | export { ReferrersSection } from './ReferrersSection'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-vitals/components/WebVitalsSection/index.ts: -------------------------------------------------------------------------------- 1 | export { WebVitalsSection } from './WebVitalsSection'; 2 | -------------------------------------------------------------------------------- /apps/web/app/utils/countryFlag.ts: -------------------------------------------------------------------------------- 1 | import getUnicodeFlagIcon from 'country-flag-icons/unicode'; 2 | 3 | export const countryFlag = getUnicodeFlagIcon; 4 | -------------------------------------------------------------------------------- /packages/cache.server/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | 3 | test('index.ts', () => { 4 | expect(true).toBe(true); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/db.server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "rules": { 4 | "@typescript-eslint/no-shadow": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/app/components/Notifications/index.ts: -------------------------------------------------------------------------------- 1 | export { Notification } from './Notification'; 2 | export { NotificationsOutlet } from './NotificationsOutlet'; 3 | -------------------------------------------------------------------------------- /apps/web/app/responses/failure.ts: -------------------------------------------------------------------------------- 1 | import { json } from '@remix-run/node'; 2 | 3 | export function failure() { 4 | return json({ success: false }); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebAnalyticsSection/index.ts: -------------------------------------------------------------------------------- 1 | export { WebAnalyticsSection } from './WebAnalyticsSection'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/hooks/useOverviewEventData/index.ts: -------------------------------------------------------------------------------- 1 | export { useOverviewEventData } from './useOverviewEventData'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/hooks/useOverviewLoaderData/index.ts: -------------------------------------------------------------------------------- 1 | export { useOverviewLoaderData } from './useOverviewLoaderData'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/components/GeneralSettingsForm/index.ts: -------------------------------------------------------------------------------- 1 | export { GeneralSettingsForm } from './GeneralSettingsForm'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/hooks/useSettingsEventData/index.ts: -------------------------------------------------------------------------------- 1 | export { useSettingsEventData } from './useSettingsEventData'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/hooks/useSettingsLoaderData/index.ts: -------------------------------------------------------------------------------- 1 | export { useSettingsLoaderData } from './useSettingsLoaderData'; 2 | -------------------------------------------------------------------------------- /apps/web/public/images/docs-docker-do-reserve-ip.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/metronome-sh/metronome/HEAD/apps/web/public/images/docs-docker-do-reserve-ip.jpg -------------------------------------------------------------------------------- /packages/cache.server/scripts/cache-clear.d.mts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"cache-clear.d.mts","sourceRoot":"","sources":["cache-clear.mts"],"names":[],"mappings":""} -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/ActionsSection/components/Invocations/index.ts: -------------------------------------------------------------------------------- 1 | export { Invocations } from './Invocations'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/LoadersSection/components/Invocations/index.ts: -------------------------------------------------------------------------------- 1 | export { Invocations } from './Invocations'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/RequestsSection/components/DataRequests/index.ts: -------------------------------------------------------------------------------- 1 | export { DataRequests } from './DataRequests'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebAnalyticsSection/components/BounceRate/index.ts: -------------------------------------------------------------------------------- 1 | export { BounceRate } from './BounceRate'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/components/InformationForm/components/RotateApiKey/index.ts: -------------------------------------------------------------------------------- 1 | export { RotateApiKey } from './RotateApiKey'; 2 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0038_chilly_ultragirl.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "teams" ALTER COLUMN "settings" SET DEFAULT '{"customerId":null,"subscription":null}'::jsonb; -------------------------------------------------------------------------------- /apps/web/app/events/EventProvider/index.ts: -------------------------------------------------------------------------------- 1 | export { EventContext, type EventContextValue } from './EventContext'; 2 | export { EventProvider } from './EventProvider'; 3 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebVitalsSection/components/WebVitalsCard/index.ts: -------------------------------------------------------------------------------- 1 | export { WebVitalsCard } from './WebVitalsCard'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/hooks/useIsNavigatingOverview/index.ts: -------------------------------------------------------------------------------- 1 | export { useIsNavigatingOverview } from './useIsNavigatingOverview'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/components/DangerZoneForm/components/DeleteProject/index.ts: -------------------------------------------------------------------------------- 1 | export { DeleteProject } from './DeleteProject'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/DevicesSection/components/OsTabContent/index.ts: -------------------------------------------------------------------------------- 1 | export { OsTabContent } from './OsTabContent'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/ReferrersSection/ReferrersTable/index.ts: -------------------------------------------------------------------------------- 1 | export { ReferrersTable } from './ReferrersTable'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebAnalyticsSection/components/TotalPageviews/index.ts: -------------------------------------------------------------------------------- 1 | export { TotalPageviews } from './TotalPageviews'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebAnalyticsSection/components/TotalSessions/index.ts: -------------------------------------------------------------------------------- 1 | export { TotalSessions } from './TotalSessions'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/hooks/useWebAnalyticsEventData/index.ts: -------------------------------------------------------------------------------- 1 | export { useWebAnalyticsEventData } from './useWebAnalyticsEventData'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-vitals/components/WebVitalsByRouteSection/index.ts: -------------------------------------------------------------------------------- 1 | export { WebVitalsByRouteSection } from './WebVitalsByRouteSection'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-vitals/components/WebVitalsSection/components/WebVitalsCard/index.ts: -------------------------------------------------------------------------------- 1 | export { WebVitalsCard } from './WebVitalsCard'; 2 | -------------------------------------------------------------------------------- /apps/web/app/filters/filters/index.ts: -------------------------------------------------------------------------------- 1 | import { dateRange } from './date-range'; 2 | import { interval } from './interval'; 3 | 4 | export const filters = { dateRange, interval }; 5 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/RequestsSection/components/DocumentRequests/index.ts: -------------------------------------------------------------------------------- 1 | export { DocumentRequests } from './DocumentRequests'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebVitalsSection/components/WebVitalsCard/components/index.ts: -------------------------------------------------------------------------------- 1 | export { WebVitalBar } from './WebVitalBar'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/hooks/useWebAnalyticsLoaderData/index.ts: -------------------------------------------------------------------------------- 1 | export { useWebAnalyticsLoaderData } from './useWebAnalyticsLoaderData'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-vitals/components/WebVitalsSection/components/WebVitalsCard/components/index.ts: -------------------------------------------------------------------------------- 1 | export { WebVitalBar } from './WebVitalBar'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebAnalyticsSection/components/VisitorsRightNow/index.ts: -------------------------------------------------------------------------------- 1 | export { VisitorsRightNow } from './VisitorsRightNow'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/index.ts: -------------------------------------------------------------------------------- 1 | export { GeneralWebAnalyticsSection } from './GeneralWebAnalyticsSection'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/LocationsSection/components/CitiesTabContent/index.ts: -------------------------------------------------------------------------------- 1 | export { CitiesTabContent } from './CitiesTabContent'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/hooks/useIsNavigatingWebAnalytics/index.ts: -------------------------------------------------------------------------------- 1 | export { useIsNavigatingWebAnalytics } from './useIsNavigatingWebAnalytics'; 2 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0037_dazzling_mikhail_rasputin.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "teams" ALTER COLUMN "settings" SET DEFAULT '{"subscription":null,"subscriptionCreated":false}'::jsonb; -------------------------------------------------------------------------------- /apps/producer/src/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export { generateRemixFunction } from './remixFunction'; 2 | export { generateRequest } from './request'; 3 | export { generateWebVital } from './webVital'; 4 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebVitalsSection/components/WebVitalsCard/components/WebVitalBar/index.ts: -------------------------------------------------------------------------------- 1 | export { WebVitalBar } from './WebVitalBar'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/DevicesSection/components/BrowsersTabContent/index.ts: -------------------------------------------------------------------------------- 1 | export { BrowsersTabContent } from './BrowsersTabContent'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/ViewsTabContent/index.ts: -------------------------------------------------------------------------------- 1 | export { ViewsTabContent } from './ViewsTabContent'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-vitals/components/WebVitalsSection/components/WebVitalsCard/components/WebVitalBar/index.ts: -------------------------------------------------------------------------------- 1 | export { WebVitalBar } from './WebVitalBar'; 2 | -------------------------------------------------------------------------------- /apps/web/app/responses/empty.ts: -------------------------------------------------------------------------------- 1 | export function empty(options?: { 2 | headers?: Record; 3 | }): Response { 4 | return new Response(null, { headers: options?.headers }); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/documents/reference/web-analytics.mdoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Web Analytics" 3 | description: "Web Analytics reference" 4 | --- 5 | 6 | # Web Analytics 7 | 8 | Coming soon. 9 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/ViewsTabTrigger/index.tsx: -------------------------------------------------------------------------------- 1 | export { ViewsTabTrigger } from './ViewsTabTrigger'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/LocationsSection/components/CountriesTabContent/index.ts: -------------------------------------------------------------------------------- 1 | export { CountriesTabContent } from './CountriesTabContent'; 2 | -------------------------------------------------------------------------------- /apps/cron/src/jobs/rotate-salt.ts: -------------------------------------------------------------------------------- 1 | import { projects } from '@metronome/db.server'; 2 | 3 | await projects.rotateSalts(); 4 | 5 | console.log('Projects salt rotated.'); 6 | 7 | process.exit(0); 8 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebAnalyticsSection/components/SessionMedianDuration/index.ts: -------------------------------------------------------------------------------- 1 | export { SessionMedianDuration } from './SessionMedianDuration'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/SessionsTabContent/index.ts: -------------------------------------------------------------------------------- 1 | export { SessionsTabContent } from './SessionsTabContent'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/SessionsTabTrigger/index.tsx: -------------------------------------------------------------------------------- 1 | export { SessionsTabTrigger } from './SessionsTabTrigger'; 2 | -------------------------------------------------------------------------------- /apps/web/app/filters/filters/interval/interval.types.ts: -------------------------------------------------------------------------------- 1 | export type IntervalParsed = 'hour' | 'day' | 'week' | 'month'; 2 | 3 | export type IntervalOptionIds = 'hourly' | 'daily' | 'weekly' | 'monthly'; 4 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export { Sidebar } from './Sidebar'; 2 | export { SidebarContainer } from './SidebarContainer'; 3 | export { SidebarSection } from './SidebarSection'; 4 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useTeamProjectLoaderData } from './useTeamProjectLoaderData'; 2 | export { useTeamProjectEventData } from './useTeamProjectEventData'; 3 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useSettingsEventData } from './useSettingsEventData'; 2 | export { useSettingsLoaderData } from './useSettingsLoaderData'; 3 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/BounceRateTabContent/index.ts: -------------------------------------------------------------------------------- 1 | export { BounceRateTabContent } from './BounceRateTabContent'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/InstallationTargets/index.ts: -------------------------------------------------------------------------------- 1 | export { InstallationTarget } from './InstallationTarget'; 2 | export { InstallationTargets } from './InstallationTargets'; 3 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/BounceRateTabTrigger/index.tsx: -------------------------------------------------------------------------------- 1 | export { BounceRateTabTrigger } from './BounceRateTabTrigger'; 2 | -------------------------------------------------------------------------------- /apps/workers/src/events.ts: -------------------------------------------------------------------------------- 1 | import { queues } from '@metronome/queues.server'; 2 | 3 | export async function events(job: typeof queues.events.$inferJob) { 4 | const { data } = job; 5 | return data; 6 | } 7 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0010_little_morgan_stark.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects" ALTER COLUMN "team_id" SET NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "teams" ADD COLUMN "created_by" text NOT NULL; -------------------------------------------------------------------------------- /apps/web/app/components/AspectRatio/AspectRatio.tsx: -------------------------------------------------------------------------------- 1 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'; 2 | 3 | const AspectRatio = AspectRatioPrimitive.Root; 4 | 5 | export { AspectRatio }; 6 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useTimezoneId/useTimezoneId.ts: -------------------------------------------------------------------------------- 1 | import { useRootLoaderData } from '..'; 2 | 3 | export function useTimezoneId() { 4 | const { timeZoneId } = useRootLoaderData(); 5 | return timeZoneId; 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/app/responses/notFound.ts: -------------------------------------------------------------------------------- 1 | export function notFound(options?: { 2 | headers?: Record; 3 | }): Response { 4 | return new Response(null, { headers: options?.headers, status: 404 }); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/VisitorsChartTabContent/index.ts: -------------------------------------------------------------------------------- 1 | export { VisitorsChartTabContent } from './VisitorsChartTabContent'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/RoutesSection/components/RoutesSectionUrlsTabContent/index.ts: -------------------------------------------------------------------------------- 1 | export { RoutesSectionUrlsTabContent } from './RoutesSectionUrlsTabContent'; 2 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0002_cloudy_doomsday.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users" ADD COLUMN "strategy" text;--> statement-breakpoint 2 | ALTER TABLE "users" ADD COLUMN "settings" jsonb DEFAULT '{"emails":[]}'::jsonb; -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0008_nebulous_talkback.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users" ALTER COLUMN "settings" SET DEFAULT '{"emails":[],"selectedEmail":null,"lastSelectedProject":null,"lastSelectedTeam":null}'::jsonb; -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0042_melodic_zarda.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects" ALTER COLUMN "client_version" SET DEFAULT '0.0.0';--> statement-breakpoint 2 | ALTER TABLE "projects" DROP COLUMN IF EXISTS "version"; -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/scripts/**.mjs 2 | **/scripts/**.mts 3 | **/drizzle/migrate.mts 4 | **/templates/**/* 5 | **/dist/**/* 6 | drizzle.config.ts 7 | tailwind.config.ts 8 | **/*.stories.tsx 9 | **/migrations/**/* 10 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useIsomorphicLayoutEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useLayoutEffect } from 'react'; 2 | 3 | export const useIsomorphicLayoutEffect = 4 | typeof window !== 'undefined' ? useLayoutEffect : useEffect; 5 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/UniqueVisitorsTabTrigger/index.tsx: -------------------------------------------------------------------------------- 1 | export { UniqueVisitorsTabTrigger } from './UniqueVisitorsTabTrigger'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/RoutesSection/components/RoutesSectionPathsTabContent/index.ts: -------------------------------------------------------------------------------- 1 | export { RoutesSectionPathsTabContent } from './RoutesSectionPathsTabContent'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/index.ts: -------------------------------------------------------------------------------- 1 | export { GeneralWebAnalyticsSection } from './GeneralWebAnalyticsSection'; 2 | export { LocationsSection } from './LocationsSection'; 3 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0012_busy_shockwave.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users" ALTER COLUMN "settings" SET DEFAULT '{"emails":[],"selectedEmail":null,"lastSelectedProjectSlug":null,"lastSelectedTeamSlug":null}'::jsonb; -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0014_fearless_blazing_skull.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects" ADD COLUMN "isPublic" boolean DEFAULT false;--> statement-breakpoint 2 | ALTER TABLE "projects" DROP COLUMN IF EXISTS "visibility"; -------------------------------------------------------------------------------- /apps/web/app/components/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/VisitorsRightNowTabTrigger/index.tsx: -------------------------------------------------------------------------------- 1 | export { VisitorsRightNowTabTrigger } from './VisitorsRightNowTabTrigger'; 2 | -------------------------------------------------------------------------------- /apps/web/app/responses/index.ts: -------------------------------------------------------------------------------- 1 | export { empty } from './empty'; 2 | export { failure } from './failure'; 3 | export { notFound } from './notFound'; 4 | export { stream } from './stream'; 5 | export { success } from './success'; 6 | -------------------------------------------------------------------------------- /apps/web/app/routes/shared/hooks/useIsNavigatingSharedProject.ts: -------------------------------------------------------------------------------- 1 | import { useIsNavigating } from '#app/hooks'; 2 | 3 | export function useIsNavigatingSharedProject() { 4 | return useIsNavigating('shared.$projectId'); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/MedianSessionTimeTabContent/index.ts: -------------------------------------------------------------------------------- 1 | export { MedianSessionTimeTabContent } from './MedianSessionTimeTabContent'; 2 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/MedianSessionTimeTabTrigger/index.tsx: -------------------------------------------------------------------------------- 1 | export { MedianSessionTimeTabTrigger } from './MedianSessionTimeTabTrigger'; 2 | -------------------------------------------------------------------------------- /apps/web/.storybook/mocks/modules/@remix-run/react/useSubmit.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | 3 | export function useSubmit() { 4 | return (data: any) => { 5 | action('submit')(data); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/errors/components/ErrorsList.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | export const ErrorsList: FunctionComponent = () => { 4 | return
Errors List
; 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web/.storybook/mocks/modules/@remix-run/react/useNavigate.ts: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | 3 | export function useNavigate() { 4 | return (data: any) => { 5 | action('navigate')(data); 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/components/ScrollArea/index.ts: -------------------------------------------------------------------------------- 1 | import { ScrollArea as ScrollAreaComponent, ScrollBar, Thumb } from './ScrollArea'; 2 | 3 | export const ScrollArea = Object.assign(ScrollAreaComponent, { 4 | ScrollBar, 5 | Thumb, 6 | }); 7 | -------------------------------------------------------------------------------- /apps/web/app/handlers/handle.ts: -------------------------------------------------------------------------------- 1 | export { createHandler } from './createHandler'; 2 | import { createHandler } from './createHandler'; 3 | 4 | export const handle = async (request: Request) => { 5 | return createHandler()(request); 6 | }; 7 | -------------------------------------------------------------------------------- /apps/web/app/events/index.ts: -------------------------------------------------------------------------------- 1 | export { EventProvider } from './EventProvider'; 2 | export type { MetronomeEventName } from './events'; 3 | export { getObservableRoutes } from './getObservableRoutes'; 4 | export { useEventContext } from './hooks'; 5 | -------------------------------------------------------------------------------- /apps/web/app/modules/nanoid.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from 'nanoid'; 2 | 3 | // prettier-ignore 4 | const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 5 | 6 | export const nanoid = customAlphabet(alphabet, 10); 7 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/index.ts: -------------------------------------------------------------------------------- 1 | export { DocsHeader } from './DocsHeader'; 2 | export { useMarkdoc } from './Markdoc'; 3 | export { Sidebar, SidebarContainer } from './Sidebar'; 4 | export { TableOfContents } from './TableOfContents'; 5 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/getters/index.ts: -------------------------------------------------------------------------------- 1 | export { getDocumentationSections } from './getDocumentationSections'; 2 | export { getDocumentMarkdocContent } from './getDocumentMarkdocContent'; 3 | export { getDocumentMeta } from './getDocumentMeta'; 4 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Metric } from './Metric'; 2 | export { Navigation } from './Navigation'; 3 | export { Section } from './Section'; 4 | export { VersionNotification } from './VersionNotification'; 5 | -------------------------------------------------------------------------------- /packages/db.server/src/modules/slugify.ts: -------------------------------------------------------------------------------- 1 | import Slugify from 'slugify'; 2 | 3 | Slugify.extend({ '.': '-' }); 4 | 5 | export function slugify(text: string): string { 6 | return Slugify(text, { lower: true, strict: true, locale: 'en' }); 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/components/Alert/index.ts: -------------------------------------------------------------------------------- 1 | import { Alert as AlertComponent, AlertDescription, AlertTitle } from './Alert'; 2 | 3 | export const Alert = Object.assign(AlertComponent, { 4 | Title: AlertTitle, 5 | Description: AlertDescription, 6 | }); 7 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/components/TeamsHeader/TeamsHeader.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Header } from '#app/components'; 4 | 5 | export const TeamsHeader: FunctionComponent = () => { 6 | return
; 7 | }; 8 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/HorizontalRule/HorizontalRule.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | export const HorizontalRule: FunctionComponent = () => { 4 | return
; 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Table/Tr.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, PropsWithChildren } from 'react'; 2 | 3 | export const Tr: FunctionComponent = ({ children }) => { 4 | return {children}; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/db.server/src/modules/deviceDetector.ts: -------------------------------------------------------------------------------- 1 | import { remember } from '@epic-web/remember'; 2 | import DeviceDetector from 'device-detector-js'; 3 | 4 | export const deviceDetector = remember('deviceDtector', () => { 5 | return new DeviceDetector(); 6 | }); 7 | -------------------------------------------------------------------------------- /apps/web/app/components/Breadcrumb/index.ts: -------------------------------------------------------------------------------- 1 | import { Breadcrumb as BreadcrumbComponent } from './Breadcrumb'; 2 | import { BreadcrumbOutlet } from './BreadcrumbOutlet'; 3 | 4 | export const Breadcrumb = Object.assign(BreadcrumbComponent, { Outlet: BreadcrumbOutlet }); 5 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Table/index.ts: -------------------------------------------------------------------------------- 1 | export { Table } from './Table'; 2 | export { TBody } from './TBody'; 3 | export { Td } from './Td'; 4 | export { Th } from './Th'; 5 | export { THead } from './THead'; 6 | export { Tr } from './Tr'; 7 | -------------------------------------------------------------------------------- /apps/workers/scripts/esbuild.watch.mjs: -------------------------------------------------------------------------------- 1 | import { context } from 'esbuild'; 2 | 3 | import { esbuildConfig } from './esbuild.mjs'; 4 | 5 | const cjsContext = await context(esbuildConfig); 6 | 7 | await cjsContext.rebuild(); 8 | 9 | await cjsContext.watch(); 10 | -------------------------------------------------------------------------------- /apps/producer/scripts/esbuild.watch.mjs: -------------------------------------------------------------------------------- 1 | import { context } from 'esbuild'; 2 | 3 | import { esbuildConfig } from './esbuild.mjs'; 4 | 5 | const cjsContext = await context(esbuildConfig); 6 | 7 | await cjsContext.rebuild(); 8 | 9 | await cjsContext.watch(); 10 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useIsNavigatingOverview } from './useIsNavigatingOverview'; 2 | export { useOverviewEventData } from './useOverviewEventData'; 3 | export { useOverviewLoaderData } from './useOverviewLoaderData'; 4 | -------------------------------------------------------------------------------- /packages/cache.server/scripts/esbuild.watch.mjs: -------------------------------------------------------------------------------- 1 | import { context } from 'esbuild'; 2 | 3 | import { esbuildConfig } from './esbuild.mjs'; 4 | 5 | const cjsContext = await context(esbuildConfig); 6 | 7 | await cjsContext.rebuild(); 8 | 9 | await cjsContext.watch(); 10 | -------------------------------------------------------------------------------- /packages/db.server/scripts/esbuild.watch.mjs: -------------------------------------------------------------------------------- 1 | import { context } from 'esbuild'; 2 | 3 | import { esbuildConfig } from './esbuild.mjs'; 4 | 5 | const cjsContext = await context(esbuildConfig); 6 | 7 | await cjsContext.rebuild(); 8 | 9 | await cjsContext.watch(); 10 | -------------------------------------------------------------------------------- /packages/env.server/scripts/esbuild.watch.mjs: -------------------------------------------------------------------------------- 1 | import { context } from 'esbuild'; 2 | 3 | import { esbuildConfig } from './esbuild.mjs'; 4 | 5 | const cjsContext = await context(esbuildConfig); 6 | 7 | await cjsContext.rebuild(); 8 | 9 | await cjsContext.watch(); 10 | -------------------------------------------------------------------------------- /packages/queues.server/scripts/esbuild.watch.mjs: -------------------------------------------------------------------------------- 1 | import { context } from 'esbuild'; 2 | 3 | import { esbuildConfig } from './esbuild.mjs'; 4 | 5 | const cjsContext = await context(esbuildConfig); 6 | 7 | await cjsContext.rebuild(); 8 | 9 | await cjsContext.watch(); 10 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/hooks/useIsNavigatingOverview/useIsNavigatingOverview.ts: -------------------------------------------------------------------------------- 1 | import { useIsNavigating } from '#app/hooks'; 2 | 3 | export function useIsNavigatingOverview() { 4 | return useIsNavigating('$teamSlug.$projectSlug.overview'); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/app/components/Avatar/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar as AvatarComponent, 3 | AvatarFallback, 4 | AvatarImage, 5 | } from './Avatar'; 6 | 7 | export const Avatar = Object.assign(AvatarComponent, { 8 | Fallback: AvatarFallback, 9 | Image: AvatarImage, 10 | }); 11 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Table/THead.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, PropsWithChildren } from 'react'; 2 | 3 | export const THead: FunctionComponent = ({ children }) => { 4 | return {children}; 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/documents/partials/metronome-init.partial.mdoc: -------------------------------------------------------------------------------- 1 | ## Create config file 2 | 3 | The `metronome.config.js` is used to set what your project sends to Metronome. Create it by running the command below. 4 | 5 | ```bash {% title="Terminal" %} 6 | npx metronome init 7 | ``` 8 | -------------------------------------------------------------------------------- /apps/web/app/components/Popover/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Popover as PopoverComponent, 3 | PopoverContent, 4 | PopoverTrigger, 5 | } from './Popover'; 6 | 7 | export const Popover = Object.assign(PopoverComponent, { 8 | Content: PopoverContent, 9 | Trigger: PopoverTrigger, 10 | }); 11 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Table/TBody.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, PropsWithChildren } from 'react'; 2 | 3 | export const TBody: FunctionComponent = ({ children }) => { 4 | return {children}; 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-vitals/hooks/useIsNavigatingWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { useIsNavigating } from '#app/hooks'; 2 | 3 | const routeId = '$teamSlug.$projectSlug.web-vitals'; 4 | 5 | export function useIsNavigatingWebVitals() { 6 | return useIsNavigating(routeId); 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/routes/shared/hooks/useSharedProjectEventData.ts: -------------------------------------------------------------------------------- 1 | import { useEventRouteData } from '#app/hooks'; 2 | 3 | import { type loader } from '../shared.$projectId.route'; 4 | 5 | export function useSharedProjectEventData() { 6 | return useEventRouteData('shared.$projectId'); 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useIsNavigatingWebAnalytics } from './useIsNavigatingWebAnalytics'; 2 | export { useWebAnalyticsEventData } from './useWebAnalyticsEventData'; 3 | export { useWebAnalyticsLoaderData } from './useWebAnalyticsLoaderData'; 4 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/hooks/useIsNavigatingWebAnalytics/useIsNavigatingWebAnalytics.ts: -------------------------------------------------------------------------------- 1 | import { useIsNavigating } from '#app/hooks'; 2 | 3 | export function useIsNavigatingWebAnalytics() { 4 | return useIsNavigating('$teamSlug.$projectSlug.web-analytics'); 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/.storybook/preview-body.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /apps/web/app/utils/waitFor.ts: -------------------------------------------------------------------------------- 1 | export function waitFor(ms: number, cb?: () => T): Promise { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | if (cb) { 5 | resolve(cb()); 6 | } else { 7 | resolve(); 8 | } 9 | }, ms); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0000_lucky_justice.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "users" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "name" text NOT NULL, 4 | "email" text NOT NULL, 5 | "password" text, 6 | "created_at" timestamp DEFAULT now() NOT NULL, 7 | "updated_at" timestamp DEFAULT now() NOT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0018_loose_bulldozer.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "requests" RENAME COLUMN "organization_id" TO "team_id";--> statement-breakpoint 2 | DROP INDEX IF EXISTS "organization_timestamp_idx";--> statement-breakpoint 3 | CREATE INDEX IF NOT EXISTS "team_timestamp_idx" ON "requests" ("team_id","timestamp"); -------------------------------------------------------------------------------- /apps/web/app/events/EventProvider/EventContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export type EventContextValue = { 4 | eventTarget: EventTarget | null; 5 | }; 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 8 | export const EventContext = createContext(null as any); 9 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/hooks/useTeamProjectEventData.ts: -------------------------------------------------------------------------------- 1 | import { useEventRouteData } from '#app/hooks'; 2 | 3 | import { type loader } from '../$teamSlug.$projectSlug.route'; 4 | 5 | export function useTeamProjectEventData() { 6 | return useEventRouteData('$teamSlug.$projectSlug'); 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/RequestsSection/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Chart } from './Chart'; 2 | export { DataRequests } from './DataRequests'; 3 | export { DocumentRequests } from './DocumentRequests'; 4 | export { Duration } from './Duration'; 5 | export { Requests } from './Requests'; 6 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Strong/Strong.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, PropsWithChildren } from 'react'; 2 | 3 | export type StrongProps = PropsWithChildren; 4 | 5 | export const Strong: FunctionComponent = ({ children }) => { 6 | return {children}; 7 | }; 8 | -------------------------------------------------------------------------------- /apps/web/app/components/Tooltip/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Tooltip as TooltipPrimitive, 3 | TooltipContent, 4 | TooltipProvider, 5 | TooltipTrigger, 6 | } from './Tooltip'; 7 | 8 | export const Tooltip = Object.assign(TooltipPrimitive, { 9 | Content: TooltipContent, 10 | Provider: TooltipProvider, 11 | Trigger: TooltipTrigger, 12 | }); 13 | -------------------------------------------------------------------------------- /apps/web/app/filters/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | analyzeDependencies, 3 | depencenciesCollides, 4 | getInitialFilterOption, 5 | getInitialFiltersOptions, 6 | getIsCustomOption, 7 | mergeFilterOptionsWithSearch, 8 | toActiveFilterOption, 9 | toFilterOption, 10 | toMap, 11 | valueToActiveFilterOption, 12 | } from './helpers'; 13 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ActionsSection } from './ActionsSection'; 2 | export { LoadersSection } from './LoadersSection'; 3 | export { RequestsSection } from './RequestsSection'; 4 | export { WebAnalyticsSection } from './WebAnalyticsSection'; 5 | export { WebVitalsSection } from './WebVitalsSection'; 6 | -------------------------------------------------------------------------------- /packages/db.server/src/utils/buildJsonObject.ts: -------------------------------------------------------------------------------- 1 | import { sql } from 'drizzle-orm'; 2 | 3 | export function buildJsonbObject(record: Record | null | undefined) { 4 | if (!record || Object.keys(record).length === 0) { 5 | return sql.raw(`'{}'::jsonb`); 6 | } 7 | 8 | return sql.raw(`'${JSON.stringify(record)}'::jsonb`); 9 | } 10 | -------------------------------------------------------------------------------- /packages/db.server/clickhouse/template.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { clickhouse } = require('../clickhouse'); 4 | 5 | module.exports.up = async function () { 6 | await clickhouse.command({ 7 | query: ``, 8 | }); 9 | }; 10 | 11 | module.exports.down = async function () { 12 | await clickhouse.command({ 13 | query: ``, 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/db.server/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'drizzle-kit'; 2 | import { env } from '@metronome/env.server'; 3 | 4 | export default { 5 | schema: './src/schema/schema.ts', 6 | out: './drizzle/migrations', 7 | driver: 'pg', 8 | dbCredentials: { 9 | connectionString: env.db().writableUrl, 10 | }, 11 | } satisfies Config; 12 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0029_previous_the_liberteens.sql: -------------------------------------------------------------------------------- 1 | --> statement-breakpoint 2 | SELECT create_hypertable('web_vitals', 'timestamp', chunk_time_interval => interval '1 day'); 3 | --> statement-breakpoint 4 | ALTER TABLE web_vitals SET (timescaledb.compress); 5 | --> statement-breakpoint 6 | SELECT add_compression_policy('web_vitals', INTERVAL '7 days'); -------------------------------------------------------------------------------- /apps/web/app/hooks/useTinyKeys/useTinyKeys.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { tinykeys } from 'tinykeys'; 3 | 4 | export function useTinyKeys(keys: Record void>) { 5 | useEffect(() => { 6 | const unsubscribe = tinykeys(window, keys); 7 | 8 | return () => { 9 | unsubscribe(); 10 | }; 11 | }, [keys]); 12 | } 13 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0006_sudden_kate_bishop.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "apps" ADD COLUMN "slug" text;--> statement-breakpoint 2 | ALTER TABLE "teams" ADD COLUMN "slug" text;--> statement-breakpoint 3 | ALTER TABLE "apps" ADD CONSTRAINT "apps_slug_unique" UNIQUE("slug");--> statement-breakpoint 4 | ALTER TABLE "teams" ADD CONSTRAINT "teams_slug_unique" UNIQUE("slug"); -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0007_perpetual_spectrum.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users" ALTER COLUMN "settings" SET DEFAULT '{"emails":[],"selectedEmail":null,"lastViewedProject":null}'::jsonb;--> statement-breakpoint 2 | ALTER TABLE "apps" ADD COLUMN "share_slug" text;--> statement-breakpoint 3 | ALTER TABLE "apps" ADD CONSTRAINT "apps_share_slug_unique" UNIQUE("share_slug"); -------------------------------------------------------------------------------- /apps/web/app/handlers/helpers.ts: -------------------------------------------------------------------------------- 1 | import { type ZodError } from 'zod'; 2 | 3 | export function formatZodError(error: ZodError): string { 4 | return error.errors 5 | .map((err) => { 6 | const field = err.path.join('.'); 7 | const message = err.message; 8 | return `${field ? field + ': ' : ''}${message}`; 9 | }) 10 | .join(', '); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/hooks/useOverviewEventData/useOverviewEventData.ts: -------------------------------------------------------------------------------- 1 | import { useEventRouteData } from '#app/hooks'; 2 | 3 | import { type loader } from '../../$teamSlug.$projectSlug.overview.route'; 4 | 5 | export function useOverviewEventData() { 6 | return useEventRouteData('$teamSlug.$projectSlug.overview'); 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/hooks/useSettingsEventData/useSettingsEventData.ts: -------------------------------------------------------------------------------- 1 | import { useEventRouteData } from '#app/hooks'; 2 | 3 | import { type loader } from '../../$teamSlug.$projectSlug.settings.route'; 4 | 5 | export function useSettingsEventData() { 6 | return useEventRouteData('$teamSlug.$projectSlug.settings'); 7 | } 8 | -------------------------------------------------------------------------------- /packages/db.server/clickhouse/clickhouse.js: -------------------------------------------------------------------------------- 1 | const { createClient } = require('@clickhouse/client'); 2 | const { env } = require('@metronome/env.server'); 3 | 4 | const { database, host, password, port, username } = env.clickhouse(); 5 | 6 | exports.clickhouse = createClient({ 7 | host: `${host}:${port}`, 8 | username, 9 | password, 10 | database, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/queues.server/src/ioredis.ts: -------------------------------------------------------------------------------- 1 | import { remember } from '@epic-web/remember'; 2 | import { env } from '@metronome/env.server'; 3 | import IOredis from 'ioredis'; 4 | 5 | export const ioredis = remember('cache.ioredis', () => { 6 | const { url, password, family } = env.cache(); 7 | return new IOredis(url, { maxRetriesPerRequest: null, password, family }); 8 | }); 9 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebAnalyticsSection/components/index.ts: -------------------------------------------------------------------------------- 1 | export { BounceRate } from './BounceRate'; 2 | export { SessionMedianDuration } from './SessionMedianDuration'; 3 | export { TotalPageviews } from './TotalPageviews'; 4 | export { TotalSessions } from './TotalSessions'; 5 | export { VisitorsRightNow } from './VisitorsRightNow'; 6 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-vitals/components/WebVitalsByRouteSection/data/tasks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "TASK-8782", 4 | "title": "You can't compress the program without quantifying the open-source SSD pixel!", 5 | "status": "in progress", 6 | "label": "documentation", 7 | "priority": "medium", 8 | "LCP": 0.5 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0022_bent_living_mummy.sql: -------------------------------------------------------------------------------- 1 | --> statement-breakpoint 2 | DROP MATERIALIZED VIEW IF EXISTS "usage_monthly"; 3 | --> statement-breakpoint 4 | DROP MATERIALIZED VIEW IF EXISTS "usage_weekly"; 5 | --> statement-breakpoint 6 | DROP MATERIALIZED VIEW IF EXISTS "usage_daily"; 7 | --> statement-breakpoint 8 | DROP MATERIALIZED VIEW IF EXISTS "usage_hourly"; 9 | -------------------------------------------------------------------------------- /packages/db.server/src/utils/prettyPrintZodError.ts: -------------------------------------------------------------------------------- 1 | import { ZodError } from 'zod'; 2 | 3 | export function prettyPrintZodError(error: ZodError): void { 4 | const errorMessages = error.errors 5 | .map((err, index) => `${index + 1}. Field: ${err.path.join('.')} - ${err.message}`) 6 | .join('\n'); 7 | 8 | console.log('Validation errors:\n' + errorMessages); 9 | } 10 | -------------------------------------------------------------------------------- /packages/db.server/src/utils/timeZones.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from '@js-temporal/polyfill'; 2 | 3 | export function getTimeZoneOffset(timeZoneId: string): string { 4 | const now = Temporal.Now.zonedDateTimeISO(timeZoneId).toInstant(); 5 | 6 | const offset = 7 | Temporal.TimeZone.from(timeZoneId).getOffsetStringFor?.(now) ?? '+00:00'; 8 | 9 | return offset; 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100, 9 | "safari": 15, 10 | "firefox": 91 11 | } 12 | } 13 | ], 14 | "@babel/preset-typescript", 15 | "@babel/preset-react" 16 | ], 17 | "plugins": [] 18 | } -------------------------------------------------------------------------------- /apps/web/app/routes/authentication/authentication.logout.route.tsx: -------------------------------------------------------------------------------- 1 | import { ActionFunctionArgs } from '@remix-run/node'; 2 | 3 | import { handle } from '#app/handlers'; 4 | 5 | export async function action({ request }: ActionFunctionArgs) { 6 | const { auth } = await handle(request); 7 | 8 | await auth.logout({ 9 | redirectTo: '/authentication/grant', 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /packages/db.server/src/models/sourcemaps/sourcemaps.types.ts: -------------------------------------------------------------------------------- 1 | import { CamelCasedProperties } from 'type-fest'; 2 | 3 | export interface ClickHouseSourcemap { 4 | project_id: string; 5 | version: string; 6 | path: string; 7 | deleted: number; 8 | created_at: Date; 9 | updated_at: Date; 10 | } 11 | 12 | export type Sourcemap = CamelCasedProperties; 13 | -------------------------------------------------------------------------------- /packages/env.server/scripts/esbuild.mjs: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild'; 2 | 3 | export const esbuildConfig = { 4 | entryPoints: ['src/index.ts'], 5 | bundle: true, 6 | sourcemap: true, 7 | packages: 'external', 8 | plugins: [], 9 | platform: 'node', 10 | format: 'cjs', 11 | outdir: 'dist/cjs', 12 | logLevel: 'info', 13 | }; 14 | 15 | await build(esbuildConfig); 16 | -------------------------------------------------------------------------------- /apps/web/app/components/Tabs/index.ts: -------------------------------------------------------------------------------- 1 | import { Tabs as Component, TabsContent, TabsList, TabsTrigger } from './Tabs'; 2 | 3 | export const Tabs: typeof Component & { 4 | Content: typeof TabsContent; 5 | List: typeof TabsList; 6 | Trigger: typeof TabsTrigger; 7 | } = Object.assign(Component, { 8 | Content: TabsContent, 9 | List: TabsList, 10 | Trigger: TabsTrigger, 11 | }); 12 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Check.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Check: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/components/Section/Section.Title.tsx: -------------------------------------------------------------------------------- 1 | import { type FunctionComponent } from 'react'; 2 | 3 | export const SectionTitle: FunctionComponent<{ title: string }> = ({ 4 | title, 5 | }) => { 6 | return ( 7 |
8 |

{title}

9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-vitals/hooks/useWebVitalsEventData.ts: -------------------------------------------------------------------------------- 1 | import { useEventRouteData } from '#app/hooks'; 2 | 3 | import { type loader } from '../$teamSlug.$projectSlug.web-vitals.route'; 4 | 5 | const routeId = '$teamSlug.$projectSlug.web-vitals'; 6 | 7 | export function useWebVitalsEventData() { 8 | return useEventRouteData(routeId); 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/ChevronRight.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const ChevronRight: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/LoaderTwo.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const LoaderTwo: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/web/app/components/Card/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Card as CardPrimitive, 3 | CardContent, 4 | CardDescription, 5 | CardFooter, 6 | CardHeader, 7 | CardTitle, 8 | } from './Card'; 9 | 10 | export const Card = Object.assign(CardPrimitive, { 11 | Content: CardContent, 12 | Description: CardDescription, 13 | Footer: CardFooter, 14 | Header: CardHeader, 15 | Title: CardTitle, 16 | }); 17 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Menu.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Menu: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/X.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const X: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/List/ListItem.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, PropsWithChildren } from 'react'; 2 | 3 | export type ListItemProps = PropsWithChildren; 4 | 5 | export const ListItem: FunctionComponent = ({ children }) => { 6 | return ( 7 |
  • 8 | {children} 9 |
  • 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0025_spicy_sentinel.sql: -------------------------------------------------------------------------------- 1 | TRUNCATE TABLE requests; 2 | -- Custom SQL migration file, put you code below! -- 3 | SELECT create_hypertable('requests', 'timestamp', chunk_time_interval => interval '1 day'); 4 | --> statement-breakpoint 5 | ALTER TABLE requests SET (timescaledb.compress); 6 | --> statement-breakpoint 7 | SELECT add_compression_policy('requests', INTERVAL '7 days'); 8 | -------------------------------------------------------------------------------- /apps/web/.storybook/mocks/index.ts: -------------------------------------------------------------------------------- 1 | export { createRemixStub } from './createRemixStub'; 2 | 3 | export function wait(minTime: number, maxTime: number, value: any): Promise { 4 | const delay = Math.random() * (maxTime - minTime) + minTime; 5 | return new Promise((resolve) => { 6 | setTimeout(() => { 7 | resolve(value); 8 | }, delay * 1000); // Convert delay to milliseconds 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Table/Table.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, PropsWithChildren } from 'react'; 2 | 3 | export const Table: FunctionComponent = ({ children }) => { 4 | return ( 5 |
    6 | {children}
    7 |
    8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/hooks/useWebAnalyticsEventData/useWebAnalyticsEventData.ts: -------------------------------------------------------------------------------- 1 | import { useEventRouteData } from '#app/hooks'; 2 | 3 | import { type loader } from '../../$teamSlug.$projectSlug.web-analytics.route'; 4 | 5 | export function useWebAnalyticsEventData() { 6 | return useEventRouteData( 7 | '$teamSlug.$projectSlug.web-analytics', 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.configPath": "./.prettierrc", 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.eslint": true 5 | } 6 | // "typescript.inlayHints.parameterNames.enabled": "all", 7 | // "typescript.inlayHints.variableTypes.suppressWhenTypeMatchesName": false, 8 | // "typescript.inlayHints.parameterTypes.enabled": true 9 | // "typescript.inlayHints.variableTypes.enabled": true 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/app/components/HoverCard/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HoverCard as HoverCardComponent, 3 | HoverCardContent, 4 | HoverCardTrigger, 5 | } from './HoverCard'; 6 | 7 | export const HoverCard: typeof HoverCardComponent & { 8 | Content: typeof HoverCardContent; 9 | Trigger: typeof HoverCardTrigger; 10 | } = Object.assign(HoverCardComponent, { 11 | Content: HoverCardContent, 12 | Trigger: HoverCardTrigger, 13 | }); 14 | -------------------------------------------------------------------------------- /apps/web/app/events/hooks/useEventContext/useEventContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { invariant } from 'ts-invariant'; 3 | 4 | import { EventContext } from '#app/events/EventProvider'; 5 | 6 | export function useEventContext() { 7 | const context = useContext(EventContext); 8 | 9 | invariant(context, 'useEventContext must be used within an EventProvider'); 10 | 11 | return context; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/app/filters/InvalidFilterValueError.ts: -------------------------------------------------------------------------------- 1 | export class InvalidFilterValueError extends Error { 2 | name: string = 'InvalidFilterValueError'; 3 | 4 | constructor(filterName: string, value: string | string[]) { 5 | super( 6 | `${InvalidFilterValueError.name} - Invalid value "${ 7 | Array.isArray(value) ? value.join(',') : value 8 | }" for filter "${filterName}"`, 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/tailwind.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "app/components", 14 | "utils": "app/component/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/app/filters/index.ts: -------------------------------------------------------------------------------- 1 | export { Filters } from './components/Filters'; 2 | export { filters } from './filters'; 3 | export type { FilterObject } from './filters.types'; 4 | export { getInitialFilterOption } from './helpers'; 5 | export { mergeFilterOptionsWithSearch, toMap } from './helpers'; 6 | export { useFilterActiveOption } from './hooks/useFilterActiveOption'; 7 | export { InvalidFilterValueError } from './InvalidFilterValueError'; 8 | -------------------------------------------------------------------------------- /apps/producer/scripts/esbuild.mjs: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild'; 2 | 3 | /** 4 | * @type {import('esbuild').BuildOptions} 5 | */ 6 | export const esbuildConfig = { 7 | entryPoints: ['src/index.ts'], 8 | bundle: true, 9 | sourcemap: true, 10 | packages: 'external', 11 | plugins: [], 12 | platform: 'node', 13 | format: 'cjs', 14 | outdir: 'dist/cjs', 15 | logLevel: 'info', 16 | }; 17 | 18 | await build(esbuildConfig); 19 | -------------------------------------------------------------------------------- /apps/web/app/components/Notifications/NotificationsOutlet.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | export interface NotificationsOutletProps { 4 | id?: string; 5 | } 6 | 7 | export const NotificationsOutlet: FunctionComponent = ({ 8 | id = 'default-notifications', 9 | }) => { 10 | return ( 11 |
    12 |
    13 |
    14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/app/filters/hooks/useFiltersContext/useFiltersContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { invariant } from 'ts-invariant'; 3 | 4 | import { filtersContext } from '#app/filters/filtersContext'; 5 | 6 | export function useFiltersContext() { 7 | const context = useContext(filtersContext); 8 | 9 | invariant(context, 'useFiltersContext must be used within a FiltersProvider'); 10 | 11 | return context; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/documents/partials/metronome-installation.partial.mdoc: -------------------------------------------------------------------------------- 1 | ## Install Metronome 2 | 3 | Install Metronome's react and {% $adapter %} prerelease packages. 4 | 5 | ```bash {% title="Terminal" %} 6 | npm install @metronome-sh/react @metronome-sh/{% $adapter %} 7 | ``` 8 | 9 | alternatively, you can use yarn: 10 | 11 | ```bash {% title="Terminal" %} 12 | yarn add @metronome-sh/react @metronome-sh/{% $adapter %} 13 | ``` 14 | -------------------------------------------------------------------------------- /apps/workers/scripts/esbuild.mjs: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild'; 2 | 3 | /** 4 | * @type {import('esbuild').BuildOptions} 5 | */ 6 | export const esbuildConfig = { 7 | entryPoints: ['src/index.ts'], 8 | bundle: true, 9 | sourcemap: true, 10 | packages: 'external', 11 | plugins: [], 12 | platform: 'node', 13 | format: 'cjs', 14 | outdir: 'dist/cjs', 15 | logLevel: 'info', 16 | }; 17 | 18 | await build(esbuildConfig); 19 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0032_cool_vampiro.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "pageviews" ADD COLUMN "country_code" text DEFAULT 'unknown' NOT NULL;--> statement-breakpoint 2 | ALTER TABLE "pageviews" ADD COLUMN "country" text DEFAULT 'unknown' NOT NULL;--> statement-breakpoint 3 | ALTER TABLE "pageviews" ADD COLUMN "region" text DEFAULT 'unknown' NOT NULL;--> statement-breakpoint 4 | ALTER TABLE "pageviews" ADD COLUMN "city" text DEFAULT 'unknown' NOT NULL; -------------------------------------------------------------------------------- /apps/web/app/hooks/useEventData.ts: -------------------------------------------------------------------------------- 1 | import { useMatches } from '@remix-run/react'; 2 | import { invariant } from 'ts-invariant'; 3 | 4 | import { useEventRouteData } from './useEventRouteData'; 5 | 6 | export function useEventData() { 7 | const matches = useMatches(); 8 | const route = matches.at(-1); 9 | 10 | invariant(route, `route is undefined.`); 11 | 12 | const data = useEventRouteData(route.id); 13 | 14 | return data; 15 | } 16 | -------------------------------------------------------------------------------- /apps/workers/src/aggregations.ts: -------------------------------------------------------------------------------- 1 | import { refreshAggregation } from '@metronome/db.server'; 2 | import { queues } from '@metronome/queues.server'; 3 | 4 | export async function aggregations(job: typeof queues.aggregations.$inferJob) { 5 | const { data } = job; 6 | const { aggregation, watermark } = data; 7 | 8 | await refreshAggregation({ aggregation, watermark }); 9 | 10 | return `aggregation for ${aggregation} completed`; 11 | } 12 | -------------------------------------------------------------------------------- /packages/db.server/scripts/esbuild.mjs: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild'; 2 | 3 | /** 4 | * @type {import('esbuild').BuildOptions} 5 | */ 6 | export const esbuildConfig = { 7 | entryPoints: ['src/index.ts'], 8 | bundle: true, 9 | sourcemap: true, 10 | packages: 'external', 11 | plugins: [], 12 | platform: 'node', 13 | format: 'cjs', 14 | outdir: 'dist/cjs', 15 | logLevel: 'info', 16 | }; 17 | 18 | await build(esbuildConfig); 19 | -------------------------------------------------------------------------------- /apps/cron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "outDir": "dist", 5 | "strict": true, 6 | "lib": ["esnext"], 7 | "esModuleInterop": true, 8 | "module": "ESNext", 9 | "moduleResolution": "Node", 10 | "target": "es2017", 11 | "allowImportingTsExtensions": true, 12 | "noEmit": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "**/*.spec.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/cache.server/scripts/esbuild.mjs: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild'; 2 | 3 | /** 4 | * @type {import('esbuild').BuildOptions} 5 | */ 6 | export const esbuildConfig = { 7 | entryPoints: ['src/index.ts'], 8 | bundle: true, 9 | sourcemap: true, 10 | packages: 'external', 11 | plugins: [], 12 | platform: 'node', 13 | format: 'cjs', 14 | outdir: 'dist/cjs', 15 | logLevel: 'info', 16 | }; 17 | 18 | await build(esbuildConfig); 19 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0005_zippy_shaman.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "projects" RENAME TO "apps";--> statement-breakpoint 2 | ALTER TABLE "apps" DROP CONSTRAINT "projects_team_id_teams_id_fk"; 3 | --> statement-breakpoint 4 | DO $$ BEGIN 5 | ALTER TABLE "apps" ADD CONSTRAINT "apps_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "teams"("id") ON DELETE no action ON UPDATE no action; 6 | EXCEPTION 7 | WHEN duplicate_object THEN null; 8 | END $$; 9 | -------------------------------------------------------------------------------- /packages/queues.server/scripts/esbuild.mjs: -------------------------------------------------------------------------------- 1 | import { build } from 'esbuild'; 2 | 3 | /** 4 | * @type {import('esbuild').BuildOptions} 5 | */ 6 | export const esbuildConfig = { 7 | entryPoints: ['src/index.ts'], 8 | bundle: true, 9 | sourcemap: true, 10 | packages: 'external', 11 | plugins: [], 12 | platform: 'node', 13 | format: 'cjs', 14 | outdir: 'dist/cjs', 15 | logLevel: 'info', 16 | }; 17 | 18 | await build(esbuildConfig); 19 | -------------------------------------------------------------------------------- /apps/web/app/filters/filters/date-range/date-range.types.ts: -------------------------------------------------------------------------------- 1 | import { type Temporal } from '@js-temporal/polyfill'; 2 | 3 | export interface DateRangeParsed { 4 | from: Temporal.ZonedDateTime; 5 | to: Temporal.ZonedDateTime; 6 | } 7 | 8 | export type DateRangeOptionIds = 9 | | 'today' 10 | | 'yesterday' 11 | | 'this-month' 12 | | 'this-week' 13 | | 'this-year' 14 | | 'last-seven-days' 15 | | 'last-thirty-days' 16 | | 'last-month'; 17 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Search.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Search: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/components/Select/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Select as SelectComponent, 3 | SelectContent, 4 | SelectGroup, 5 | SelectItem, 6 | SelectLabel, 7 | SelectTrigger, 8 | SelectValue, 9 | } from './Select'; 10 | 11 | export const Select = Object.assign(SelectComponent, { 12 | Content: SelectContent, 13 | Group: SelectGroup, 14 | Item: SelectItem, 15 | Label: SelectLabel, 16 | Trigger: SelectTrigger, 17 | Value: SelectValue, 18 | }); 19 | -------------------------------------------------------------------------------- /apps/web/app/components/Breadcrumb/BreadcrumbOutlet.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | export interface BreadcrumbOutletProps { 4 | id?: string; 5 | } 6 | 7 | export const BreadcrumbOutlet: FunctionComponent = ({ 8 | id = 'default-breadcrumb', 9 | }) => { 10 | return ( 11 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/documents/reference/requests.mdoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Requests" 3 | description: "Requests reference" 4 | --- 5 | 6 | # Requests 7 | 8 | Metronome tracks the requests that are made to the Remix app. 9 | 10 | {% image src="/images/reference-requests-dark.png" className="w-full mt-4 mx-auto rounded-lg hidden dark:block" /%} 11 | {% image src="/images/reference-requests-light.png" className="w-full mt-4 mx-auto rounded-lg block dark:hidden" /%} 12 | -------------------------------------------------------------------------------- /packages/db.server/src/modules/clickhouse.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '@clickhouse/client'; 2 | import { remember } from '@epic-web/remember'; 3 | import { env } from '@metronome/env.server'; 4 | 5 | const { database, host, password, port, username } = env.clickhouse(); 6 | 7 | export const clickhouse = remember('clickhouse', () => 8 | createClient({ 9 | host: `${host}:${port}`, 10 | username, 11 | password, 12 | database, 13 | }), 14 | ); 15 | -------------------------------------------------------------------------------- /scripts/setup.mjs: -------------------------------------------------------------------------------- 1 | import { Chalk } from 'chalk'; 2 | import boxen from 'boxen'; 3 | 4 | const chalk = new Chalk({ level: 3 }); 5 | 6 | const fly = chalk.magenta('Fly.io'); 7 | 8 | // Boxed Text 9 | const text = ` 10 | ✅ Created ${fly} web deployment files 11 | ✅ Created ${fly} redis deployment files 12 | `; 13 | 14 | console.log( 15 | boxen(text, { 16 | title: 'Deployment set up', 17 | padding: 0.5, 18 | borderColor: 'green', 19 | }), 20 | ); 21 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/constants.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | // When we change to vite this will be easier to deal with. 5 | // Read the package.json file and where imports.#app/* points to 6 | const packageJson = JSON.parse( 7 | fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'), 8 | ); 9 | 10 | export const DOCUMENTS_PATH = packageJson.imports['#app/*'].replace( 11 | '*', 12 | '/routes/docs/documents', 13 | ); 14 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Filter.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Filter: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/ArrowNarrowLeft.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const ArrowNarrowLeft: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/ArrowNarrowRight.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const ArrowNarrowRight: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/BrandSafari.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const BrandSafari: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Refresh.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Refresh: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/List/List.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, PropsWithChildren } from 'react'; 2 | 3 | export type ListProps = PropsWithChildren<{ 4 | ordered?: boolean; 5 | }>; 6 | 7 | export const List: FunctionComponent = ({ children, ordered }) => { 8 | return ordered ? ( 9 |
      {children}
    10 | ) : ( 11 |
      {children}
    12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0019_massive_thunderbird.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "usage" ( 2 | "team_id" text NOT NULL, 3 | "project_id" text NOT NULL, 4 | "timestamp" timestamp with time zone NOT NULL, 5 | "events" bigint NOT NULL 6 | ); 7 | --> statement-breakpoint 8 | CREATE INDEX IF NOT EXISTS "usage_team_timestamp_idx" ON "usage" ("team_id","timestamp");--> statement-breakpoint 9 | CREATE INDEX IF NOT EXISTS "usage_project_timestamp_idx" ON "usage" ("project_id","timestamp"); -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0040_stiff_captain_britain.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "teams" ALTER COLUMN "settings" SET DEFAULT '{"subscription":null}'::jsonb;--> statement-breakpoint 2 | ALTER TABLE "users" ALTER COLUMN "settings" SET DEFAULT '{"emails":[],"selectedEmail":null,"lastSelectedProjectSlug":null,"lastSelectedTeamSlug":null,"customerId":null}'::jsonb;--> statement-breakpoint 3 | ALTER TABLE "users_to_teams" ADD CONSTRAINT "users_to_teams_user_id_team_id" PRIMARY KEY("user_id","team_id"); -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-vitals/components/WebVitalsByRouteSection/data/schema.tsx: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | // We're keeping a simple non-relational schema here. 4 | // IRL, you will have a schema for your data models. 5 | export const taskSchema = z.object({ 6 | id: z.string(), 7 | title: z.string(), 8 | status: z.string(), 9 | label: z.string(), 10 | priority: z.string(), 11 | }); 12 | 13 | export type Task = z.infer; 14 | -------------------------------------------------------------------------------- /packages/db.server/src/models/spans/spans.types.ts: -------------------------------------------------------------------------------- 1 | import { type SpanSchema } from './spans'; 2 | import { z } from 'zod'; 3 | 4 | export interface ClickHouseSpan { 5 | project_id: string; 6 | trace_id: string; 7 | span_id: string; 8 | parent_span_id: string | null; 9 | name: string; 10 | start_time: number; 11 | end_time: number; 12 | 'span_attributes.key': string[]; 13 | 'span_attributes.value': string[]; 14 | } 15 | 16 | export type Span = z.infer; 17 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/BrandOpera.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const BrandOpera: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Table/Th.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, PropsWithChildren } from 'react'; 2 | 3 | export type ThProps = PropsWithChildren<{ 4 | width?: number; 5 | }>; 6 | 7 | export const Th: FunctionComponent = ({ children }) => { 8 | return ( 9 | 10 | 11 | {children} 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Sidebar/SidebarContainer.tsx: -------------------------------------------------------------------------------- 1 | import { type FunctionComponent } from 'react'; 2 | 3 | import { Sidebar } from '.'; 4 | 5 | export const SidebarContainer: FunctionComponent = () => { 6 | return ( 7 |
    8 |
    9 | 10 |
    11 |
    12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useRootLoaderData/useRootLoaderData.ts: -------------------------------------------------------------------------------- 1 | import { SerializeFrom } from '@remix-run/node'; 2 | import { useRouteLoaderData } from '@remix-run/react'; 3 | import { invariant } from 'ts-invariant'; 4 | 5 | import { type loader } from '../../root'; 6 | 7 | export function useRootLoaderData(): SerializeFrom { 8 | const data = useRouteLoaderData('root'); 9 | invariant(data, `Route loader data for route root is undefined.`); 10 | return data; 11 | } 12 | -------------------------------------------------------------------------------- /packages/db.server/src/models/events/events.types.ts: -------------------------------------------------------------------------------- 1 | import { CamelCasedProperties, SetFieldType } from 'type-fest'; 2 | import { type EventSchema } from './events'; 3 | import { z } from 'zod'; 4 | 5 | export interface ClickHouseEvent { 6 | project_id: string; 7 | trace_id: string; 8 | span_id: string; 9 | timestamp: number; 10 | name: string; 11 | 'event_attributes.key': string[]; 12 | 'event_attributes.value': string[]; 13 | } 14 | 15 | export type Event = z.infer; 16 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Eye.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Eye: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/RouteSquareTwo.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const RouteSquareTwo: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/app/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { checkForProjectClientUpdates } from './checkForProjectClientUpdates'; 2 | export { countryFlag } from './countryFlag'; 3 | export { formatDuration } from './formatDuration'; 4 | export { formatNumber } from './formatNumber'; 5 | export { formatTime } from './formatTime'; 6 | export { inRoute } from './inRoute'; 7 | export { isRangeWithinToday } from './isRangeWithinToday'; 8 | export { namedAction } from './namedAction'; 9 | export { getTimeZoneFromRequest } from './timeZone'; 10 | -------------------------------------------------------------------------------- /apps/web/app/components/Card/Card.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Card } from '.'; 3 | import { Label } from '../Label'; 4 | import { Button } from '../Button'; 5 | 6 | const meta: Meta = { 7 | title: 'Card', 8 | component: Card, 9 | parameters: { 10 | layout: 'centered', 11 | }, 12 | args: {}, 13 | }; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | export const Default: Story = { 19 | args: {}, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/db.server/src/legacy/legacySpan.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const LegacysPanSchema = z.object({ 4 | id: z.string(), 5 | name: z.string(), 6 | traceId: z.string(), 7 | parentSpanId: z.string().nullable(), 8 | attributes: z.any(), 9 | events: z.array(z.any()), 10 | timestamp: z.number(), 11 | startNano: z.string(), 12 | endNano: z.string(), 13 | durationNano: z.string(), 14 | status: z.string(), 15 | }); 16 | 17 | export type LegacySpan = z.infer; 18 | -------------------------------------------------------------------------------- /apps/cron/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | import { env } from '@metronome/env.server'; 5 | import Bree from 'bree'; 6 | 7 | const bree = new Bree({ 8 | root: path.join(path.dirname(fileURLToPath(import.meta.url)), 'jobs'), 9 | defaultExtension: 'ts', 10 | jobs: [ 11 | { 12 | name: 'rotate-salt', 13 | interval: env.when({ production: 'at 12:00 am', development: '5m' }), 14 | }, 15 | ], 16 | }); 17 | 18 | await bree.start(); 19 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/hooks/useDocsLoaderData/useDocsLoaderData.ts: -------------------------------------------------------------------------------- 1 | import { SerializeFrom } from '@remix-run/node'; 2 | import { useRouteLoaderData } from '@remix-run/react'; 3 | import { invariant } from 'ts-invariant'; 4 | 5 | import { type loader } from '../../docs.$.route'; 6 | 7 | export function useDocsLoaderData(): SerializeFrom { 8 | const data = useRouteLoaderData('docs.$'); 9 | invariant(data, `Route loader data for route docs.$ is undefined.`); 10 | return data; 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Heartbeat.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Heartbeat: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Sun.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Sun: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Sidebar/SheetCloseContainer.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, PropsWithChildren } from 'react'; 2 | 3 | import { Sheet } from '#app/components'; 4 | 5 | import { useSidebarContext } from './provider'; 6 | 7 | export const SheetCloseContainer: FunctionComponent = ({ children }) => { 8 | const { inSheet } = useSidebarContext(); 9 | 10 | if (!inSheet) { 11 | return <>{children}; 12 | } 13 | 14 | return {children}; 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Sidebar/provider.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | import { invariant } from 'ts-invariant'; 3 | 4 | export const sidebarContext = createContext<{ inSheet: boolean }>({ inSheet: false }); 5 | 6 | export const SidebarProvider = sidebarContext.Provider; 7 | 8 | export const useSidebarContext = () => { 9 | const context = useContext(sidebarContext); 10 | invariant(context, 'useSidebarContext must be used within a SidebarProvider'); 11 | return context; 12 | }; 13 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/SquareRoundedPlus.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const SquareRoundedPlus: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/hooks/useTeamLoaderData/useTeamLoaderData.ts: -------------------------------------------------------------------------------- 1 | import { SerializeFrom } from '@remix-run/node'; 2 | import { useRouteLoaderData } from '@remix-run/react'; 3 | import { invariant } from 'ts-invariant'; 4 | 5 | import { type loader } from '../../$teamSlug.route'; 6 | 7 | export function useTeamLoaderData(): SerializeFrom { 8 | const data = useRouteLoaderData('$teamSlug'); 9 | invariant(data, `Route loader data for route $teamSlug is undefined.`); 10 | return data; 11 | } 12 | -------------------------------------------------------------------------------- /apps/workers/src/index.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@metronome/env.server'; 2 | import { queues } from '@metronome/queues.server'; 3 | 4 | import { aggregations } from './aggregations'; 5 | import { events } from './events'; 6 | import { metrics } from './metrics'; 7 | 8 | console.log('[workers]', 'Initializing queues'); 9 | 10 | queues.metrics.worker(metrics, env.when({ production: 10, development: 1 })); 11 | queues.events.worker(events, env.when({ production: 10, development: 1 })); 12 | queues.aggregations.worker(aggregations, 1); 13 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/InfoSquareRounded.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const InfoSquareRounded: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Paragraph/Paragraph.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { type FunctionComponent, type PropsWithChildren } from 'react'; 3 | 4 | export type ParagraphProps = PropsWithChildren<{ 5 | className?: string; 6 | }>; 7 | 8 | export const Paragraph: FunctionComponent = ({ 9 | children, 10 | className, 11 | }) => { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Alarm.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Alarm: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/CircleFilled.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const CircleFilled: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Home.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Home: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | {/* */} 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/SquareFilled.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const SquareFilled: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/ReferrersSection/ReferrersSection.tsx: -------------------------------------------------------------------------------- 1 | import { type FunctionComponent } from 'react'; 2 | 3 | import { Section } from '../../../../components'; 4 | import { ReferrersTable } from './ReferrersTable'; 5 | 6 | export const ReferrersSection: FunctionComponent = () => { 7 | return ( 8 |
    9 | 10 |
    11 | 12 |
    13 |
    14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/TerminalTwo.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const TerminalTwo: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/BuildingLighthouse.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const BuildingLighthouse: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/app/routes/shared/hooks/useSharedProjectLoaderData.ts: -------------------------------------------------------------------------------- 1 | import { SerializeFrom } from '@remix-run/node'; 2 | import { useRouteLoaderData } from '@remix-run/react'; 3 | import { invariant } from 'ts-invariant'; 4 | 5 | import { type loader } from '../shared.$projectId.route'; 6 | 7 | export function useSharedProjectLoaderData(): SerializeFrom { 8 | const data = useRouteLoaderData('shared.$projectId'); 9 | invariant(data, `Route loader data for route shared.$projectId is undefined.`); 10 | return data; 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Button } from './Button'; 2 | export { Code } from './Code'; 3 | export { Fence } from './Fence'; 4 | export { Heading } from './Heading'; 5 | export { Image } from './Image'; 6 | export { InstallationTarget, InstallationTargets } from './InstallationTargets'; 7 | export { Link } from './Link'; 8 | export { List, ListItem } from './List'; 9 | export { Paragraph } from './Paragraph'; 10 | export { Strong } from './Strong'; 11 | export { Table, TBody, Td, Th, THead, Tr } from './Table'; 12 | -------------------------------------------------------------------------------- /apps/web/app/utils/inRoute.ts: -------------------------------------------------------------------------------- 1 | import { match } from 'path-to-regexp'; 2 | 3 | export function inRoute( 4 | request: Request, 5 | routePatterns: string | string[], 6 | ): boolean { 7 | const url = new URL(request.url); 8 | const currentPath = url.pathname; 9 | const patterns = Array.isArray(routePatterns) 10 | ? routePatterns 11 | : [routePatterns]; 12 | 13 | return patterns.some((pattern) => { 14 | const routeMatch = match(pattern, { decode: decodeURIComponent }); 15 | return routeMatch(currentPath) !== false; 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/AlertSquareRoundedOutline.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const AlertSquareRoundedOutline: FunctionComponent = ( 6 | props, 7 | ) => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/app/routes/authentication/authentication.success.route.tsx: -------------------------------------------------------------------------------- 1 | import { type LoaderFunctionArgs, redirect } from '@remix-run/node'; 2 | 3 | import { handle } from '#app/handlers'; 4 | 5 | export async function loader({ request }: LoaderFunctionArgs) { 6 | const { auth } = await handle(request); 7 | 8 | const user = await auth.user(); 9 | 10 | const team = user.usersToTeams.at(0); 11 | 12 | if (!team) { 13 | throw new Error('Unable to find the team associated with the user'); 14 | } 15 | 16 | return redirect(`/${team.team.slug}`); 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Code/Code.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { FunctionComponent } from 'react'; 3 | 4 | export type CodeProps = { 5 | content: string; 6 | className?: string; 7 | }; 8 | 9 | export const Code: FunctionComponent = ({ content, className }) => { 10 | return ( 11 |
    12 |
    13 |         {content}
    14 |       
    15 |
    16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/web/.storybook/mocks/modules/@remix-run/react/Link.tsx: -------------------------------------------------------------------------------- 1 | import { action } from '@storybook/addon-actions'; 2 | import { FunctionComponent } from 'react'; 3 | import { 4 | Link as RemixLink, 5 | LinkProps, 6 | } from '../../../../../node_modules/@remix-run/react'; 7 | import React from 'react'; 8 | 9 | export const Link: FunctionComponent = ({ to, ...props }) => { 10 | return ( 11 | { 15 | action('navigate')(to); 16 | }} 17 | /> 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Clipboard.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Clipboard: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Table/Td.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { FunctionComponent, PropsWithChildren } from 'react'; 3 | 4 | export type TdProps = PropsWithChildren<{ 5 | className?: string; 6 | }>; 7 | 8 | export const Td: FunctionComponent = ({ className, children }) => { 9 | const isString = typeof children === 'string'; 10 | 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/app/utils/formatTime.ts: -------------------------------------------------------------------------------- 1 | export function formatTime(milliseconds: number | null): string { 2 | if (milliseconds === null) { 3 | return '--:--'; 4 | } 5 | 6 | // Calculate minutes and seconds 7 | const minutes = Math.floor(milliseconds / 60000); 8 | const seconds = Math.floor((milliseconds % 60000) / 1000); 9 | 10 | // Convert to mm:ss format 11 | const formattedMinutes = String(minutes).padStart(2, '0'); 12 | const formattedSeconds = String(seconds).padStart(2, '0'); 13 | 14 | return `${formattedMinutes}:${formattedSeconds}`; 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/components/DangerZoneForm/DangerZoneForm.tsx: -------------------------------------------------------------------------------- 1 | import { type FunctionComponent } from 'react'; 2 | 3 | import { Form } from '#app/components'; 4 | 5 | import { DeleteProject } from './components/DeleteProject'; 6 | 7 | export const DangerZoneForm: FunctionComponent = () => { 8 | return ( 9 |
    10 | 14 | 15 |
    16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/DeviceDesktop.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const DeviceDesktop: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Quote.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Quote: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/db.server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "declarationMap": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "resolveJsonModule": true, 15 | "baseUrl": "." 16 | }, 17 | "include": ["src/**/*.ts"], 18 | "exclude": ["node_modules", "dist", "cli"] 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/InstallationTargets/InstallationTargets.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { type FunctionComponent, type PropsWithChildren } from 'react'; 3 | 4 | export type InstallationTargetProps = PropsWithChildren<{ 5 | className?: string; 6 | }>; 7 | 8 | export const InstallationTargets: FunctionComponent< 9 | InstallationTargetProps 10 | > = ({ className, children }) => { 11 | return ( 12 |
      13 | {children} 14 |
    15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/app/routes/metrics/metrics.route.ts: -------------------------------------------------------------------------------- 1 | import { queues } from '@metronome/queues.server'; 2 | import { type ActionFunctionArgs } from '@remix-run/node'; 3 | 4 | export async function action({ request }: ActionFunctionArgs) { 5 | const apiKey = request.headers.get('ApiKey'); 6 | 7 | if (!apiKey) return new Response(null, { status: 202 }); 8 | 9 | const data = await request.json(); 10 | 11 | if (!data) return new Response(null, { status: 202 }); 12 | 13 | await queues.metrics.add({ apiKey, data }); 14 | 15 | return new Response(null, { status: 202 }); 16 | } 17 | -------------------------------------------------------------------------------- /packages/cache.server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "declarationMap": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "resolveJsonModule": true, 15 | "baseUrl": "." 16 | }, 17 | "include": ["src/**/*.ts"], 18 | "exclude": ["node_modules", "dist", "cli"] 19 | } 20 | -------------------------------------------------------------------------------- /apps/producer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "declarationMap": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "resolveJsonModule": true, 15 | "baseUrl": ".", 16 | "paths": {} 17 | }, 18 | "include": ["src/**/*.ts"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/components/Section/Section.tsx: -------------------------------------------------------------------------------- 1 | import { type FunctionComponent, type PropsWithChildren } from 'react'; 2 | 3 | import { cn } from '#app/components'; 4 | 5 | import { SectionTitle } from './Section.Title'; 6 | 7 | export type SectionProps = PropsWithChildren<{ 8 | className?: string; 9 | }>; 10 | 11 | export const Section: FunctionComponent & { 12 | Title: typeof SectionTitle; 13 | } = ({ className, children }) => { 14 | return
    {children}
    ; 15 | }; 16 | 17 | Section.Title = SectionTitle; 18 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/hooks/useTeamProjectLoaderData/useTeamProjectLoaderData.ts: -------------------------------------------------------------------------------- 1 | import { SerializeFrom } from '@remix-run/node'; 2 | import { useRouteLoaderData } from '@remix-run/react'; 3 | import { invariant } from 'ts-invariant'; 4 | 5 | import { loader } from '../../$teamSlug.$projectSlug.route'; 6 | 7 | export function useTeamProjectLoaderData(): SerializeFrom { 8 | const data = useRouteLoaderData('$teamSlug.$projectSlug'); 9 | invariant(data, `Route loader data for route $teamSlug is undefined.`); 10 | return data; 11 | } 12 | -------------------------------------------------------------------------------- /apps/workers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "declarationMap": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "resolveJsonModule": true, 15 | "baseUrl": ".", 16 | "paths": {} 17 | }, 18 | "include": ["src/**/*.ts"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0041_aberrant_meltdown.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "users_to_teams" DROP CONSTRAINT "users_to_teams_user_id_team_id";--> statement-breakpoint 2 | ALTER TABLE "users" ALTER COLUMN "settings" SET DEFAULT '{"emails":[],"seenNotifications":[],"selectedEmail":null,"lastSelectedProjectSlug":null,"lastSelectedTeamSlug":null,"customerId":null}'::jsonb;--> statement-breakpoint 3 | ALTER TABLE "users_to_teams" ADD CONSTRAINT "users_to_teams_user_id_team_id_pk" PRIMARY KEY("user_id","team_id");--> statement-breakpoint 4 | ALTER TABLE "projects" ADD COLUMN "version" text DEFAULT '0.0.0'; -------------------------------------------------------------------------------- /apps/web/app/utils/isRangeWithinToday.ts: -------------------------------------------------------------------------------- 1 | import { Temporal } from '@js-temporal/polyfill'; 2 | 3 | export function isRangeWithinToday(range: { 4 | from: Temporal.ZonedDateTime; 5 | to: Temporal.ZonedDateTime; 6 | }): boolean { 7 | const from = range.from.withPlainTime('00:00:00'); 8 | const to = range.to.withPlainTime('00:00:00'); 9 | const now = Temporal.Now.zonedDateTimeISO( 10 | range.from.timeZoneId, 11 | ).withPlainTime('00:00:00'); 12 | 13 | return ( 14 | from.epochSeconds === to.epochSeconds && 15 | from.epochSeconds === now.epochSeconds 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/env.server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "declarationMap": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "resolveJsonModule": true, 15 | "baseUrl": ".", 16 | "paths": {} 17 | }, 18 | "include": ["src/**/*.ts"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/BrandWindows.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const BrandWindows: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/World.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const World: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/queues.server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "outDir": "./dist", 9 | "rootDir": "./src", 10 | "declaration": true, 11 | "declarationMap": true, 12 | "strict": true, 13 | "skipLibCheck": true, 14 | "resolveJsonModule": true, 15 | "baseUrl": ".", 16 | "paths": {} 17 | }, 18 | "include": ["src/**/*.ts"], 19 | "exclude": ["node_modules", "dist"] 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Rocket.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Rocket: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/app/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useEventRouteData } from './useEventRouteData'; 2 | export { useIsNavigating } from './useIsNavigating'; 3 | export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect'; 4 | export { useMediaQuery } from './useMediaQuery'; 5 | export { usePrevious } from './usePrevious'; 6 | export { useRootLoaderData } from './useRootLoaderData'; 7 | export { useTimezoneId } from './useTimezoneId'; 8 | export { useTimeZoneSync } from './useTimeZoneSync'; 9 | export { useTinyKeys } from './useTinyKeys'; 10 | export { useWindowVisibility } from './useWindowVisibility'; 11 | -------------------------------------------------------------------------------- /apps/web/.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | import '../app/tailwind.css'; 3 | 4 | const preview: Preview = { 5 | globalTypes: { 6 | darkMode: { 7 | defaultValue: 'dark', 8 | }, 9 | className: { 10 | defaultValue: 'dark', 11 | }, 12 | }, 13 | parameters: { 14 | backgrounds: { disable: true }, 15 | actions: { argTypesRegex: '^on[A-Z].*' }, 16 | controls: { 17 | matchers: { 18 | color: /(background|color)$/i, 19 | date: /Date$/, 20 | }, 21 | }, 22 | }, 23 | }; 24 | 25 | export default preview; 26 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/EyeClosed.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const EyeClosed: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/ClipboardCheck.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const ClipboardCheck: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/FileText.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const FileText: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/public/images/favicon-alternate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/web/app/components/Brand/images/logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/TriangleInvertedFilled.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const TriangleInvertedFilled: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Image/Image.tsx: -------------------------------------------------------------------------------- 1 | import { type FunctionComponent } from 'react'; 2 | 3 | export type ImageProps = { 4 | src: string; 5 | alt: string; 6 | className?: string; 7 | byUrl?: string; 8 | byLabel?: string; 9 | fromUrl?: string; 10 | fromLabel?: string; 11 | }; 12 | 13 | export const Image: FunctionComponent = ({ 14 | src, 15 | alt, 16 | className, 17 | }) => { 18 | return ( 19 | {alt} 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0027_freezing_jean_grey.sql: -------------------------------------------------------------------------------- 1 | SELECT create_hypertable('actions', 'timestamp', chunk_time_interval => interval '1 day'); 2 | --> statement-breakpoint 3 | ALTER TABLE actions SET (timescaledb.compress); 4 | --> statement-breakpoint 5 | SELECT add_compression_policy('actions', INTERVAL '7 days'); 6 | --> statement-breakpoint 7 | SELECT create_hypertable('loaders', 'timestamp', chunk_time_interval => interval '1 day'); 8 | --> statement-breakpoint 9 | ALTER TABLE loaders SET (timescaledb.compress); 10 | --> statement-breakpoint 11 | SELECT add_compression_policy('loaders', INTERVAL '7 days'); 12 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/TriangleFilled.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const TriangleFilled: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/.storybook/mocks/modules/@remix-run/react/useFetcher.ts: -------------------------------------------------------------------------------- 1 | import { useFetcher as useFetcherPrimitive } from '../../../../../node_modules/@remix-run/react'; 2 | import { useContext } from 'react'; 3 | import { MockContext } from '../../../MockContext'; 4 | import { action } from '@storybook/addon-actions'; 5 | 6 | export function useFetcher() { 7 | const { fetcher = {} } = useContext(MockContext); 8 | const fetcherPrimitive = useFetcherPrimitive(); 9 | 10 | const submit = (data: any, options) => { 11 | action('submit')({ data, options }); 12 | }; 13 | 14 | return { ...fetcherPrimitive, ...fetcher, submit }; 15 | } 16 | -------------------------------------------------------------------------------- /apps/web/app/components/Ping/Ping.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { cn } from '../utils'; 4 | 5 | export const Ping: FunctionComponent<{ className?: string }> = ({ 6 | className, 7 | }) => { 8 | return ( 9 | 10 | 11 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-vitals/hooks/useWebVitalsLoaderData.ts: -------------------------------------------------------------------------------- 1 | import { SerializeFrom } from '@remix-run/node'; 2 | import { useRouteLoaderData } from '@remix-run/react'; 3 | import { invariant } from 'ts-invariant'; 4 | 5 | import { type loader } from '../$teamSlug.$projectSlug.web-vitals.route'; 6 | 7 | const routeId = '$teamSlug.$projectSlug.web-vitals'; 8 | 9 | export function useWebVitalsLoaderData(): SerializeFrom { 10 | const data = useRouteLoaderData(routeId); 11 | invariant(data, `Route loader data for route ${routeId} is undefined.`); 12 | return data; 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/BrandChrome.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const BrandChrome: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/MoodSadDizzy.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const MoodSadDizzy: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/hooks/useOverviewLoaderData/useOverviewLoaderData.ts: -------------------------------------------------------------------------------- 1 | import { SerializeFrom } from '@remix-run/node'; 2 | import { useRouteLoaderData } from '@remix-run/react'; 3 | import { invariant } from 'ts-invariant'; 4 | 5 | import { type loader } from '../../$teamSlug.$projectSlug.overview.route'; 6 | 7 | export function useOverviewLoaderData(): SerializeFrom { 8 | const data = useRouteLoaderData('$teamSlug.$projectSlug.overview'); 9 | invariant(data, `Route loader data for route $teamSlug.$projectSlug.overview is undefined.`); 10 | return data; 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/RotateTwo.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const RotateTwo: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/web/app/components/Table/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table as TableComponent, 3 | TableBody, 4 | TableCaption, 5 | TableCell, 6 | TableHead, 7 | TableHeader, 8 | TableRow, 9 | } from './Table'; 10 | 11 | export const Table: typeof TableComponent & { 12 | Body: typeof TableBody; 13 | Caption: typeof TableCaption; 14 | Cell: typeof TableCell; 15 | Head: typeof TableHead; 16 | Header: typeof TableHeader; 17 | Row: typeof TableRow; 18 | } = Object.assign(TableComponent, { 19 | Body: TableBody, 20 | Caption: TableCaption, 21 | Cell: TableCell, 22 | Head: TableHead, 23 | Header: TableHeader, 24 | Row: TableRow, 25 | }); 26 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Calendar.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Calendar: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | {/* */} 14 | {/* */} 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/UserScan.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const UserScan: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/web/app/hooks/usePrevious/usePrevious.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | export const usePrevious = (value: T, initialize = false): T => { 4 | // The ref object is a generic container whose current property is mutable ... 5 | // ... and can hold any value, similar to an instance property on a class 6 | const ref: any = useRef(initialize ? value : undefined); 7 | // Store current value in ref 8 | useEffect(() => { 9 | ref.current = value; 10 | }, [value]); // Only re-run if value changes 11 | // Return previous value (happens before update in useEffect above) 12 | return ref.current; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0033_little_wrecker.sql: -------------------------------------------------------------------------------- 1 | --> statement-breakpoint 2 | SELECT create_hypertable('sessions', 'timestamp', chunk_time_interval => interval '1 day'); 3 | --> statement-breakpoint 4 | ALTER TABLE sessions SET (timescaledb.compress); 5 | --> statement-breakpoint 6 | SELECT add_compression_policy('sessions', INTERVAL '7 days'); 7 | --> statement-breakpoint 8 | SELECT create_hypertable('pageviews', 'timestamp', chunk_time_interval => interval '1 day'); 9 | --> statement-breakpoint 10 | ALTER TABLE pageviews SET (timescaledb.compress); 11 | --> statement-breakpoint 12 | SELECT add_compression_policy('pageviews', INTERVAL '7 days'); -------------------------------------------------------------------------------- /apps/web/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle hydrating your app on the client for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.client 5 | */ 6 | 7 | import { RemixBrowser } from '@remix-run/react'; 8 | import { startTransition, StrictMode } from 'react'; 9 | import { hydrateRoot } from 'react-dom/client'; 10 | 11 | startTransition(() => { 12 | hydrateRoot( 13 | document, 14 | 15 | 16 | , 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/web/app/utils/formatNumber.ts: -------------------------------------------------------------------------------- 1 | export function formatNumber( 2 | value: number | undefined | null, 3 | defaultValue?: string, 4 | ): string { 5 | if (value === undefined || value === null) { 6 | return defaultValue ?? '-'; 7 | } 8 | 9 | if (value < 1000) { 10 | return value.toString(); 11 | } else if (value >= 1000 && value < 1_000_000) { 12 | return (Math.floor(value / 10) / 100).toFixed(2) + 'k'; 13 | } else if (value >= 1_000_000 && value < 1_000_000_000) { 14 | return (Math.floor(value / 10_000) / 100).toFixed(2) + 'M'; 15 | } else { 16 | return (Math.floor(value / 10_000_000) / 100).toFixed(2) + 'B'; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Github.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Github: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/web/app/components/RouteDisplay/RouteDisplay.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { RouteDisplay } from '.'; 3 | 4 | const meta = { 5 | title: 'Basic/RouteDisplay', 6 | component: RouteDisplay, 7 | parameters: { 8 | layout: 'centered', 9 | }, 10 | argTypes: {}, 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = { 18 | args: { 19 | route: '/reports/:reportId/detail', 20 | }, 21 | }; 22 | 23 | export const WithSplat: Story = { 24 | args: { 25 | route: '/docs/*', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Sidebar/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { type FunctionComponent } from 'react'; 2 | 3 | import { useDocsLoaderData } from '../../hooks/useDocsLoaderData'; 4 | import { SidebarSection } from './SidebarSection'; 5 | import { SidebarProvider } from './provider'; 6 | 7 | export const Sidebar: FunctionComponent<{ inSheet?: boolean }> = ({ inSheet = false }) => { 8 | const { sections } = useDocsLoaderData(); 9 | 10 | return ( 11 | 12 | {sections.map((section) => ( 13 | 14 | ))} 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/hooks/useWebAnalyticsLoaderData/useWebAnalyticsLoaderData.ts: -------------------------------------------------------------------------------- 1 | import { SerializeFrom } from '@remix-run/node'; 2 | import { useRouteLoaderData } from '@remix-run/react'; 3 | import { invariant } from 'ts-invariant'; 4 | 5 | import { type loader } from '../../$teamSlug.$projectSlug.web-analytics.route'; 6 | 7 | export function useWebAnalyticsLoaderData(): SerializeFrom { 8 | const data = useRouteLoaderData('$teamSlug.$projectSlug.web-analytics'); 9 | invariant(data, `Route loader data for route $teamSlug.$projectSlug.web-analytics is undefined.`); 10 | return data; 11 | } 12 | -------------------------------------------------------------------------------- /apps/workers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@metronome/workers", 3 | "version": "4.0.0", 4 | "author": "Erick Tamayo", 5 | "license": "FSL-1.0-MIT", 6 | "main": "./dist/cjs/index.js", 7 | "types": "dist/index.d.ts", 8 | "scripts": { 9 | "dev": "tsx --watch src/index.ts", 10 | "start": "tsx src/index.ts" 11 | }, 12 | "keywords": [], 13 | "devDependencies": { 14 | "@types/node": "^20.8.9", 15 | "typescript": "^5.2.2" 16 | }, 17 | "dependencies": { 18 | "@metronome/db.server": "workspace:*", 19 | "@metronome/env.server": "workspace:*", 20 | "@metronome/queues.server": "workspace:*", 21 | "tsx": "^3.14.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/BrandAndroid.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const BrandAndroid: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/web/app/components/Spinner/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { type FunctionComponent } from 'react'; 2 | 3 | import { Icon } from '..'; 4 | import { cn } from '../utils'; 5 | 6 | export type SpinnerProps = { 7 | className?: string; 8 | containerClassName?: string; 9 | }; 10 | 11 | export const Spinner: FunctionComponent = ({ 12 | className, 13 | containerClassName, 14 | }) => { 15 | return ( 16 |
    17 | 23 |
    24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /apps/web/app/filters/filters/interval/interval.server.ts: -------------------------------------------------------------------------------- 1 | import { type ServerFilterProps } from '#app/filters/filters.types'; 2 | 3 | import { type IntervalParsed } from './interval.types'; 4 | 5 | export const server = { 6 | parse: (activeOption) => { 7 | const [value] = activeOption.value; 8 | 9 | switch (value) { 10 | case 'hourly': 11 | return 'hour'; 12 | case 'daily': 13 | return 'day'; 14 | case 'weekly': 15 | return 'week'; 16 | case 'monthly': 17 | return 'month'; 18 | default: 19 | throw new Error('Invalid interval'); 20 | } 21 | }, 22 | } satisfies ServerFilterProps; 23 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@remix-run/react'; 2 | import { type FunctionComponent } from 'react'; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export type ButtonProps = Pick & { 6 | label: string; 7 | className?: string; 8 | // rightIcon?: keyof typeof icons; 9 | }; 10 | 11 | export const Button: FunctionComponent = ({ 12 | to, 13 | label, 14 | className, 15 | }) => { 16 | return ( 17 |
    18 | 19 | {label} 20 | 21 |
    22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/settings/hooks/useSettingsLoaderData/useSettingsLoaderData.ts: -------------------------------------------------------------------------------- 1 | import { SerializeFrom } from '@remix-run/node'; 2 | import { useRouteLoaderData } from '@remix-run/react'; 3 | import { invariant } from 'ts-invariant'; 4 | 5 | import { type loader } from '../../$teamSlug.$projectSlug.settings.route'; 6 | 7 | export function useSettingsLoaderData(): SerializeFrom { 8 | const data = useRouteLoaderData( 9 | '$teamSlug.$projectSlug.settings', 10 | ); 11 | invariant( 12 | data, 13 | `Route loader data for route $teamSlug.$projectSlug.settings is undefined.`, 14 | ); 15 | return data; 16 | } 17 | -------------------------------------------------------------------------------- /packages/cache.server/src/modules/ioredis.ts: -------------------------------------------------------------------------------- 1 | import { remember } from '@epic-web/remember'; 2 | import { env } from '@metronome/env.server'; 3 | import IOredis from 'ioredis'; 4 | 5 | export const ioredis = remember('cache.ioredis', () => { 6 | const { url, password, family } = env.cache(); 7 | return new IOredis(url, { 8 | maxRetriesPerRequest: null, 9 | password, 10 | family, 11 | }); 12 | }); 13 | 14 | export const ioredisUnique = remember('cache.ioredisUnique', () => { 15 | const { url, password, family } = env.cache({ unique: true }); 16 | return new IOredis(url, { 17 | maxRetriesPerRequest: null, 18 | password, 19 | family, 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/db.server/src/utils/device.ts: -------------------------------------------------------------------------------- 1 | import { deviceDetector } from '../modules/deviceDetector'; 2 | 3 | function getCategory(type?: string) { 4 | switch (type) { 5 | case 'desktop': 6 | return 'desktop'; 7 | case 'tablet': 8 | case 'mobile': 9 | case 'phablet': 10 | case 'smartphone': 11 | return 'mobile'; 12 | default: 13 | return 'unknown'; 14 | } 15 | } 16 | 17 | export function getDeviceProps(userAgent: string) { 18 | const result = deviceDetector.parse(userAgent); 19 | const category = getCategory(result.device?.type); 20 | 21 | const type = result.device?.type || 'unknown'; 22 | 23 | return { category, type }; 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/BrandApple.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const BrandApple: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/ClipboardCopy.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const ClipboardCopy: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/SettingsTwo.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const SettingsTwo: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | {' '} 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/app/routes/notifications/notifications.$notificationId.route.tsx: -------------------------------------------------------------------------------- 1 | import { users } from '@metronome/db.server'; 2 | import { ActionFunctionArgs } from '@remix-run/node'; 3 | import { invariant } from 'ts-invariant'; 4 | 5 | import { handle } from '#app/handlers'; 6 | import { success } from '#app/responses'; 7 | 8 | export async function action({ request, params }: ActionFunctionArgs) { 9 | const { notificationId = '' } = params; 10 | invariant(notificationId, 'notificationId is required'); 11 | 12 | const { auth } = await handle(request); 13 | 14 | const user = await auth.user(); 15 | 16 | await users.markNotificationAsSeen(user.id, notificationId); 17 | 18 | return success(); 19 | } 20 | -------------------------------------------------------------------------------- /apps/producer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@metronome/producer", 3 | "version": "4.0.0", 4 | "author": "Erick Tamayo", 5 | "license": "FSL-1.0-MIT", 6 | "description": "Producer that posts random seed data for testing purposes", 7 | "main": "./dist/cjs/index.js", 8 | "types": "dist/index.d.ts", 9 | "scripts": { 10 | "dev": "tsx --watch src/index.ts" 11 | }, 12 | "keywords": [], 13 | "devDependencies": { 14 | "@types/node": "^20.8.9", 15 | "typescript": "^5.2.2" 16 | }, 17 | "dependencies": { 18 | "@faker-js/faker": "^8.2.0", 19 | "@metronome/env.server": "workspace:*", 20 | "node-fetch": "^3.3.2", 21 | "tsx": "^3.14.0", 22 | "web-vitals": "^3.5.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/SquaresFilled.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const SquaresFilled: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/HeartFilled.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const HeartFilled: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/db.server/src/utils/url.ts: -------------------------------------------------------------------------------- 1 | import { getDomain, getPublicSuffix } from 'tldjs'; 2 | 3 | export function getDisplayNameFromURL(incomingUrl: string): string | null { 4 | try { 5 | const url = new URL(incomingUrl); 6 | 7 | // Remove 'www.' prefix if it exists 8 | if (url.hostname.startsWith('www.')) { 9 | url.hostname = url.hostname.substring(4); 10 | } 11 | 12 | // Use tldjs to get the domain name without the TLD 13 | const domain = getDomain(url.hostname); 14 | const sufix = getPublicSuffix(url.hostname); 15 | 16 | if (domain && sufix) { 17 | return domain.replace(`.${sufix}`, ''); 18 | } 19 | } catch (error) { 20 | return ''; 21 | } 22 | 23 | return ''; 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/TimelineEventExclamation.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const TimelineEventExclamation: FunctionComponent = ( 6 | props, 7 | ) => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /apps/web/app/components/Dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog as DialogComponent, 3 | DialogContent, 4 | DialogDescription, 5 | DialogFooter, 6 | DialogHeader, 7 | DialogTitle, 8 | DialogTrigger, 9 | } from './Dialog'; 10 | 11 | export const Dialog: typeof DialogComponent & { 12 | Content: typeof DialogContent; 13 | Description: typeof DialogDescription; 14 | Footer: typeof DialogFooter; 15 | Header: typeof DialogHeader; 16 | Title: typeof DialogTitle; 17 | Trigger: typeof DialogTrigger; 18 | } = Object.assign(DialogComponent, { 19 | Content: DialogContent, 20 | Description: DialogDescription, 21 | Header: DialogHeader, 22 | Title: DialogTitle, 23 | Trigger: DialogTrigger, 24 | Footer: DialogFooter, 25 | }); 26 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Key.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Key: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0030_warm_jazinda.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "web_vitals" RENAME COLUMN "organization_id" TO "team_id";--> statement-breakpoint 2 | DROP INDEX IF EXISTS "actions_organization_timestamp_idx";--> statement-breakpoint 3 | DROP INDEX IF EXISTS "loaders_organization_timestamp_idx";--> statement-breakpoint 4 | DROP INDEX IF EXISTS "web_vitals_organization_timestamp_idx";--> statement-breakpoint 5 | CREATE INDEX IF NOT EXISTS "actions_team_timestamp_idx" ON "actions" ("team_id","timestamp");--> statement-breakpoint 6 | CREATE INDEX IF NOT EXISTS "loaders_team_timestamp_idx" ON "loaders" ("team_id","timestamp");--> statement-breakpoint 7 | CREATE INDEX IF NOT EXISTS "web_vitals_team_timestamp_idx" ON "web_vitals" ("team_id","timestamp"); -------------------------------------------------------------------------------- /apps/web/app/events/events.ts: -------------------------------------------------------------------------------- 1 | export type MetronomeEventName = 2 | | 'project:first-event' 3 | | 'project:pageviews' 4 | | 'project:visitor-right-now' 5 | | 'project:bounce-rate' 6 | | 'project:requests' 7 | | 'project:requests-series' 8 | | 'project:loaders-overview' 9 | | 'project:loaders-series' 10 | | 'project:actions-overview' 11 | | 'project:actions-series' 12 | | 'project:sessions-overview' 13 | | 'project:web-vitals-overview' 14 | | 'project:usage' 15 | | 'project:visitors-series' 16 | | 'project:views-series' 17 | | 'project:sessions-series' 18 | | 'project:locations-by-country' 19 | | 'project:locations-by-city' 20 | | 'project:routes-list' 21 | | 'project:urls-list' 22 | | 'project:sources-list'; 23 | -------------------------------------------------------------------------------- /apps/web/app/hooks/useTimeZoneSync/useTimeZoneSync.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from '@remix-run/react'; 2 | import isbot from 'isbot'; 3 | import { useEffect } from 'react'; 4 | 5 | export function useTimeZoneSync() { 6 | const location = useLocation(); 7 | 8 | useEffect(() => { 9 | if (isbot(navigator.userAgent)) return; 10 | 11 | const regexp = new RegExp('(^| )timeZone=([^;]+)'); 12 | const cookietimeZone = (document.cookie.match(regexp) || [])[2]; 13 | const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; 14 | 15 | if (cookietimeZone !== `${timeZone}`) { 16 | document.cookie = `timeZone=${timeZone}; path=/; max-age=31536000`; 17 | window.location.reload(); 18 | } 19 | }, [location.key]); 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/components/Brand/Brand.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { Brand } from '.'; 3 | import { remixRootDecorator, mockUseFetcher } from '@metronome/storybook'; 4 | 5 | const meta: Meta = { 6 | title: 'Brand', 7 | component: Brand, 8 | parameters: { 9 | layout: 'centered', 10 | }, 11 | args: {}, 12 | decorators: [ 13 | mockUseFetcher({ 14 | state: 'loading', 15 | }), 16 | remixRootDecorator, 17 | ], 18 | }; 19 | 20 | export default meta; 21 | type Story = StoryObj; 22 | 23 | export const Default: Story = { 24 | args: {}, 25 | }; 26 | 27 | export const Logo: Story = { 28 | args: {}, 29 | render: (args) => , 30 | }; 31 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/BrandEdge.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const BrandEdge: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Bug.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const Bug: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/components/Markdoc/components/Alert/Alert.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, PropsWithChildren } from 'react'; 2 | 3 | import { Alert as AlertPrimitive, Icon } from '#app/components'; 4 | 5 | type AlertProps = PropsWithChildren<{ 6 | title?: string; 7 | }>; 8 | 9 | export const Alert: FunctionComponent = ({ children, title }) => { 10 | return ( 11 | 12 | 13 | {title ? ( 14 | 15 | {title} 16 | 17 | ) : null} 18 | {children} 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /apps/cron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@metronome/cron", 3 | "version": "4.0.0", 4 | "author": "Erick Tamayo", 5 | "license": "FSL-1.0-MIT", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "tsx --watch src/index.ts", 9 | "start": "tsx src/index.ts" 10 | }, 11 | "keywords": [], 12 | "devDependencies": { 13 | "@types/node": "^20.10.5", 14 | "typescript": "^5.3.3" 15 | }, 16 | "dependencies": { 17 | "@breejs/ts-worker": "^2.0.0", 18 | "@faker-js/faker": "^8.3.1", 19 | "@metronome/cron": "link:", 20 | "@metronome/db.server": "workspace:^", 21 | "@metronome/env.server": "workspace:*", 22 | "bree": "^9.2.2", 23 | "node-fetch": "^3.3.2", 24 | "tsx": "^3.14.0", 25 | "web-vitals": "^3.5.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/app/components/UrlRouteDisplay/UrlRouteDisplay.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { UrlRouteDisplay } from '.'; 3 | 4 | const meta = { 5 | title: 'Basic/UrlRouteDisplay', 6 | component: UrlRouteDisplay, 7 | parameters: { 8 | layout: 'centered', 9 | }, 10 | argTypes: {}, 11 | } satisfies Meta; 12 | 13 | export default meta; 14 | 15 | type Story = StoryObj; 16 | 17 | export const Default: Story = { 18 | args: { 19 | url: '/reports/134556/detail', 20 | route: '/reports/:reportId/detail', 21 | }, 22 | }; 23 | 24 | export const WithSplat: Story = { 25 | args: { 26 | url: '/docs/getting-started/express-adapter', 27 | route: '/docs/*', 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | dist 3 | tmp 4 | /out-tsc 5 | 6 | # dependencies 7 | node_modules 8 | .env 9 | .env.backup 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | # Misc 42 | .remix 43 | storybook-static 44 | .cache 45 | 46 | # Remix 47 | apps/web/build 48 | apps/web/public/build 49 | 50 | # GeoIp 51 | /packages/db.server/geoip 52 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/web-analytics/components/GeneralWebAnalyticsSection/components/index.ts: -------------------------------------------------------------------------------- 1 | export { BounceRateTabTrigger } from './BounceRateTabTrigger'; 2 | export { MedianSessionTimeTabContent } from './MedianSessionTimeTabContent'; 3 | export { MedianSessionTimeTabTrigger } from './MedianSessionTimeTabTrigger'; 4 | export { SessionsTabContent } from './SessionsTabContent'; 5 | export { SessionsTabTrigger } from './SessionsTabTrigger'; 6 | export { UniqueVisitorsTabTrigger } from './UniqueVisitorsTabTrigger'; 7 | export { ViewsTabContent } from './ViewsTabContent'; 8 | export { ViewsTabTrigger } from './ViewsTabTrigger'; 9 | export { VisitorsChartTabContent } from './VisitorsChartTabContent'; 10 | export { VisitorsRightNowTabTrigger } from './VisitorsRightNowTabTrigger'; 11 | -------------------------------------------------------------------------------- /packages/db.server/src/utils/referrer.ts: -------------------------------------------------------------------------------- 1 | import { env } from '@metronome/env.server'; 2 | 3 | type ResolvedReferrer = { 4 | referrer: string | null; 5 | referrerDomain: string | null; 6 | }; 7 | 8 | export function resolveReferrer(referrer: string): ResolvedReferrer { 9 | if (env.dev) referrer = 'https://www.google.com/'; 10 | 11 | if (!referrer) return { referrer: null, referrerDomain: null }; 12 | 13 | try { 14 | const url = new URL(referrer); 15 | const referrerDomain = `${url.protocol}//${url.hostname}`; 16 | 17 | return { referrer, referrerDomain }; 18 | } catch (error) { 19 | console.warn( 20 | `Could not resolve referrer ${referrer}: `, 21 | (error as Error)?.name, 22 | ); 23 | return { referrer: null, referrerDomain: null }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/.storybook/mocks/modules/@remix-run/react/useNavigation.ts: -------------------------------------------------------------------------------- 1 | import { useNavigation as useNavigationPrimitive } from '../../../../../node_modules/@remix-run/react'; 2 | import { useContext, useEffect, useState } from 'react'; 3 | import { MockContext } from '../../../MockContext'; 4 | 5 | export function useNavigation() { 6 | const { navigation } = useContext(MockContext); 7 | 8 | const [navigationState, setNavigationState] = useState( 9 | Array.isArray(navigation) ? navigation[0] : navigation, 10 | ); 11 | 12 | useEffect(() => { 13 | if (Array.isArray(navigation)) { 14 | setNavigationState(navigation[1]); 15 | } 16 | }, [navigation]); 17 | 18 | const navigationPrimitive = useNavigationPrimitive(); 19 | return { ...navigationPrimitive, ...navigationState }; 20 | } 21 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_URL="http://localhost" 2 | APP_PORT="3000" 3 | NODE_ENV="development" 4 | SESSION_SECRET="secret" 5 | 6 | DB_READ_DATABASE="metronome" 7 | DB_READ_USER="metronome" 8 | DB_READ_PASSWORD="secret" 9 | DB_READ_HOST="localhost" 10 | DB_READ_PORT="5432" 11 | 12 | DB_WRITE_DATABASE="metronome" 13 | DB_WRITE_USER="metronome" 14 | DB_WRITE_PASSWORD="secret" 15 | DB_WRITE_HOST="localhost" 16 | DB_WRITE_PORT="5432" 17 | 18 | REDIS_QUEUE_URL="redis://localhost:6379" 19 | REDIS_QUEUE_PASSWORD="" 20 | REDIS_QUEUE_FAMILY=4 21 | 22 | REDIS_CACHE_URL="redis://localhost:6379" 23 | REDIS_CACHE_PASSWORD="" 24 | REDIS_CACHE_FAMILY=4 25 | 26 | REDIS_UNIQUE_URL="redis://localhost:6379" 27 | REDIS_UNIQUE_PASSWORD="" 28 | REDIS_UNIQUE_FAMILY=4 29 | 30 | PRODUCER_PROJECT_API_KEY="" 31 | 32 | MAXMIND_LICENSE_KEY="" 33 | -------------------------------------------------------------------------------- /packages/db.server/clickhouse/migrations/1705987423878-create-events-table.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { clickhouse } = require('../clickhouse'); 4 | 5 | module.exports.up = async function () { 6 | await clickhouse.command({ 7 | query: ` 8 | CREATE TABLE events ( 9 | id UUID DEFAULT generateUUIDv4(), 10 | project_id String, 11 | trace_id String, 12 | span_id String, 13 | timestamp UInt64, 14 | name String, 15 | event_attributes Nested (key String, value String), 16 | ) ENGINE = MergeTree() 17 | ORDER BY (timestamp, project_id, trace_id, span_id); 18 | `, 19 | }); 20 | }; 21 | 22 | module.exports.down = async function () { 23 | await clickhouse.command({ 24 | query: `DROP TABLE IF EXISTS events;`, 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/documents/about/pricing.mdoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Pricing" 3 | description: "Pricing of Metronome.sh" 4 | --- 5 | 6 | # Pricing 7 | 8 | ## Free 9 | 10 | Free tier is available for all users. It includes: 11 | 12 | * Limited to one project 13 | * Web Vitals 14 | * Request metrics 15 | * Loader metrics 16 | * Action metrics 17 | * Errors 18 | * Up to 5,000 Data points per month 19 | 20 | ## Standard Plan 21 | 22 | The standard plan starts at **USD 16.00/month**. It includes all features of the free tier and: 23 | 24 | * Create multiple projects 25 | * First 1 million data points are included, then **USD 16.00 per additional 1 million data points** 26 | * Web Analytics (coming soon!) 27 | * Events (coming soon!) 28 | * LightHouse (coming soon!) 29 | 30 | ## Teams Plan 31 | 32 | Coming soon. 33 | -------------------------------------------------------------------------------- /apps/web/app/components/Form/index.ts: -------------------------------------------------------------------------------- 1 | export { useFormField } from './Form'; 2 | import { 3 | Form as FormProvider, 4 | FormControl, 5 | FormDescription, 6 | FormField, 7 | FormItem, 8 | FormLabel, 9 | FormMessage, 10 | FormSection, 11 | } from './Form'; 12 | 13 | export const Form: { 14 | Provider: typeof FormProvider; 15 | Item: typeof FormItem; 16 | Label: typeof FormLabel; 17 | Control: typeof FormControl; 18 | Description: typeof FormDescription; 19 | Message: typeof FormMessage; 20 | Field: typeof FormField; 21 | Section: typeof FormSection; 22 | } = { 23 | Provider: FormProvider, 24 | Item: FormItem, 25 | Label: FormLabel, 26 | Control: FormControl, 27 | Description: FormDescription, 28 | Message: FormMessage, 29 | Field: FormField, 30 | Section: FormSection, 31 | }; 32 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/CaretDownFilled.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const CaretDownFilled: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/Svg.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent, SVGProps } from 'react'; 2 | 3 | import { cn } from '../utils'; 4 | 5 | export type SvgProps = SVGProps; 6 | 7 | export const Svg: FunctionComponent = ({ 8 | children, 9 | className, 10 | strokeWidth = 1.75, 11 | ...props 12 | }) => { 13 | return ( 14 | 28 | {children} 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/db.server/src/models/actions.ts: -------------------------------------------------------------------------------- 1 | import { actions } from '../schema'; 2 | import { ActionEventSchema } from '../schemaValidation'; 3 | import { ActionEvent } from '../types'; 4 | import { 5 | createRemixFunctionInsert, 6 | createRemixFunctionOverview, 7 | createRemixFunctionOverviewSeries, 8 | createRemixFunctionWatch, 9 | } from '../utils/remixFunctions'; 10 | 11 | export function isActionEvent(event: unknown): event is ActionEvent { 12 | const result = ActionEventSchema.safeParse(event); 13 | return result.success; 14 | } 15 | 16 | export const insert = createRemixFunctionInsert(actions); 17 | 18 | export const overview = createRemixFunctionOverview(actions); 19 | 20 | export const overviewSeries = createRemixFunctionOverviewSeries(actions); 21 | 22 | export const watch = createRemixFunctionWatch(actions); 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=linux/amd64 node:20-bullseye-slim as base 2 | 3 | # Install openssl for Prisma 4 | RUN apt-get update && apt-get install -y openssl git curl 5 | 6 | # Install pnpm 7 | RUN npm install -g pnpm 8 | 9 | WORKDIR /home/node/app 10 | 11 | COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ 12 | 13 | # Packages 14 | COPY packages ./packages/ 15 | 16 | # Web 17 | COPY apps/web ./apps/web 18 | 19 | # Workers 20 | COPY apps/workers ./apps/workers 21 | 22 | # Cron 23 | COPY apps/cron ./apps/cron 24 | 25 | RUN pnpm i 26 | 27 | RUN pnpm build 28 | 29 | # Remove node_modules from everywhere 30 | RUN find . '(' -name \"node_modules\" ')' -type d -prune -exec rm -rf '{}' + 31 | 32 | # Reinstall only production dependencies 33 | RUN pnpm i --prod 34 | 35 | CMD ["pnpm", "--filter", "@metronome/web", "start"] 36 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/documents/about/support.mdoc: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Support" 3 | description: "Support" 4 | --- 5 | 6 | # Support 7 | 8 | If you encounter any issues or have questions about Metronome, there are several ways to reach out for support: 9 | 10 | ## Email 11 | Send us an email at [erick@metronome.sh](mailto:erick@metronome.sh) for any queries or concerns. 12 | 13 | ## Twitter 14 | Feel free to tweet us or send a DM on Twitter: [@metronome_sh](https://twitter.com/metronome_sh) 15 | 16 | ## Discord 17 | Join the conversation and ask questions on the **Remix discord channel**. 18 | 19 | ## GitHub Issue 20 | If you believe you've found a bug or want to request a feature, create a new issue in our [GitHub repository](https://github.com/metronome-sh/metronome-sh/issues). 21 | 22 | Thank you for being a part of Metronome! 23 | -------------------------------------------------------------------------------- /apps/web/public/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/web/app/routes/teams/routes/projects/routes/overview/components/WebAnalyticsSection/WebAnalyticsSection.tsx: -------------------------------------------------------------------------------- 1 | import { type FunctionComponent } from 'react'; 2 | 3 | import { Section } from '../../../../components/Section'; 4 | import { 5 | BounceRate, 6 | SessionMedianDuration, 7 | TotalPageviews, 8 | TotalSessions, 9 | VisitorsRightNow, 10 | } from './components'; 11 | 12 | export const WebAnalyticsSection: FunctionComponent = () => { 13 | return ( 14 |
    15 | 16 |
    17 | 18 | 19 | 20 | 21 | 22 |
    23 |
    24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /apps/web/app/components/Label/Label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from '@radix-ui/react-label'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | import * as React from 'react'; 4 | 5 | import { cn } from '../utils.ts'; 6 | 7 | const labelVariants = cva( 8 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 9 | ); 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )); 22 | Label.displayName = LabelPrimitive.Root.displayName; 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /apps/web/app/filters/hooks/useFilterActiveOption/useFilterActiveOption.tsx: -------------------------------------------------------------------------------- 1 | import { useSearchParams } from '@remix-run/react'; 2 | import { useMemo, useRef } from 'react'; 3 | 4 | import { 5 | type ActiveFilterOption, 6 | type FilterObject, 7 | } from '#app/filters/filters.types'; 8 | import { mergeFilterOptionsWithSearch } from '#app/filters/helpers'; 9 | 10 | export function useFilterActiveOption( 11 | filter: FilterObject, 12 | ) { 13 | // TODO filter returns a function, so this is a hack to get around the useMemo 14 | const filterRef = useRef(filter); 15 | 16 | const [search] = useSearchParams(); 17 | 18 | const [activeOption] = useMemo( 19 | () => mergeFilterOptionsWithSearch(search, [filterRef.current]), 20 | [search], 21 | ); 22 | 23 | return activeOption as ActiveFilterOption; 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/app/routes/docs/getters/getDocumentMeta.ts: -------------------------------------------------------------------------------- 1 | import { parse, Tokenizer } from '@markdoc/markdoc'; 2 | import fs from 'fs/promises'; 3 | import yaml from 'js-yaml'; 4 | import path from 'path'; 5 | 6 | import { DOCUMENTS_PATH } from '../constants'; 7 | import { type DocumentMeta } from '../types'; 8 | 9 | export async function getDocumentMeta(filename: string): Promise { 10 | const source = await fs.readFile( 11 | path.resolve(DOCUMENTS_PATH, filename), 12 | 'utf-8', 13 | ); 14 | 15 | const tokenizer = new Tokenizer({ allowComments: true }); 16 | 17 | const tokens = tokenizer.tokenize(source); 18 | 19 | const ast = parse(tokens); 20 | 21 | const documentMeta = ( 22 | ast.attributes.frontmatter ? yaml.load(ast.attributes.frontmatter) : {} 23 | ) as Record; 24 | 25 | return documentMeta; 26 | } 27 | -------------------------------------------------------------------------------- /apps/web/app/utils/checkForProjectClientUpdates.ts: -------------------------------------------------------------------------------- 1 | import { cache } from '@metronome/cache.server'; 2 | import { $ } from 'execa'; 3 | import * as semver from 'semver'; 4 | 5 | export async function checkForProjectClientUpdates(currentClientVersion: string) { 6 | const latestClientVersion = await cache.remember( 7 | 'latestMetronomeClientVersion', 8 | async () => { 9 | const { stdout } = await $`npm view @metronome-sh/react version`; 10 | return stdout.trim(); 11 | }, 12 | 60 * 60 * 24, 13 | ); 14 | 15 | const needsToUpdate = false; 16 | 17 | try { 18 | semver.lt(currentClientVersion, latestClientVersion); 19 | } catch (error) { 20 | console.error(error); 21 | } 22 | 23 | return { 24 | latestClientVersion, 25 | needsToUpdate: currentClientVersion === '0.0.0' ? false : needsToUpdate, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/cache.server/scripts/cache-clear.mts: -------------------------------------------------------------------------------- 1 | import IOredis from 'ioredis'; 2 | import { env } from '@metronome/env.server'; 3 | 4 | (async () => { 5 | const PREFIX = 'metronome_cache:'; 6 | 7 | const { url, password, family } = env.cache(); 8 | const client = new IOredis(url, { maxRetriesPerRequest: null, password, family }); 9 | 10 | let cursor = '0'; 11 | 12 | do { 13 | const reply = await client.scan(cursor, 'MATCH', `${PREFIX}*`, 'COUNT', 100); 14 | cursor = reply[0]; 15 | const keys = reply[1]; 16 | 17 | await Promise.all( 18 | keys.map(async (key) => { 19 | await client.del(key, (err, reply) => { 20 | if (err) throw err; 21 | else console.log(`Deleted key ${key}:`, reply); 22 | }); 23 | }), 24 | ); 25 | } while (cursor !== '0'); 26 | 27 | process.exit(0); 28 | })(); 29 | -------------------------------------------------------------------------------- /packages/db.server/drizzle/migrations/0009_volatile_mach_iv.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "apps" RENAME TO "projects";--> statement-breakpoint 2 | ALTER TABLE "projects" DROP CONSTRAINT "apps_slug_unique";--> statement-breakpoint 3 | ALTER TABLE "projects" DROP CONSTRAINT "apps_share_slug_unique";--> statement-breakpoint 4 | ALTER TABLE "projects" DROP CONSTRAINT "apps_team_id_teams_id_fk"; 5 | --> statement-breakpoint 6 | DO $$ BEGIN 7 | ALTER TABLE "projects" ADD CONSTRAINT "projects_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "teams"("id") ON DELETE no action ON UPDATE no action; 8 | EXCEPTION 9 | WHEN duplicate_object THEN null; 10 | END $$; 11 | --> statement-breakpoint 12 | ALTER TABLE "projects" ADD CONSTRAINT "projects_slug_unique" UNIQUE("slug");--> statement-breakpoint 13 | ALTER TABLE "projects" ADD CONSTRAINT "projects_share_slug_unique" UNIQUE("share_slug"); -------------------------------------------------------------------------------- /apps/web/remix.env.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/triple-slash-reference */ 2 | /// 3 | /// 4 | 5 | import { type TypedDeferredData } from '@remix-run/node'; 6 | import { JsonifyObject } from 'type-fest/source/jsonify'; 7 | 8 | type UnwrapPromise = T extends Promise ? U : T; 9 | 10 | type UnwrapPromiseObject = { 11 | [K in keyof T]: UnwrapPromise; 12 | }; 13 | 14 | declare global { 15 | type UnwrapDeferred = LoaderFunction extends ( 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | ...args: any[] 18 | ) => Promise> 19 | ? UnwrapPromiseObject 20 | : never; 21 | 22 | type UnwrapJsonifyObject = T extends JsonifyObject[] 23 | ? U[] 24 | : never; 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/app/components/Icon/BrandUbuntu.tsx: -------------------------------------------------------------------------------- 1 | import { FunctionComponent } from 'react'; 2 | 3 | import { Svg, SvgProps } from './Svg.tsx'; 4 | 5 | export const BrandUbuntu: FunctionComponent = (props) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | --------------------------------------------------------------------------------