├── .cursorrules ├── .dockerignore ├── .env.example ├── .github └── workflows │ └── docker-build.yml ├── .gitignore ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE.md ├── README.md ├── TRADEMARK.md ├── apps ├── api │ ├── Dockerfile │ ├── package.json │ ├── scripts │ │ ├── get-bots.ts │ │ ├── get-referrers.ts │ │ ├── mock-basic.json │ │ ├── mock-big.json │ │ ├── mock-minimal.json │ │ ├── mock.ts │ │ └── test.ts │ ├── src │ │ ├── bots │ │ │ ├── bots.readme.md │ │ │ ├── bots.ts │ │ │ └── index.ts │ │ ├── controllers │ │ │ ├── ai.controller.ts │ │ │ ├── error.html │ │ │ ├── event.controller.ts │ │ │ ├── export.controller.ts │ │ │ ├── healthcheck.controller.ts │ │ │ ├── import.controller.ts │ │ │ ├── live.controller.ts │ │ │ ├── misc.controller.ts │ │ │ ├── oauth-callback.controller.tsx │ │ │ ├── profile.controller.ts │ │ │ ├── track.controller.ts │ │ │ └── webhook.controller.ts │ │ ├── hooks │ │ │ ├── client.hook.ts │ │ │ ├── fix.hook.ts │ │ │ ├── ip.hook.ts │ │ │ ├── is-bot.hook.ts │ │ │ ├── request-id.hook.ts │ │ │ ├── request-logging.hook.ts │ │ │ └── timestamp.hook.ts │ │ ├── index.ts │ │ ├── referrers │ │ │ ├── index.ts │ │ │ └── referrers.readme.md │ │ ├── routes │ │ │ ├── ai.router.ts │ │ │ ├── event.router.ts │ │ │ ├── export.router.ts │ │ │ ├── import.router.ts │ │ │ ├── live.router.ts │ │ │ ├── misc.router.ts │ │ │ ├── oauth-callback.router.ts │ │ │ ├── profile.router.ts │ │ │ ├── track.router.ts │ │ │ └── webhook.router.ts │ │ └── utils │ │ │ ├── ai-tools.ts │ │ │ ├── ai.ts │ │ │ ├── auth.ts │ │ │ ├── deduplicate.ts │ │ │ ├── errors.ts │ │ │ ├── logger.ts │ │ │ ├── parse-ip.ts │ │ │ ├── parse-zod-query-string.ts │ │ │ ├── parseUrlMeta.ts │ │ │ └── rate-limiter.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── dashboard │ ├── .gitignore │ ├── .sentryclirc │ ├── Dockerfile │ ├── README.md │ ├── components.json │ ├── entrypoint.sh │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.cjs │ ├── public │ │ ├── favicon.ico │ │ └── logo.svg │ ├── src │ │ ├── app │ │ │ ├── (app) │ │ │ │ ├── [organizationSlug] │ │ │ │ │ ├── [projectId] │ │ │ │ │ │ ├── chat │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── dashboards │ │ │ │ │ │ │ ├── [dashboardId] │ │ │ │ │ │ │ │ ├── list-reports.tsx │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ ├── list-dashboards │ │ │ │ │ │ │ │ ├── header.tsx │ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ │ └── list-dashboards.tsx │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── events │ │ │ │ │ │ │ ├── charts.tsx │ │ │ │ │ │ │ ├── conversions.tsx │ │ │ │ │ │ │ ├── events.tsx │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── layout-content.tsx │ │ │ │ │ │ ├── layout-menu.tsx │ │ │ │ │ │ ├── layout-organization-selector.tsx │ │ │ │ │ │ ├── layout-project-selector.tsx │ │ │ │ │ │ ├── layout-sidebar.tsx │ │ │ │ │ │ ├── layout-sticky-below-header.tsx │ │ │ │ │ │ ├── layout.tsx │ │ │ │ │ │ ├── page-layout.tsx │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ ├── pages │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ └── pages.tsx │ │ │ │ │ │ ├── profiles │ │ │ │ │ │ │ ├── [profileId] │ │ │ │ │ │ │ │ ├── most-events │ │ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ │ │ └── most-events.tsx │ │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ │ ├── popular-routes │ │ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ │ │ └── popular-routes.tsx │ │ │ │ │ │ │ │ ├── profile-activity │ │ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ │ │ └── profile-activity.tsx │ │ │ │ │ │ │ │ ├── profile-charts.tsx │ │ │ │ │ │ │ │ ├── profile-events.tsx │ │ │ │ │ │ │ │ └── profile-metrics │ │ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ │ │ └── profile-metrics.tsx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ ├── power-users.tsx │ │ │ │ │ │ │ ├── profile-last-seen │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ └── profiles.tsx │ │ │ │ │ │ ├── realtime │ │ │ │ │ │ │ ├── map │ │ │ │ │ │ │ │ ├── coordinates.ts │ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ │ ├── map.helpers.tsx │ │ │ │ │ │ │ │ ├── map.tsx │ │ │ │ │ │ │ │ └── markers.ts │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ ├── realtime-live-events │ │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ │ └── live-events.tsx │ │ │ │ │ │ │ ├── realtime-live-histogram.tsx │ │ │ │ │ │ │ └── realtime-reloader.tsx │ │ │ │ │ │ ├── reports │ │ │ │ │ │ │ ├── [reportId] │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ └── report-editor.tsx │ │ │ │ │ │ ├── retention │ │ │ │ │ │ │ ├── last-active-users │ │ │ │ │ │ │ │ ├── chart.tsx │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ ├── rolling-active-users │ │ │ │ │ │ │ │ ├── chart.tsx │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ ├── users-retention-series │ │ │ │ │ │ │ │ ├── chart.tsx │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ └── weekly-cohorts │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ ├── settings │ │ │ │ │ │ │ ├── integrations │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ │ ├── notifications │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ ├── organization │ │ │ │ │ │ │ │ ├── invites │ │ │ │ │ │ │ │ │ ├── create-invite.tsx │ │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ │ │ ├── members │ │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ │ ├── organization │ │ │ │ │ │ │ │ │ ├── billing-faq.tsx │ │ │ │ │ │ │ │ │ ├── billing.tsx │ │ │ │ │ │ │ │ │ ├── current-subscription.tsx │ │ │ │ │ │ │ │ │ ├── edit-organization.tsx │ │ │ │ │ │ │ │ │ ├── organization.tsx │ │ │ │ │ │ │ │ │ └── usage.tsx │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ ├── profile │ │ │ │ │ │ │ │ ├── edit-profile.tsx │ │ │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ │ │ ├── logout.tsx │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ ├── projects │ │ │ │ │ │ │ │ ├── delete-project.tsx │ │ │ │ │ │ │ │ ├── edit-project-details.tsx │ │ │ │ │ │ │ │ ├── edit-project-filters.tsx │ │ │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ │ │ └── project-clients.tsx │ │ │ │ │ │ │ └── references │ │ │ │ │ │ │ │ ├── list-references.tsx │ │ │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── side-effects-free-plan.tsx │ │ │ │ │ │ ├── side-effects-timezone.tsx │ │ │ │ │ │ ├── side-effects-trial.tsx │ │ │ │ │ │ └── side-effects.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── (auth) │ │ │ │ ├── layout.tsx │ │ │ │ ├── live-events │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── live-events.tsx │ │ │ │ ├── login │ │ │ │ │ └── page.tsx │ │ │ │ └── reset-password │ │ │ │ │ └── page.tsx │ │ │ ├── (onboarding) │ │ │ │ ├── layout.tsx │ │ │ │ ├── onboarding-layout.tsx │ │ │ │ ├── onboarding │ │ │ │ │ ├── [projectId] │ │ │ │ │ │ ├── connect │ │ │ │ │ │ │ ├── connect-app.tsx │ │ │ │ │ │ │ ├── connect-backend.tsx │ │ │ │ │ │ │ ├── connect-web.tsx │ │ │ │ │ │ │ ├── onboarding-connect.tsx │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ └── verify │ │ │ │ │ │ │ ├── onboarding-verify-listener.tsx │ │ │ │ │ │ │ ├── onboarding-verify.tsx │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── page.tsx │ │ │ │ │ └── project │ │ │ │ │ │ ├── onboarding-create-project.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── skip-onboarding.tsx │ │ │ │ └── steps.tsx │ │ │ ├── (public) │ │ │ │ └── share │ │ │ │ │ └── overview │ │ │ │ │ └── [id] │ │ │ │ │ └── page.tsx │ │ │ ├── api │ │ │ │ └── headers │ │ │ │ │ └── route.ts │ │ │ ├── favicon.ico │ │ │ ├── global-error.tsx │ │ │ ├── layout.tsx │ │ │ ├── maintenance │ │ │ │ └── page.tsx │ │ │ ├── manifest.ts │ │ │ ├── providers.tsx │ │ │ └── robots.txt │ │ ├── components │ │ │ ├── animate-height.tsx │ │ │ ├── auth │ │ │ │ ├── or.tsx │ │ │ │ ├── reset-password-form.tsx │ │ │ │ ├── share-enter-password.tsx │ │ │ │ ├── sign-in-email-form.tsx │ │ │ │ ├── sign-in-github.tsx │ │ │ │ ├── sign-in-google.tsx │ │ │ │ └── sign-up-email-form.tsx │ │ │ ├── button-container.tsx │ │ │ ├── card.tsx │ │ │ ├── chart-ssr.tsx │ │ │ ├── charts │ │ │ │ └── chart-tooltip.tsx │ │ │ ├── chat │ │ │ │ ├── chat-form.tsx │ │ │ │ ├── chat-message.tsx │ │ │ │ ├── chat-messages.tsx │ │ │ │ ├── chat-report.tsx │ │ │ │ └── chat.tsx │ │ │ ├── click-to-copy.tsx │ │ │ ├── clients │ │ │ │ ├── client-actions.tsx │ │ │ │ ├── create-client-success.tsx │ │ │ │ └── table │ │ │ │ │ ├── columns.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── color-square.tsx │ │ │ ├── dark-mode-toggle.tsx │ │ │ ├── data-table.tsx │ │ │ ├── dot.tsx │ │ │ ├── events │ │ │ │ ├── event-field-value.tsx │ │ │ │ ├── event-icon.tsx │ │ │ │ ├── event-list-item.tsx │ │ │ │ ├── event-listener.tsx │ │ │ │ ├── list-properties-icon.tsx │ │ │ │ └── table │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── events-data-table.tsx │ │ │ │ │ ├── events-table-columns.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── fade-in.tsx │ │ │ ├── forms │ │ │ │ ├── checkbox-item.tsx │ │ │ │ ├── copy-input.tsx │ │ │ │ ├── input-with-label.tsx │ │ │ │ └── tag-input.tsx │ │ │ ├── full-page-empty-state.tsx │ │ │ ├── full-page-loading-state.tsx │ │ │ ├── full-width-navbar.tsx │ │ │ ├── fullscreen-toggle.tsx │ │ │ ├── grid-table.tsx │ │ │ ├── integrations │ │ │ │ ├── active-integrations.tsx │ │ │ │ ├── all-integrations.tsx │ │ │ │ ├── forms │ │ │ │ │ ├── discord-integration.tsx │ │ │ │ │ ├── slack-integration.tsx │ │ │ │ │ └── webhook-integration.tsx │ │ │ │ ├── integration-card.tsx │ │ │ │ └── integrations.tsx │ │ │ ├── links.tsx │ │ │ ├── logo.tsx │ │ │ ├── markdown.tsx │ │ │ ├── notifications │ │ │ │ ├── notification-provider.tsx │ │ │ │ ├── notification-rules.tsx │ │ │ │ ├── notifications.tsx │ │ │ │ ├── rule-card.tsx │ │ │ │ └── table │ │ │ │ │ ├── columns.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── overview │ │ │ │ ├── filters │ │ │ │ │ ├── origin-filter.tsx │ │ │ │ │ ├── overview-filters-buttons.tsx │ │ │ │ │ ├── overview-filters-drawer-content.tsx │ │ │ │ │ └── overview-filters-drawer.tsx │ │ │ │ ├── live-counter │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── live-counter.tsx │ │ │ │ ├── overview-chart-toggle.tsx │ │ │ │ ├── overview-constants.tsx │ │ │ │ ├── overview-details-button.tsx │ │ │ │ ├── overview-hydrate-options.tsx │ │ │ │ ├── overview-interval.tsx │ │ │ │ ├── overview-live-histogram.tsx │ │ │ │ ├── overview-metric-card.tsx │ │ │ │ ├── overview-metrics.tsx │ │ │ │ ├── overview-range.tsx │ │ │ │ ├── overview-share │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── overview-share.tsx │ │ │ │ ├── overview-top-bots.tsx │ │ │ │ ├── overview-top-devices.tsx │ │ │ │ ├── overview-top-events │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── overview-top-events.tsx │ │ │ │ ├── overview-top-generic-modal.tsx │ │ │ │ ├── overview-top-geo.tsx │ │ │ │ ├── overview-top-pages-modal.tsx │ │ │ │ ├── overview-top-pages.tsx │ │ │ │ ├── overview-top-sources.tsx │ │ │ │ ├── overview-widget-table.tsx │ │ │ │ ├── overview-widget.tsx │ │ │ │ ├── useOverviewOptions.ts │ │ │ │ └── useOverviewWidget.tsx │ │ │ ├── page-tabs.tsx │ │ │ ├── pagination.tsx │ │ │ ├── ping.tsx │ │ │ ├── profiles │ │ │ │ ├── profile-avatar.tsx │ │ │ │ └── table │ │ │ │ │ ├── columns.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── projects │ │ │ │ └── project-card.tsx │ │ │ ├── react-virtualized-auto-sizer.tsx │ │ │ ├── references │ │ │ │ └── table.tsx │ │ │ ├── report-chart │ │ │ │ ├── area │ │ │ │ │ ├── chart.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── aspect-container.tsx │ │ │ │ ├── bar │ │ │ │ │ ├── chart.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── common │ │ │ │ │ ├── axis.tsx │ │ │ │ │ ├── empty.tsx │ │ │ │ │ ├── error.tsx │ │ │ │ │ ├── linear-gradient.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── previous-diff-indicator.tsx │ │ │ │ │ ├── report-chart-tooltip.tsx │ │ │ │ │ ├── report-table.tsx │ │ │ │ │ ├── serie-icon.flags.tsx │ │ │ │ │ ├── serie-icon.tsx │ │ │ │ │ ├── serie-icon.urls.ts │ │ │ │ │ └── serie-name.tsx │ │ │ │ ├── context.tsx │ │ │ │ ├── conversion │ │ │ │ │ ├── chart.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── summary.tsx │ │ │ │ ├── funnel │ │ │ │ │ ├── chart.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── histogram │ │ │ │ │ ├── chart.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── line │ │ │ │ │ ├── chart.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── map │ │ │ │ │ ├── chart.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── metric │ │ │ │ │ ├── chart.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── metric-card.tsx │ │ │ │ ├── pie │ │ │ │ │ ├── chart.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── retention │ │ │ │ │ ├── chart.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── table.tsx │ │ │ │ │ └── tooltip.tsx │ │ │ │ └── shortcut.tsx │ │ │ ├── report │ │ │ │ ├── ReportChartType.tsx │ │ │ │ ├── ReportInterval.tsx │ │ │ │ ├── ReportLineType.tsx │ │ │ │ ├── ReportSaveButton.tsx │ │ │ │ ├── edit-report-name.tsx │ │ │ │ ├── reportSlice.ts │ │ │ │ └── sidebar │ │ │ │ │ ├── EventPropertiesCombobox.tsx │ │ │ │ │ ├── PropertiesCombobox.tsx │ │ │ │ │ ├── ReportBreakdownMore.tsx │ │ │ │ │ ├── ReportBreakdowns.tsx │ │ │ │ │ ├── ReportEventMore.tsx │ │ │ │ │ ├── ReportEvents.tsx │ │ │ │ │ ├── ReportFormula.tsx │ │ │ │ │ ├── ReportSettings.tsx │ │ │ │ │ ├── ReportSidebar.tsx │ │ │ │ │ └── filters │ │ │ │ │ ├── FilterItem.tsx │ │ │ │ │ └── FiltersList.tsx │ │ │ ├── settings-toggle.tsx │ │ │ ├── settings │ │ │ │ ├── invites │ │ │ │ │ ├── columns.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── members │ │ │ │ │ ├── columns.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── sign-out-button.tsx │ │ │ ├── skeleton.tsx │ │ │ ├── stats.tsx │ │ │ ├── syntax.tsx │ │ │ ├── time-window-picker.tsx │ │ │ ├── tooltip-complete.tsx │ │ │ ├── ui │ │ │ │ ├── RenderDots.tsx │ │ │ │ ├── accordion.tsx │ │ │ │ ├── alert-dialog.tsx │ │ │ │ ├── alert.tsx │ │ │ │ ├── aspect-ratio.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── calendar.tsx │ │ │ │ ├── carousel.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── combobox-advanced.tsx │ │ │ │ ├── combobox.tsx │ │ │ │ ├── command.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── dropdown-menu.tsx │ │ │ │ ├── gradient-background.tsx │ │ │ │ ├── input-enter.tsx │ │ │ │ ├── input-otp.tsx │ │ │ │ ├── input-with-toggle.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── key-value.tsx │ │ │ │ ├── label.tsx │ │ │ │ ├── padding.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── progress.tsx │ │ │ │ ├── radio-group.tsx │ │ │ │ ├── scroll-area.tsx │ │ │ │ ├── sheet.tsx │ │ │ │ ├── sonner.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── table.tsx │ │ │ │ ├── tabs.tsx │ │ │ │ ├── textarea.tsx │ │ │ │ ├── toast.tsx │ │ │ │ ├── toaster.tsx │ │ │ │ ├── toggle-group.tsx │ │ │ │ ├── toggle.tsx │ │ │ │ └── tooltip.tsx │ │ │ ├── widget-table.tsx │ │ │ └── widget.tsx │ │ ├── env.mjs │ │ ├── hocs │ │ │ ├── with-loading-widget.tsx │ │ │ └── with-suspense.tsx │ │ ├── hooks │ │ │ ├── use-dashed-stroke.tsx │ │ │ ├── use-scroll-anchor.ts │ │ │ ├── useAppParams.ts │ │ │ ├── useAuth.tsx │ │ │ ├── useBreakpoint.ts │ │ │ ├── useClientSecret.ts │ │ │ ├── useCursor.ts │ │ │ ├── useDebounceFn.ts │ │ │ ├── useDebounceState.ts │ │ │ ├── useDebounceValue.ts │ │ │ ├── useEventNames.ts │ │ │ ├── useEventProperties.ts │ │ │ ├── useEventQueryFilters.ts │ │ │ ├── useFormatDateInterval.ts │ │ │ ├── useLogout.ts │ │ │ ├── useMappings.ts │ │ │ ├── useNumerFormatter.ts │ │ │ ├── useProfileProperties.ts │ │ │ ├── useProfileValues.ts │ │ │ ├── usePropertyValues.ts │ │ │ ├── useRechartDataModel.ts │ │ │ ├── useThrottle.ts │ │ │ ├── useVisibleSeries.ts │ │ │ └── useWS.ts │ │ ├── instrumentation.ts │ │ ├── mappings.json │ │ ├── middleware.ts │ │ ├── modals │ │ │ ├── AddClient.tsx │ │ │ ├── AddDashboard.tsx │ │ │ ├── AddProject.tsx │ │ │ ├── AddReference.tsx │ │ │ ├── Confirm.tsx │ │ │ ├── DateRangerPicker.tsx │ │ │ ├── EditClient.tsx │ │ │ ├── EditDashboard.tsx │ │ │ ├── EditReport.tsx │ │ │ ├── Instructions.tsx │ │ │ ├── Modal │ │ │ │ └── Container.tsx │ │ │ ├── OnboardingTroubleshoot.tsx │ │ │ ├── OverviewChartDetails.tsx │ │ │ ├── SaveReport.tsx │ │ │ ├── ShareOverviewModal.tsx │ │ │ ├── Testimonial.tsx │ │ │ ├── add-integration.tsx │ │ │ ├── add-notification-rule.tsx │ │ │ ├── edit-event.tsx │ │ │ ├── event-details.tsx │ │ │ ├── index.tsx │ │ │ └── request-reset-password.tsx │ │ ├── redux │ │ │ └── index.ts │ │ ├── styles │ │ │ └── globals.css │ │ ├── translations │ │ │ ├── countries.ts │ │ │ └── properties.ts │ │ ├── trpc │ │ │ └── client.tsx │ │ ├── types │ │ │ ├── index.ts │ │ │ └── react-simple-map.d.ts │ │ └── utils │ │ │ ├── casing.ts │ │ │ ├── clipboard.ts │ │ │ ├── cn.ts │ │ │ ├── date.ts │ │ │ ├── getDbId.ts │ │ │ ├── getters.ts │ │ │ ├── math.ts │ │ │ ├── meta.ts │ │ │ ├── should-ignore-keypress.ts │ │ │ ├── slug.ts │ │ │ ├── storage.ts │ │ │ ├── theme.ts │ │ │ └── truncate.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── docs │ ├── Dockerfile │ ├── index.js │ └── package.json ├── public │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── app │ │ ├── (content) │ │ │ ├── [...pages] │ │ │ │ └── page.tsx │ │ │ ├── articles │ │ │ │ ├── [articleSlug] │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── api │ │ │ └── search │ │ │ │ └── route.ts │ │ ├── docs │ │ │ ├── [[...slug]] │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── global.css │ │ ├── layout.config.tsx │ │ ├── layout.tsx │ │ ├── manifest.ts │ │ ├── not-found.tsx │ │ ├── page.tsx │ │ └── sitemap.ts │ ├── components.json │ ├── components │ │ ├── Swirls.tsx │ │ ├── article-card.tsx │ │ ├── common-sdk-config.mdx │ │ ├── device-id-warning.tsx │ │ ├── feature.tsx │ │ ├── figure.tsx │ │ ├── footer.tsx │ │ ├── github-button.tsx │ │ ├── hero-carousel.tsx │ │ ├── hero-map.tsx │ │ ├── hero.tsx │ │ ├── line.tsx │ │ ├── logo.tsx │ │ ├── navbar.tsx │ │ ├── personal-data-warning.tsx │ │ ├── pricing-slider.tsx │ │ ├── section.tsx │ │ ├── sections │ │ │ ├── faq.tsx │ │ │ ├── features.tsx │ │ │ ├── features │ │ │ │ ├── events-feature.tsx │ │ │ │ ├── product-analytics-feature.tsx │ │ │ │ ├── profiles-feature.tsx │ │ │ │ └── web-analytics-feature.tsx │ │ │ ├── pricing.tsx │ │ │ ├── sdks.tsx │ │ │ ├── stats.tsx │ │ │ ├── supporter.tsx │ │ │ └── testimonials.tsx │ │ ├── simple-chart.tsx │ │ ├── tag.tsx │ │ ├── toc.tsx │ │ ├── twitter-card.tsx │ │ ├── ui │ │ │ ├── accordion.tsx │ │ │ ├── button.tsx │ │ │ ├── slider.tsx │ │ │ └── tooltip.tsx │ │ ├── web-sdk-config.mdx │ │ ├── world-map-string.ts │ │ └── world-map.tsx │ ├── content │ │ ├── articles │ │ │ ├── .cursorrules │ │ │ ├── alternatives-to-mixpanel.mdx │ │ │ ├── how-to-create-a-funnel.mdx │ │ │ ├── how-to-secure-ubuntu-server.mdx │ │ │ ├── how-to-self-host-openpanel.mdx │ │ │ ├── introduction-to-openpanel.mdx │ │ │ ├── recap-2024.mdx │ │ │ ├── self-hosted-web-analytics.mdx │ │ │ ├── top-7-open-source-web-analytics-tools.mdx │ │ │ └── vs-mixpanel.mdx │ │ ├── docs │ │ │ ├── api │ │ │ │ ├── export.mdx │ │ │ │ ├── meta.json │ │ │ │ └── track.mdx │ │ │ ├── index.mdx │ │ │ ├── migration │ │ │ │ └── beta-v1.mdx │ │ │ ├── sdks │ │ │ │ ├── astro.mdx │ │ │ │ ├── express.mdx │ │ │ │ ├── javascript.mdx │ │ │ │ ├── meta.json │ │ │ │ ├── nextjs.mdx │ │ │ │ ├── python.mdx │ │ │ │ ├── react-native.mdx │ │ │ │ ├── react.mdx │ │ │ │ ├── remix.mdx │ │ │ │ ├── script.mdx │ │ │ │ ├── vue.mdx │ │ │ │ └── web.mdx │ │ │ └── self-hosting │ │ │ │ ├── changelog.mdx │ │ │ │ ├── meta.json │ │ │ │ ├── migrating-from-clerk.mdx │ │ │ │ └── self-hosting.mdx │ │ └── pages │ │ │ ├── about.mdx │ │ │ ├── contact.mdx │ │ │ ├── cookies.mdx │ │ │ ├── pricing.mdx │ │ │ ├── privacy.mdx │ │ │ ├── supporter-thanks.mdx │ │ │ ├── supporter.mdx │ │ │ └── terms.mdx │ ├── lib │ │ ├── dark-mode.ts │ │ ├── github.ts │ │ ├── source.ts │ │ └── utils.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── apple-touch-icon.png │ │ ├── article-example.jpg │ │ ├── avatar-2.jpg │ │ ├── avatar-3.jpg │ │ ├── avatar.jpg │ │ ├── content │ │ │ ├── cover-alternatives.jpg │ │ │ ├── cover-best-web-analytics.jpg │ │ │ ├── cover-default.jpg │ │ │ ├── cover-mixpanel.jpg │ │ │ ├── funnel │ │ │ │ ├── funnel-1.png │ │ │ │ ├── funnel-2.png │ │ │ │ ├── funnel-3.png │ │ │ │ ├── funnel-4.png │ │ │ │ ├── funnel-5.png │ │ │ │ └── funnel-breakdown.png │ │ │ ├── funnels.jpg │ │ │ ├── how-to-self-host-openpanel.jpg │ │ │ ├── recap-2024.jpg │ │ │ ├── screenshot-realtime.png │ │ │ ├── screenshot-report-bar.png │ │ │ ├── screenshot-report-funnel.png │ │ │ ├── screenshot-report-line.png │ │ │ ├── screenshot-web-analytics.png │ │ │ ├── secure-server.jpg │ │ │ └── self-hosted-analytics.jpg │ │ ├── dashboard-dark.png │ │ ├── dashboard-light.png │ │ ├── dubble-swirl.svg │ │ ├── favicon.ico │ │ ├── funnel-dark.png │ │ ├── funnel-light.png │ │ ├── google30d28bbdbd56aa6e.html │ │ ├── icons │ │ │ ├── discord.png │ │ │ ├── email.png │ │ │ ├── github.png │ │ │ └── x.png │ │ ├── logo.png │ │ ├── ogimage.jpg │ │ ├── op.js │ │ ├── op1.js │ │ ├── product-analytics-light.png │ │ ├── profile-dark.png │ │ ├── profile-light.png │ │ ├── retention-dark.png │ │ ├── retention-light.png │ │ ├── single-swirl.svg │ │ ├── site.webmanifest │ │ ├── swirl-2.png │ │ ├── swirl.png │ │ ├── twitter-carl.jpg │ │ ├── twitter-greg.png │ │ ├── twitter-jacob.jpg │ │ ├── twitter-lee.jpg │ │ ├── twitter-piotr.jpg │ │ ├── twitter-pontus.jpg │ │ ├── twitter-steven.jpg │ │ ├── web-app-manifest-192x192.png │ │ └── web-app-manifest-512x512.png │ ├── source.config.ts │ ├── tailwind.config.js │ └── tsconfig.json └── worker │ ├── Dockerfile │ ├── package.json │ ├── src │ ├── boot-cron.ts │ ├── boot-workers.ts │ ├── index.ts │ ├── jobs │ │ ├── cron.delete-projects.ts │ │ ├── cron.ping.ts │ │ ├── cron.salt.ts │ │ ├── cron.ts │ │ ├── events.create-session-end.ts │ │ ├── events.incoming-event.ts │ │ ├── events.incoming-events.test.ts │ │ ├── events.ts │ │ ├── misc.trail-ending-soon.ts │ │ ├── misc.ts │ │ ├── notification.ts │ │ └── sessions.ts │ ├── metrics.ts │ ├── referrers │ │ ├── index.ts │ │ └── referrers.readme.md │ └── utils │ │ ├── logger.ts │ │ ├── parse-referrer.ts │ │ └── session-handler.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── biome.json ├── docker-compose.yml ├── package.json ├── packages ├── auth │ ├── constants.ts │ ├── index.ts │ ├── nextjs.ts │ ├── package.json │ ├── server │ │ └── oauth.ts │ ├── src │ │ ├── cookie.ts │ │ ├── index.ts │ │ ├── oauth.ts │ │ ├── password.ts │ │ └── session.ts │ └── tsconfig.json ├── cli │ ├── package.json │ ├── src │ │ ├── cli.ts │ │ └── importer │ │ │ ├── importer.ts │ │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── common │ ├── index.ts │ ├── package.json │ ├── server │ │ ├── crypto.ts │ │ ├── id.ts │ │ ├── index.ts │ │ ├── parser-user-agent.ts │ │ └── profileId.ts │ ├── src │ │ ├── date.ts │ │ ├── get-previous-metric.ts │ │ ├── group-by-labels.ts │ │ ├── id.ts │ │ ├── math.ts │ │ ├── names.ts │ │ ├── object.ts │ │ ├── slug.ts │ │ ├── string.ts │ │ ├── timezones.ts │ │ └── url.ts │ └── tsconfig.json ├── constants │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── db │ ├── code-migrations │ │ ├── 1-settings.ts │ │ ├── 2-accounts.ts │ │ ├── 3-init-ch.sql │ │ ├── 3-init-ch.ts │ │ ├── 4-add-sessions.sql │ │ ├── 4-add-sessions.ts │ │ ├── helpers.ts │ │ └── migrate.ts │ ├── index.ts │ ├── package.json │ ├── prisma │ │ ├── migrations │ │ │ ├── 20231010091416_init │ │ │ │ └── migration.sql │ │ │ ├── 20231010094459_add_dates │ │ │ │ └── migration.sql │ │ │ ├── 20231010184810_fix_profile │ │ │ │ └── migration.sql │ │ │ ├── 20231010185023_add_profile_properties │ │ │ │ └── migration.sql │ │ │ ├── 20231010195623_add_client │ │ │ │ └── migration.sql │ │ │ ├── 20231010195805_add_timestamps_on_client │ │ │ │ └── migration.sql │ │ │ ├── 20231010202343_add_profile_id_on_event │ │ │ │ └── migration.sql │ │ │ ├── 20231010202552_profile_nullable_on_events │ │ │ │ └── migration.sql │ │ │ ├── 20231011063223_rename_profile_id_to_external │ │ │ │ └── migration.sql │ │ │ ├── 20231011064100_add_unique_external_id_and_project_id │ │ │ │ └── migration.sql │ │ │ ├── 20231012082544_external_id_optional │ │ │ │ └── migration.sql │ │ │ ├── 20231018180355_dashboard_and_reports │ │ │ │ └── migration.sql │ │ │ ├── 20231018181159_add_name_to_report │ │ │ │ └── migration.sql │ │ │ ├── 20231023172003_org_to_clients │ │ │ │ └── migration.sql │ │ │ ├── 20231023172105_org_required_for_client │ │ │ │ └── migration.sql │ │ │ ├── 20231024074846_add_slugs │ │ │ │ └── migration.sql │ │ │ ├── 20231101143637_add_minute_to_interval │ │ │ │ └── migration.sql │ │ │ ├── 20240107183438_add_histogram │ │ │ │ └── migration.sql │ │ │ ├── 20240107203928_range_remove │ │ │ │ └── migration.sql │ │ │ ├── 20240107204032_range_add │ │ │ │ └── migration.sql │ │ │ ├── 20240110151531_add_cors_settings_on_client │ │ │ │ └── migration.sql │ │ │ ├── 20240112083054_add_failed_events │ │ │ │ └── migration.sql │ │ │ ├── 20240113095542_remove_slug_2 │ │ │ │ └── migration.sql │ │ │ ├── 20240116100132_add_recent_dashboards │ │ │ │ └── migration.sql │ │ │ ├── 20240116101051_fix_recent_dashboards │ │ │ │ └── migration.sql │ │ │ ├── 20240116101524_add_relations_to_recent │ │ │ │ └── migration.sql │ │ │ ├── 20240116101723_remove_name_from_recent_dashboards │ │ │ │ └── migration.sql │ │ │ ├── 20240116183124_add_invite │ │ │ │ └── migration.sql │ │ │ ├── 20240117210232_add_line_type │ │ │ │ └── migration.sql │ │ │ ├── 20240121195834_add_previous_boolean_on_report │ │ │ │ └── migration.sql │ │ │ ├── 20240122191559_remove_foreigen_key_profile_events │ │ │ │ └── migration.sql │ │ │ ├── 20240129163925_add_salts │ │ │ │ └── migration.sql │ │ │ ├── 20240131204540_add_formula │ │ │ │ └── migration.sql │ │ │ ├── 20240131212106_add_metric │ │ │ │ └── migration.sql │ │ │ ├── 20240202175049_add_events_count_on_project │ │ │ │ └── migration.sql │ │ │ ├── 20240204201022_add_waitlist │ │ │ │ └── migration.sql │ │ │ ├── 20240207084900_remove_users │ │ │ │ └── migration.sql │ │ │ ├── 20240207085251_remove_org │ │ │ │ └── migration.sql │ │ │ ├── 20240207201711_add_org_slug_on_dashboards │ │ │ │ └── migration.sql │ │ │ ├── 20240212195057_add_share_overview │ │ │ │ └── migration.sql │ │ │ ├── 20240212211702_make_pw_opptional_share │ │ │ │ └── migration.sql │ │ │ ├── 20240214222026_add_event_meta │ │ │ │ └── migration.sql │ │ │ ├── 20240216202332_add_project_id_to_event_meta │ │ │ │ └── migration.sql │ │ │ ├── 20240216202514_fix_event_meta │ │ │ │ └── migration.sql │ │ │ ├── 20240216202657_add_unique_event_meta │ │ │ │ └── migration.sql │ │ │ ├── 20240219083932_add_map_to_chart_types │ │ │ │ └── migration.sql │ │ │ ├── 20240223193217_add_funnel_chart_type │ │ │ │ └── migration.sql │ │ │ ├── 20240306193027_add_reference │ │ │ │ └── migration.sql │ │ │ ├── 20240306195438_add_date_to_reference │ │ │ │ └── migration.sql │ │ │ ├── 20240311201118_add_accepted_to_waitinglist │ │ │ │ └── migration.sql │ │ │ ├── 20240325221639_add_project_access │ │ │ │ └── migration.sql │ │ │ ├── 20240325221913_add_uuid_project_access │ │ │ │ └── migration.sql │ │ │ ├── 20240325222110_add_org_slug_to_project_access │ │ │ │ └── migration.sql │ │ │ ├── 20240408181306_rename_columns │ │ │ │ └── migration.sql │ │ │ ├── 20240408184129_rename_colimns_2 │ │ │ │ └── migration.sql │ │ │ ├── 20240411180308_add_client_type │ │ │ │ └── migration.sql │ │ │ ├── 20240411180951_change_default_client_type │ │ │ │ └── migration.sql │ │ │ ├── 20240412180636_cors_nullable │ │ │ │ └── migration.sql │ │ │ ├── 20240412195501_add_project_type │ │ │ │ └── migration.sql │ │ │ ├── 20240427075544_change_default │ │ │ │ └── migration.sql │ │ │ ├── 20240615125828_add_orgs_memebers_and_users │ │ │ │ └── migration.sql │ │ │ ├── 20240615130029_user_camel_case │ │ │ │ └── migration.sql │ │ │ ├── 20240615195851_add_meta_to_members │ │ │ │ └── migration.sql │ │ │ ├── 20240615205817_add_deleted_flag │ │ │ │ └── migration.sql │ │ │ ├── 20240615213456_add_organization_id_to_the_rest_of_the_models │ │ │ │ └── migration.sql │ │ │ ├── 20240616172839_add_user_to_project_access │ │ │ │ └── migration.sql │ │ │ ├── 20240621191237_add_cross_domain_support │ │ │ │ └── migration.sql │ │ │ ├── 20240922184723_add_retention │ │ │ │ └── migration.sql │ │ │ ├── 20240925202841_notifications │ │ │ │ └── migration.sql │ │ │ ├── 20240925204316_notification_send_to_app_and_email │ │ │ │ └── migration.sql │ │ │ ├── 20240926175415_renaming │ │ │ │ └── migration.sql │ │ │ ├── 20240927190558_rename_notification_controls │ │ │ │ └── migration.sql │ │ │ ├── 20240927195752_add_name_to_rules │ │ │ │ └── migration.sql │ │ │ ├── 20241001215748_add_payload_to_notifications │ │ │ │ └── migration.sql │ │ │ ├── 20241010202123_add_week_as_interval │ │ │ │ └── migration.sql │ │ │ ├── 20241015090417_add_criteria │ │ │ │ └── migration.sql │ │ │ ├── 20241018190421_add_funnel_options_to_report │ │ │ │ └── migration.sql │ │ │ ├── 20241022195212_add_template_to_notification_rules │ │ │ │ └── migration.sql │ │ │ ├── 20241120131305_remove_org_slug │ │ │ │ └── migration.sql │ │ │ ├── 20241205190155_extend_project_settings │ │ │ │ └── migration.sql │ │ │ ├── 20241205191533_cors_as_array │ │ │ │ └── migration.sql │ │ │ ├── 20241207213908_auth │ │ │ │ └── migration.sql │ │ │ ├── 20241207215335_auth2 │ │ │ │ └── migration.sql │ │ │ ├── 20241208213543_auth_3 │ │ │ │ └── migration.sql │ │ │ ├── 20241209130044_invites │ │ │ │ └── migration.sql │ │ │ ├── 20241209131153_invite_2 │ │ │ │ └── migration.sql │ │ │ ├── 20241209133013_invite_3 │ │ │ │ └── migration.sql │ │ │ ├── 20241209133136_invite_4 │ │ │ │ └── migration.sql │ │ │ ├── 20241210091648_reset_pw │ │ │ │ └── migration.sql │ │ │ ├── 20241210091731_reset_pw_2 │ │ │ │ └── migration.sql │ │ │ ├── 20241210143440_code_migration │ │ │ │ └── migration.sql │ │ │ ├── 20241212214628_code_migration │ │ │ │ └── migration.sql │ │ │ ├── 20241221092853_add_email_to_account │ │ │ │ └── migration.sql │ │ │ ├── 20241229150840_add_rule_to_notification │ │ │ │ └── migration.sql │ │ │ ├── 20250101222359_payments │ │ │ │ └── migration.sql │ │ │ ├── 20250104140340_add_events_count_on_org_level │ │ │ │ └── migration.sql │ │ │ ├── 20250104192616_subscription_v2 │ │ │ │ └── migration.sql │ │ │ ├── 20250104194353_subscription_v2 │ │ │ │ └── migration.sql │ │ │ ├── 20250104194646_subs_v3 │ │ │ │ └── migration.sql │ │ │ ├── 20250123081016_add_buffer_for_events │ │ │ │ └── migration.sql │ │ │ ├── 20250126163736_profile_buffer │ │ │ │ └── migration.sql │ │ │ ├── 20250127054047_bot_events_buffer │ │ │ │ └── migration.sql │ │ │ ├── 20250210192312_clean_up │ │ │ │ └── migration.sql │ │ │ ├── 20250219225132_rename_subscription_period_limit │ │ │ │ └── migration.sql │ │ │ ├── 20250220215129_subscription_wip │ │ │ │ └── migration.sql │ │ │ ├── 20250221204153_sub_wip │ │ │ │ └── migration.sql │ │ │ ├── 20250225220926_delete_project │ │ │ │ └── migration.sql │ │ │ ├── 20250225230336_cascade_delete_on_projet │ │ │ │ └── migration.sql │ │ │ ├── 20250225230540_cascade_delete_2 │ │ │ │ └── migration.sql │ │ │ ├── 20250326202409_on_cascade │ │ │ │ └── migration.sql │ │ │ ├── 20250326202444_add_conversion │ │ │ │ └── migration.sql │ │ │ ├── 20250331190933_more_cascade │ │ │ │ └── migration.sql │ │ │ ├── 20250409203918_add_chat │ │ │ │ └── migration.sql │ │ │ ├── 20250518190347_add_timezone │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── src │ │ ├── buffers │ │ │ ├── base-buffer.ts │ │ │ ├── bot-buffer-redis.ts │ │ │ ├── event-buffer-redis.ts │ │ │ ├── index.ts │ │ │ ├── partial-json-match.ts │ │ │ ├── profile-buffer-redis.ts │ │ │ └── session-buffer.ts │ │ ├── clickhouse │ │ │ ├── client.ts │ │ │ ├── migration.ts │ │ │ └── query-builder.ts │ │ ├── prisma-client.ts │ │ ├── services │ │ │ ├── chart.service.ts │ │ │ ├── clients.service.ts │ │ │ ├── conversion.service.ts │ │ │ ├── dashboard.service.ts │ │ │ ├── event.service.ts │ │ │ ├── funnel.service.ts │ │ │ ├── id.service.ts │ │ │ ├── insights.service.ts │ │ │ ├── notification.service.ts │ │ │ ├── organization.service.ts │ │ │ ├── overview.service.ts │ │ │ ├── profile.service.ts │ │ │ ├── project.service.ts │ │ │ ├── reference.service.ts │ │ │ ├── reports.service.ts │ │ │ ├── retention.service.ts │ │ │ ├── salt.service.ts │ │ │ ├── session.service.ts │ │ │ ├── share.service.ts │ │ │ └── user.service.ts │ │ ├── sql-builder.ts │ │ └── types.ts │ ├── test.ts │ └── tsconfig.json ├── email │ ├── index.ts │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── footer.tsx │ │ │ └── layout.tsx │ │ ├── emails │ │ │ ├── email-invite.tsx │ │ │ ├── email-reset-password.tsx │ │ │ ├── index.tsx │ │ │ └── trial-ending-soon.tsx │ │ └── index.tsx │ └── tsconfig.json ├── integrations │ ├── index.ts │ ├── package.json │ ├── src │ │ ├── discord.ts │ │ └── slack.ts │ └── tsconfig.json ├── json │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── logger │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── payments │ ├── index.ts │ ├── package.json │ ├── scripts │ │ ├── create-custom-pricing.ts │ │ └── create-products.ts │ ├── src │ │ ├── polar.ts │ │ └── prices.ts │ └── tsconfig.json ├── queue │ ├── index.ts │ ├── package.json │ ├── src │ │ ├── queues.ts │ │ └── utils.ts │ └── tsconfig.json ├── redis │ ├── cachable.ts │ ├── index.ts │ ├── package.json │ ├── publisher.ts │ ├── redis.ts │ ├── run-every.ts │ └── tsconfig.json ├── sdks │ ├── _info │ │ ├── frameworks.tsx │ │ ├── icons │ │ │ ├── astro-icon.tsx │ │ │ ├── express-icon.tsx │ │ │ ├── flutter-icon.tsx │ │ │ ├── html-icon.tsx │ │ │ ├── kotlin-icon.tsx │ │ │ ├── laravel-icon.tsx │ │ │ ├── nextjs-icon.tsx │ │ │ ├── node-icon.tsx │ │ │ ├── python-icon.tsx │ │ │ ├── react-icon.tsx │ │ │ ├── remix-icon.tsx │ │ │ ├── rest-icon.tsx │ │ │ ├── swift-icon.tsx │ │ │ ├── types.ts │ │ │ └── vue-icon.tsx │ │ ├── index.ts │ │ ├── package.json │ │ └── tsconfig.json │ ├── astro │ │ ├── README.md │ │ ├── env.d.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── IdentifyComponent.astro │ │ │ ├── OpenPanelComponent.astro │ │ │ ├── SetGlobalPropertiesComponent.astro │ │ │ └── asto-utils.ts │ │ └── tsconfig.json │ ├── express │ │ ├── index.ts │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── nextjs │ │ ├── createNextRouteHandler.ts │ │ ├── index.tsx │ │ ├── package.json │ │ ├── server.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── react-native │ │ ├── index.ts │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── sdk │ │ ├── index.ts │ │ ├── package.json │ │ ├── src │ │ │ ├── api.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ └── web │ │ ├── index.ts │ │ ├── package.json │ │ ├── src │ │ ├── index.ts │ │ ├── tracker.ts │ │ └── types.d.ts │ │ ├── tsconfig.json │ │ └── tsup.config.ts ├── trpc │ ├── index.ts │ ├── package.json │ ├── src │ │ ├── access.ts │ │ ├── errors.ts │ │ ├── root.ts │ │ ├── routers │ │ │ ├── auth.ts │ │ │ ├── chart.helpers.ts │ │ │ ├── chart.ts │ │ │ ├── client.ts │ │ │ ├── dashboard.ts │ │ │ ├── event.ts │ │ │ ├── integration.ts │ │ │ ├── notification.ts │ │ │ ├── onboarding.ts │ │ │ ├── organization.ts │ │ │ ├── overview.ts │ │ │ ├── profile.ts │ │ │ ├── project.ts │ │ │ ├── reference.ts │ │ │ ├── report.ts │ │ │ ├── share.ts │ │ │ ├── subscription.ts │ │ │ ├── ticket.ts │ │ │ └── user.ts │ │ └── trpc.ts │ └── tsconfig.json └── validation │ ├── index.ts │ ├── package.json │ ├── src │ ├── index.ts │ └── types.validation.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── self-hosting ├── .env.template ├── .gitignore ├── caddy │ └── Caddyfile.template ├── clickhouse │ ├── clickhouse-config.xml │ ├── clickhouse-user-config.xml │ └── init-db.sh ├── danger_wipe_everything ├── docker-compose.template.yml ├── logs ├── package-lock.json ├── package.json ├── quiz.ts ├── rebuild ├── setup ├── start ├── stop ├── tsconfig.json └── update ├── sh ├── docker-build ├── docker-publish └── move-sdks-to-examples └── tooling ├── publish ├── package.json ├── publish.ts └── tsconfig.json └── typescript ├── base.json └── package.json /.cursorrules: -------------------------------------------------------------------------------- 1 | - When we write clickhouse queries you should always use the custom query builder we have in 2 | - `./packages/db/src/clickhouse/query-builder.ts` 3 | - `./packages/db/src/clickhouse/query-functions.ts` -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.env 2 | **/node_modules 3 | **/dist 4 | Dockerfile 5 | .dockerignore 6 | node_modules 7 | npm-debug.log 8 | README.md 9 | .next 10 | .git 11 | docker 12 | converage -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # STORAGE 2 | REDIS_URL="redis://127.0.0.1:6379" 3 | DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres?schema=public" 4 | DATABASE_URL_DIRECT="$DATABASE_URL" 5 | CLICKHOUSE_URL="http://localhost:8123/openpanel" 6 | 7 | # REST 8 | BATCH_SIZE="5000" 9 | BATCH_INTERVAL="10000" 10 | CONCURRENCY="10" 11 | NEXT_PUBLIC_DASHBOARD_URL="http://localhost:3000" 12 | NEXT_PUBLIC_API_URL="http://localhost:3333" 13 | WORKER_PORT=9999 14 | API_PORT=3333 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.mdx 2 | *.sql -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["yoavbls.pretty-ts-errors"] 3 | } 4 | -------------------------------------------------------------------------------- /apps/api/src/bots/bots.readme.md: -------------------------------------------------------------------------------- 1 | # Device Detector - The Universal Device Detection library for parsing User Agents 2 | 3 | > @link https://matomo.org 4 | > @license http://www.gnu.org/licenses/lgpl.html LGPL v3 or lat 5 | 6 | [bots.ts](./bots.ts) is based on matomo bots.yml file. You can see the original version here [here](https://raw.githubusercontent.com/matomo-org/device-detector/master/regexes/bots.yml). 7 | -------------------------------------------------------------------------------- /apps/api/src/bots/index.ts: -------------------------------------------------------------------------------- 1 | import bots from './bots'; 2 | 3 | export function isBot(ua: string) { 4 | const res = bots.find((bot) => { 5 | if (new RegExp(bot.regex).test(ua)) { 6 | return true; 7 | } 8 | return false; 9 | }); 10 | 11 | if (!res) { 12 | return null; 13 | } 14 | 15 | return { 16 | name: res.name, 17 | type: 'category' in res ? res.category : 'Unknown', 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /apps/api/src/hooks/client.hook.ts: -------------------------------------------------------------------------------- 1 | import { SdkAuthError, validateSdkRequest } from '@/utils/auth'; 2 | import type { PostEventPayload, TrackHandlerPayload } from '@openpanel/sdk'; 3 | import type { FastifyReply, FastifyRequest } from 'fastify'; 4 | 5 | export async function clientHook( 6 | req: FastifyRequest<{ 7 | Body: PostEventPayload | TrackHandlerPayload; 8 | }>, 9 | reply: FastifyReply, 10 | ) { 11 | try { 12 | const client = await validateSdkRequest(req); 13 | req.client = client; 14 | } catch (error) { 15 | if (error instanceof SdkAuthError) { 16 | req.log.warn('Invalid SDK request', error); 17 | return reply.status(401).send(error.message); 18 | } 19 | 20 | req.log.error('Invalid SDK request', error); 21 | return reply.status(500).send('Internal server error'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/api/src/hooks/fix.hook.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyRequest } from 'fastify'; 2 | 3 | export async function fixHook(request: FastifyRequest) { 4 | const ua = request.headers['user-agent']; 5 | // Swift SDK issue: https://github.com/Openpanel-dev/swift-sdk/commit/d588fa761a36a33f3b78eb79d83bfd524e3c7144 6 | if (ua) { 7 | const regex = /OpenPanel\/(\d+\.\d+\.\d+)\sOpenPanel\/(\d+\.\d+\.\d+)/; 8 | const match = ua.match(regex); 9 | if (match) { 10 | request.headers['user-agent'] = ua.replace( 11 | regex, 12 | `OpenPanel/${match[1]}`, 13 | ); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/api/src/hooks/ip.hook.ts: -------------------------------------------------------------------------------- 1 | import { getClientIp } from '@/utils/parse-ip'; 2 | import type { 3 | FastifyReply, 4 | FastifyRequest, 5 | HookHandlerDoneFunction, 6 | } from 'fastify'; 7 | 8 | export async function ipHook(request: FastifyRequest) { 9 | const ip = getClientIp(request); 10 | if (ip) { 11 | request.clientIp = ip; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /apps/api/src/hooks/request-id.hook.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | FastifyReply, 3 | FastifyRequest, 4 | HookHandlerDoneFunction, 5 | } from 'fastify'; 6 | 7 | export async function requestIdHook(request: FastifyRequest) { 8 | if (!request.headers['request-id']) { 9 | request.headers['request-id'] = request.id; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/api/src/hooks/timestamp.hook.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyRequest } from 'fastify'; 2 | 3 | export async function timestampHook(request: FastifyRequest) { 4 | request.timestamp = Date.now(); 5 | } 6 | -------------------------------------------------------------------------------- /apps/api/src/referrers/referrers.readme.md: -------------------------------------------------------------------------------- 1 | # Snowplow Referer Parser 2 | 3 | The file index.ts in this dir is generated from snowplows referer database [Snowplow Referer Parser](https://github.com/snowplow-referer-parser/referer-parser). 4 | 5 | The orginal [referers.yml](https://github.com/snowplow-referer-parser/referer-parser/blob/master/resources/referers.yml) is based on Piwik's SearchEngines.php and Socials.php, copyright 2012 Matthieu Aubry and available under the GNU General Public License v3. 6 | -------------------------------------------------------------------------------- /apps/api/src/routes/ai.router.ts: -------------------------------------------------------------------------------- 1 | import * as controller from '@/controllers/ai.controller'; 2 | import { activateRateLimiter } from '@/utils/rate-limiter'; 3 | import type { FastifyPluginCallback, FastifyRequest } from 'fastify'; 4 | 5 | const aiRouter: FastifyPluginCallback = async (fastify) => { 6 | await activateRateLimiter< 7 | FastifyRequest<{ 8 | Querystring: { 9 | projectId: string; 10 | }; 11 | }> 12 | >({ 13 | fastify, 14 | max: process.env.NODE_ENV === 'production' ? 20 : 100, 15 | timeWindow: '300 seconds', 16 | keyGenerator: (req) => { 17 | return req.query.projectId; 18 | }, 19 | }); 20 | 21 | fastify.route({ 22 | method: 'POST', 23 | url: '/chat', 24 | handler: controller.chat, 25 | }); 26 | }; 27 | 28 | export default aiRouter; 29 | -------------------------------------------------------------------------------- /apps/api/src/routes/event.router.ts: -------------------------------------------------------------------------------- 1 | import * as controller from '@/controllers/event.controller'; 2 | import type { FastifyPluginCallback } from 'fastify'; 3 | 4 | import { clientHook } from '@/hooks/client.hook'; 5 | import { isBotHook } from '@/hooks/is-bot.hook'; 6 | 7 | const eventRouter: FastifyPluginCallback = async (fastify) => { 8 | fastify.addHook('preHandler', clientHook); 9 | fastify.addHook('preHandler', isBotHook); 10 | 11 | fastify.route({ 12 | method: 'POST', 13 | url: '/', 14 | handler: controller.postEvent, 15 | }); 16 | }; 17 | 18 | export default eventRouter; 19 | -------------------------------------------------------------------------------- /apps/api/src/routes/misc.router.ts: -------------------------------------------------------------------------------- 1 | import * as controller from '@/controllers/misc.controller'; 2 | import type { FastifyPluginCallback } from 'fastify'; 3 | 4 | const miscRouter: FastifyPluginCallback = async (fastify) => { 5 | fastify.route({ 6 | method: 'POST', 7 | url: '/ping', 8 | handler: controller.ping, 9 | }); 10 | 11 | fastify.route({ 12 | method: 'GET', 13 | url: '/stats', 14 | handler: controller.stats, 15 | }); 16 | 17 | fastify.route({ 18 | method: 'GET', 19 | url: '/favicon', 20 | handler: controller.getFavicon, 21 | }); 22 | 23 | fastify.route({ 24 | method: 'GET', 25 | url: '/favicon/clear', 26 | handler: controller.clearFavicons, 27 | }); 28 | }; 29 | 30 | export default miscRouter; 31 | -------------------------------------------------------------------------------- /apps/api/src/routes/oauth-callback.router.ts: -------------------------------------------------------------------------------- 1 | import * as controller from '@/controllers/oauth-callback.controller'; 2 | import type { FastifyPluginCallback } from 'fastify'; 3 | 4 | const router: FastifyPluginCallback = async (fastify) => { 5 | fastify.route({ 6 | method: 'GET', 7 | url: '/github/callback', 8 | handler: controller.githubCallback, 9 | }); 10 | fastify.route({ 11 | method: 'GET', 12 | url: '/google/callback', 13 | handler: controller.googleCallback, 14 | }); 15 | }; 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /apps/api/src/routes/profile.router.ts: -------------------------------------------------------------------------------- 1 | import * as controller from '@/controllers/profile.controller'; 2 | import { clientHook } from '@/hooks/client.hook'; 3 | import { isBotHook } from '@/hooks/is-bot.hook'; 4 | import type { FastifyPluginCallback } from 'fastify'; 5 | 6 | const eventRouter: FastifyPluginCallback = async (fastify) => { 7 | fastify.addHook('preHandler', clientHook); 8 | fastify.addHook('preHandler', isBotHook); 9 | 10 | fastify.route({ 11 | method: 'POST', 12 | url: '/', 13 | handler: controller.updateProfile, 14 | }); 15 | 16 | fastify.route({ 17 | method: 'POST', 18 | url: '/increment', 19 | handler: controller.incrementProfileProperty, 20 | }); 21 | 22 | fastify.route({ 23 | method: 'POST', 24 | url: '/decrement', 25 | handler: controller.decrementProfileProperty, 26 | }); 27 | }; 28 | 29 | export default eventRouter; 30 | -------------------------------------------------------------------------------- /apps/api/src/routes/webhook.router.ts: -------------------------------------------------------------------------------- 1 | import * as controller from '@/controllers/webhook.controller'; 2 | import type { FastifyPluginCallback } from 'fastify'; 3 | 4 | const webhookRouter: FastifyPluginCallback = async (fastify) => { 5 | fastify.route({ 6 | method: 'GET', 7 | url: '/slack', 8 | handler: controller.slackWebhook, 9 | }); 10 | fastify.route({ 11 | method: 'POST', 12 | url: '/polar', 13 | handler: controller.polarWebhook, 14 | config: { 15 | rawBody: true, 16 | }, 17 | }); 18 | }; 19 | 20 | export default webhookRouter; 21 | -------------------------------------------------------------------------------- /apps/api/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger } from '@openpanel/logger'; 2 | 3 | export const logger = createLogger({ name: 'api' }); 4 | -------------------------------------------------------------------------------- /apps/api/src/utils/parse-zod-query-string.ts: -------------------------------------------------------------------------------- 1 | import { getSafeJson } from '@openpanel/json'; 2 | 3 | export const parseQueryString = (obj: Record): any => { 4 | return Object.fromEntries( 5 | Object.entries(obj).map(([k, v]) => { 6 | if (typeof v === 'object') return [k, parseQueryString(v)]; 7 | if ( 8 | /^-?[0-9]+(\.[0-9]+)?$/i.test(v) && 9 | !Number.isNaN(Number.parseFloat(v)) 10 | ) 11 | return [k, Number.parseFloat(v)]; 12 | if (v === 'true') return [k, true]; 13 | if (v === 'false') return [k, false]; 14 | if (typeof v === 'string') { 15 | if (getSafeJson(v) !== null) { 16 | return [k, getSafeJson(v)]; 17 | } 18 | return [k, v]; 19 | } 20 | return [k, null]; 21 | }), 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@openpanel/tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./src/*"] 7 | }, 8 | "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" 9 | }, 10 | "include": ["."], 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/api/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | import type { Options } from 'tsup'; 3 | 4 | const options: Options = { 5 | clean: true, 6 | entry: ['src/index.ts'], 7 | noExternal: [/^@openpanel\/.*$/u, /^@\/.*$/u], 8 | external: [ 9 | '@hyperdx/node-opentelemetry', 10 | 'winston', 11 | '@node-rs/argon2', 12 | 'bcrypt', 13 | ], 14 | ignoreWatch: ['../../**/{.git,node_modules}/**'], 15 | sourcemap: true, 16 | splitting: false, 17 | }; 18 | 19 | if (process.env.WATCH) { 20 | options.watch = ['src/**/*', '../../packages/**/*']; 21 | 22 | options.onSuccess = 'node dist/index.js'; 23 | options.minify = false; 24 | } 25 | 26 | export default defineConfig(options); 27 | -------------------------------------------------------------------------------- /apps/dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # database 12 | /prisma/db.sqlite 13 | /prisma/db.sqlite-journal 14 | 15 | # next.js 16 | /.next/ 17 | /out/ 18 | next-env.d.ts 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | 30 | # local env files 31 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables 32 | .env 33 | .env*.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | -------------------------------------------------------------------------------- /apps/dashboard/.sentryclirc: -------------------------------------------------------------------------------- 1 | 2 | [auth] 3 | token=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 4 | -------------------------------------------------------------------------------- /apps/dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Dashboard 2 | -------------------------------------------------------------------------------- /apps/dashboard/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/utils/cn" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/dashboard/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /apps/dashboard/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Openpanel-dev/openpanel/39775142e20a688d9f9af23e7e39610646f77cc2/apps/dashboard/public/favicon.ico -------------------------------------------------------------------------------- /apps/dashboard/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import { pushModal } from '@/modals'; 5 | import { PlusIcon } from 'lucide-react'; 6 | 7 | export function HeaderDashboards() { 8 | return ( 9 |
10 |

Dashboards

11 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx: -------------------------------------------------------------------------------- 1 | import FullPageLoadingState from '@/components/full-page-loading-state'; 2 | import { Padding } from '@/components/ui/padding'; 3 | import withSuspense from '@/hocs/with-suspense'; 4 | 5 | import { getDashboardsByProjectId } from '@openpanel/db'; 6 | 7 | import { HeaderDashboards } from './header'; 8 | import { ListDashboards } from './list-dashboards'; 9 | 10 | interface Props { 11 | projectId: string; 12 | } 13 | 14 | const ListDashboardsServer = async ({ projectId }: Props) => { 15 | const dashboards = await getDashboardsByProjectId(projectId); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default withSuspense(ListDashboardsServer, FullPageLoadingState); 26 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx: -------------------------------------------------------------------------------- 1 | import ListDashboardsServer from './list-dashboards'; 2 | 3 | interface PageProps { 4 | params: { 5 | projectId: string; 6 | }; 7 | } 8 | 9 | export default function Page({ params: { projectId } }: PageProps) { 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-content.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/utils/cn'; 4 | import { useSelectedLayoutSegments } from 'next/navigation'; 5 | 6 | const NOT_MIGRATED_PAGES = ['reports']; 7 | 8 | export default function LayoutContent({ 9 | children, 10 | }: { 11 | children: React.ReactNode; 12 | }) { 13 | const segments = useSelectedLayoutSegments(); 14 | 15 | if (segments[0] && NOT_MIGRATED_PAGES.includes(segments[0])) { 16 | return
{children}
; 17 | } 18 | 19 | return ( 20 |
26 | {children} 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utils/cn'; 2 | 3 | interface StickyBelowHeaderProps { 4 | children: React.ReactNode; 5 | className?: string; 6 | } 7 | 8 | export function StickyBelowHeader({ 9 | children, 10 | className, 11 | }: StickyBelowHeaderProps) { 12 | return ( 13 |
19 | {children} 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx: -------------------------------------------------------------------------------- 1 | interface PageLayoutProps { 2 | title: React.ReactNode; 3 | } 4 | 5 | function PageLayout({ title }: PageLayoutProps) { 6 | return ( 7 | <> 8 |
9 |
{title}
10 |
11 | 12 | ); 13 | } 14 | 15 | export default PageLayout; 16 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/index.tsx: -------------------------------------------------------------------------------- 1 | import withLoadingWidget from '@/hocs/with-loading-widget'; 2 | import { escape } from 'sqlstring'; 3 | 4 | import { TABLE_NAMES, chQuery } from '@openpanel/db'; 5 | 6 | import PopularRoutes from './popular-routes'; 7 | 8 | type Props = { 9 | projectId: string; 10 | profileId: string; 11 | }; 12 | 13 | const PopularRoutesServer = async ({ projectId, profileId }: Props) => { 14 | const data = await chQuery<{ count: number; path: string }>( 15 | `SELECT count(*) as count, path FROM ${TABLE_NAMES.events} WHERE name = 'screen_view' AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY path ORDER BY count DESC`, 16 | ); 17 | return ; 18 | }; 19 | 20 | export default withLoadingWidget(PopularRoutesServer); 21 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/index.tsx: -------------------------------------------------------------------------------- 1 | import withLoadingWidget from '@/hocs/with-loading-widget'; 2 | import { escape } from 'sqlstring'; 3 | 4 | import { TABLE_NAMES, chQuery } from '@openpanel/db'; 5 | 6 | import ProfileActivity from './profile-activity'; 7 | 8 | type Props = { 9 | projectId: string; 10 | profileId: string; 11 | }; 12 | 13 | const ProfileActivityServer = async ({ projectId, profileId }: Props) => { 14 | const data = await chQuery<{ count: number; date: string }>( 15 | `SELECT count(*) as count, toStartOfDay(created_at) as date FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY date ORDER BY date DESC`, 16 | ); 17 | return ; 18 | }; 19 | 20 | export default withLoadingWidget(ProfileActivityServer); 21 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx: -------------------------------------------------------------------------------- 1 | import withSuspense from '@/hocs/with-suspense'; 2 | 3 | import type { IServiceProfile } from '@openpanel/db'; 4 | import { getProfileMetrics } from '@openpanel/db'; 5 | 6 | import ProfileMetrics from './profile-metrics'; 7 | 8 | type Props = { 9 | projectId: string; 10 | profile: IServiceProfile; 11 | }; 12 | 13 | const ProfileMetricsServer = async ({ projectId, profile }: Props) => { 14 | const data = await getProfileMetrics(profile.id, projectId); 15 | return ; 16 | }; 17 | 18 | export default withSuspense(ProfileMetricsServer, () => null); 19 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/index.tsx: -------------------------------------------------------------------------------- 1 | import { subMinutes } from 'date-fns'; 2 | import { escape } from 'sqlstring'; 3 | 4 | import { TABLE_NAMES, chQuery, formatClickhouseDate } from '@openpanel/db'; 5 | 6 | import type { Coordinate } from './coordinates'; 7 | import Map from './map'; 8 | 9 | type Props = { 10 | projectId: string; 11 | }; 12 | const RealtimeMap = async ({ projectId }: Props) => { 13 | const res = await chQuery( 14 | `SELECT DISTINCT city, longitude as long, latitude as lat FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND created_at >= '${formatClickhouseDate(subMinutes(new Date(), 30))}' ORDER BY created_at DESC`, 15 | ); 16 | 17 | return ; 18 | }; 19 | 20 | export default RealtimeMap; 21 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/index.tsx: -------------------------------------------------------------------------------- 1 | import { escape } from 'sqlstring'; 2 | 3 | import { TABLE_NAMES, getEvents } from '@openpanel/db'; 4 | 5 | import LiveEvents from './live-events'; 6 | 7 | type Props = { 8 | projectId: string; 9 | limit?: number; 10 | }; 11 | const RealtimeLiveEventsServer = async ({ projectId, limit = 30 }: Props) => { 12 | const events = await getEvents( 13 | `SELECT * FROM ${TABLE_NAMES.events} WHERE created_at > now() - INTERVAL 2 HOUR AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT ${limit}`, 14 | { 15 | profile: true, 16 | }, 17 | ); 18 | return ; 19 | }; 20 | 21 | export default RealtimeLiveEventsServer; 22 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-reloader.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import useWS from '@/hooks/useWS'; 4 | import { useQueryClient } from '@tanstack/react-query'; 5 | import { useRouter } from 'next/navigation'; 6 | 7 | type Props = { 8 | projectId: string; 9 | }; 10 | 11 | const RealtimeReloader = ({ projectId }: Props) => { 12 | const client = useQueryClient(); 13 | const router = useRouter(); 14 | 15 | useWS( 16 | `/live/events/${projectId}`, 17 | () => { 18 | if (!document.hidden) { 19 | client.refetchQueries({ 20 | type: 'active', 21 | }); 22 | } 23 | }, 24 | { 25 | debounce: { 26 | maxWait: 60000, 27 | delay: 60000, 28 | }, 29 | }, 30 | ); 31 | 32 | return null; 33 | }; 34 | 35 | export default RealtimeReloader; 36 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx: -------------------------------------------------------------------------------- 1 | import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; 2 | import EditReportName from '@/components/report/edit-report-name'; 3 | import { notFound } from 'next/navigation'; 4 | 5 | import { getReportById } from '@openpanel/db'; 6 | 7 | import ReportEditor from '../report-editor'; 8 | 9 | interface PageProps { 10 | params: { 11 | projectId: string; 12 | reportId: string; 13 | }; 14 | } 15 | 16 | export default async function Page({ params: { reportId } }: PageProps) { 17 | const report = await getReportById(reportId); 18 | 19 | if (!report) { 20 | return notFound(); 21 | } 22 | 23 | return ( 24 | <> 25 | } /> 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx: -------------------------------------------------------------------------------- 1 | import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; 2 | import EditReportName from '@/components/report/edit-report-name'; 3 | 4 | import ReportEditor from './report-editor'; 5 | 6 | export default function Page() { 7 | return ( 8 | <> 9 | } /> 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/index.tsx: -------------------------------------------------------------------------------- 1 | import { Widget, WidgetHead } from '@/components/widget'; 2 | import withLoadingWidget from '@/hocs/with-loading-widget'; 3 | 4 | import { getRetentionLastSeenSeries } from '@openpanel/db'; 5 | 6 | import Chart from './chart'; 7 | 8 | type Props = { 9 | projectId: string; 10 | }; 11 | 12 | const LastActiveUsersServer = async ({ projectId }: Props) => { 13 | const res = await getRetentionLastSeenSeries({ projectId }); 14 | 15 | return ( 16 | 17 | 18 | Last time in days a user was active 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default withLoadingWidget(LastActiveUsersServer); 26 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/index.tsx: -------------------------------------------------------------------------------- 1 | import { Widget, WidgetHead } from '@/components/widget'; 2 | import withLoadingWidget from '@/hocs/with-loading-widget'; 3 | 4 | import { getRetentionSeries } from '@openpanel/db'; 5 | 6 | import Chart from './chart'; 7 | 8 | type Props = { 9 | projectId: string; 10 | }; 11 | 12 | const UsersRetentionSeries = async ({ projectId }: Props) => { 13 | const res = await getRetentionSeries({ projectId }); 14 | 15 | return ( 16 | 17 | 18 | Stickyness / Retention (%) 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default withLoadingWidget(UsersRetentionSeries); 26 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/loading.tsx: -------------------------------------------------------------------------------- 1 | import FullPageLoadingState from '@/components/full-page-loading-state'; 2 | 3 | export default FullPageLoadingState; 4 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/loading.tsx: -------------------------------------------------------------------------------- 1 | import FullPageLoadingState from '@/components/full-page-loading-state'; 2 | 3 | export default FullPageLoadingState; 4 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/members/index.tsx: -------------------------------------------------------------------------------- 1 | import { MembersTable } from '@/components/settings/members'; 2 | 3 | import { getMembers, getProjectsByOrganizationId } from '@openpanel/db'; 4 | 5 | interface Props { 6 | organizationId: string; 7 | } 8 | 9 | const MembersServer = async ({ organizationId }: Props) => { 10 | const [members, projects] = await Promise.all([ 11 | getMembers(organizationId), 12 | getProjectsByOrganizationId(organizationId), 13 | ]); 14 | 15 | return ; 16 | }; 17 | 18 | export default MembersServer; 19 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/organization.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import type { IServiceOrganization } from '@openpanel/db'; 4 | import EditOrganization from './edit-organization'; 5 | 6 | interface OrganizationProps { 7 | organization: IServiceOrganization; 8 | } 9 | export default function Organization({ organization }: OrganizationProps) { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/page.tsx: -------------------------------------------------------------------------------- 1 | export { default } from './organization/page'; 2 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/loading.tsx: -------------------------------------------------------------------------------- 1 | import FullPageLoadingState from '@/components/full-page-loading-state'; 2 | 3 | export default FullPageLoadingState; 4 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/logout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import SignOutButton from '@/components/sign-out-button'; 4 | import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; 5 | 6 | export function Logout() { 7 | return ( 8 | 9 | 10 | Sad part 11 | 12 | 13 |

Sometimes you need to go. See you next time

14 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { Padding } from '@/components/ui/padding'; 2 | import { auth } from '@openpanel/auth/nextjs'; 3 | import { getUserById } from '@openpanel/db'; 4 | 5 | import EditProfile from './edit-profile'; 6 | 7 | export default async function Page() { 8 | const { userId } = await auth(); 9 | const profile = await getUserById(userId!); 10 | 11 | return ( 12 | 13 |

Profile

14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/loading.tsx: -------------------------------------------------------------------------------- 1 | import FullPageLoadingState from '@/components/full-page-loading-state'; 2 | 3 | export default FullPageLoadingState; 4 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/loading.tsx: -------------------------------------------------------------------------------- 1 | import FullPageLoadingState from '@/components/full-page-loading-state'; 2 | 3 | export default FullPageLoadingState; 4 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx: -------------------------------------------------------------------------------- 1 | import { getReferences } from '@openpanel/db'; 2 | 3 | import ListReferences from './list-references'; 4 | 5 | interface PageProps { 6 | params: { 7 | projectId: string; 8 | }; 9 | } 10 | 11 | export default async function Page({ params: { projectId } }: PageProps) { 12 | const references = await getReferences({ 13 | where: { 14 | projectId, 15 | }, 16 | take: 50, 17 | skip: 0, 18 | }); 19 | 20 | return ( 21 | <> 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(app)/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | 3 | import { auth } from '@openpanel/auth/nextjs'; 4 | import { getOrganizations } from '@openpanel/db'; 5 | 6 | export default async function Page() { 7 | const { userId } = await auth(); 8 | const organizations = await getOrganizations(userId); 9 | 10 | if (organizations.length > 0) { 11 | return redirect(`/${organizations[0]?.id}`); 12 | } 13 | 14 | return redirect('/onboarding/project'); 15 | } 16 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import LiveEventsServer from './live-events'; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | }; 6 | 7 | const Page = ({ children }: Props) => { 8 | return ( 9 | <> 10 |
11 |
12 |
13 | 14 |
15 |
{children}
16 |
17 |
18 | 19 | ); 20 | }; 21 | 22 | export default Page; 23 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(auth)/live-events/index.tsx: -------------------------------------------------------------------------------- 1 | import LiveEvents from './live-events'; 2 | 3 | const LiveEventsServer = () => { 4 | return ; 5 | }; 6 | 7 | export default LiveEventsServer; 8 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(auth)/reset-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ResetPasswordForm } from '@/components/auth/reset-password-form'; 2 | import { auth } from '@openpanel/auth/nextjs'; 3 | import { redirect } from 'next/navigation'; 4 | 5 | export default async function Page() { 6 | const session = await auth(); 7 | 8 | if (session.userId) { 9 | return redirect('/'); 10 | } 11 | 12 | return ( 13 |
14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/(onboarding)/onboarding/project/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from '@openpanel/auth/nextjs'; 2 | import { getOrganizations } from '@openpanel/db'; 3 | import { OnboardingCreateProject } from './onboarding-create-project'; 4 | 5 | const Page = async () => { 6 | const { userId } = await auth(); 7 | const organizations = await getOrganizations(userId); 8 | return ; 9 | }; 10 | 11 | export default Page; 12 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/api/headers/route.ts: -------------------------------------------------------------------------------- 1 | export const runtime = 'edge'; 2 | export const dynamic = 'force-dynamic'; // no caching 3 | 4 | export async function GET(request: Request) { 5 | const headers = Object.fromEntries(request.headers.entries()); 6 | return Response.json({ headers, region: process.env.VERCEL_REGION }); 7 | } 8 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Openpanel-dev/openpanel/39775142e20a688d9f9af23e7e39610646f77cc2/apps/dashboard/src/app/favicon.ico -------------------------------------------------------------------------------- /apps/dashboard/src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | 5 | export default function GlobalError({ 6 | error, 7 | }: { 8 | error: Error & { digest?: string }; 9 | reset: () => void; 10 | }) { 11 | useEffect(() => {}, [error]); 12 | 13 | return ( 14 | 15 | 16 |

Something went wrong

17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/manifest.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from 'next'; 2 | 3 | export const dynamic = 'static'; 4 | 5 | export default function manifest(): MetadataRoute.Manifest { 6 | return { 7 | id: process.env.NEXT_PUBLIC_DASHBOARD_URL, 8 | name: 'Openpanel.dev', 9 | short_name: 'Openpanel.dev', 10 | description: '', 11 | start_url: '/', 12 | display: 'standalone', 13 | background_color: '#fff', 14 | theme_color: '#fff', 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /apps/dashboard/src/app/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / -------------------------------------------------------------------------------- /apps/dashboard/src/components/animate-height.tsx: -------------------------------------------------------------------------------- 1 | import ReactAnimateHeight from 'react-animate-height'; 2 | 3 | type Props = { 4 | children: React.ReactNode; 5 | className?: string; 6 | open: boolean; 7 | }; 8 | 9 | const AnimateHeight = ({ children, className, open }: Props) => { 10 | return ( 11 | 16 | {children} 17 | 18 | ); 19 | }; 20 | 21 | export default AnimateHeight; 22 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/auth/or.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utils/cn'; 2 | 3 | export function Or({ className }: { className?: string }) { 4 | return ( 5 |
6 |
7 | OR 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/button-container.tsx: -------------------------------------------------------------------------------- 1 | import type { HtmlProps } from '@/types'; 2 | import { cn } from '@/utils/cn'; 3 | 4 | export function ButtonContainer({ 5 | className, 6 | ...props 7 | }: HtmlProps) { 8 | return ( 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/click-to-copy.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { clipboard } from '@/utils/clipboard'; 4 | import { toast } from 'sonner'; 5 | 6 | import { Tooltiper } from './ui/tooltip'; 7 | 8 | type Props = { 9 | children: React.ReactNode; 10 | className?: string; 11 | value: string; 12 | }; 13 | 14 | const ClickToCopy = ({ children, value }: Props) => { 15 | return ( 16 | { 21 | clipboard(value); 22 | toast('Copied to clipboard'); 23 | }} 24 | > 25 | {children} 26 | 27 | ); 28 | }; 29 | 30 | export default ClickToCopy; 31 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/color-square.tsx: -------------------------------------------------------------------------------- 1 | import type { HtmlProps } from '@/types'; 2 | import { cn } from '@/utils/cn'; 3 | 4 | type ColorSquareProps = HtmlProps; 5 | 6 | export function ColorSquare({ children, className }: ColorSquareProps) { 7 | return ( 8 |
14 | {children} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/fade-in.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/utils/cn'; 4 | import { useEffect, useRef } from 'react'; 5 | 6 | type Props = { 7 | className?: string; 8 | children: React.ReactNode; 9 | }; 10 | 11 | export function FadeIn({ className, children }: Props) { 12 | const ref = useRef(null); 13 | useEffect(() => { 14 | if (ref.current) { 15 | ref.current.classList.remove('opacity-0'); 16 | ref.current.classList.add('opacity-100'); 17 | } 18 | }, []); 19 | return ( 20 |
24 | {children} 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/forms/copy-input.tsx: -------------------------------------------------------------------------------- 1 | import { clipboard } from '@/utils/clipboard'; 2 | import { cn } from '@/utils/cn'; 3 | import { CopyIcon } from 'lucide-react'; 4 | 5 | import { Label } from '../ui/label'; 6 | 7 | type Props = { 8 | label: React.ReactNode; 9 | value: string; 10 | className?: string; 11 | }; 12 | 13 | const CopyInput = ({ label, value, className }: Props) => { 14 | return ( 15 | 26 | ); 27 | }; 28 | 29 | export default CopyInput; 30 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/full-page-loading-state.tsx: -------------------------------------------------------------------------------- 1 | import type { LucideIcon } from 'lucide-react'; 2 | import { Loader2Icon } from 'lucide-react'; 3 | 4 | import { FullPageEmptyState } from './full-page-empty-state'; 5 | 6 | const FullPageLoadingState = () => { 7 | return ( 8 | ( 13 | 14 | )) as LucideIcon 15 | } 16 | > 17 | Wait a moment while we fetch your dashboards 18 | 19 | ); 20 | }; 21 | 22 | export default FullPageLoadingState; 23 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/full-width-navbar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { cn } from '@/utils/cn'; 4 | 5 | import { Logo, LogoSquare } from './logo'; 6 | 7 | type Props = { 8 | children: React.ReactNode; 9 | className?: string; 10 | }; 11 | 12 | const FullWidthNavbar = ({ children, className }: Props) => { 13 | return ( 14 |
15 |
16 | 17 | {children} 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default FullWidthNavbar; 24 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/links.tsx: -------------------------------------------------------------------------------- 1 | import { useAppParams } from '@/hooks/useAppParams'; 2 | import type { LinkProps } from 'next/link'; 3 | import Link from 'next/link'; 4 | 5 | export function ProjectLink({ 6 | children, 7 | ...props 8 | }: LinkProps & { 9 | children: React.ReactNode; 10 | className?: string; 11 | title?: string; 12 | }) { 13 | const { organizationId, projectId } = useAppParams(); 14 | if (typeof props.href === 'string') { 15 | return ( 16 | 24 | {children} 25 | 26 | ); 27 | } 28 | 29 | return

ProjectLink

; 30 | } 31 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/logo.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utils/cn'; 2 | 3 | interface LogoProps { 4 | className?: string; 5 | } 6 | 7 | export function LogoSquare({ className }: LogoProps) { 8 | return ( 9 | Openpanel logo 14 | ); 15 | } 16 | 17 | export function Logo({ className }: LogoProps) { 18 | return ( 19 |
22 | 23 | openpanel.dev 24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/notifications/notification-provider.tsx: -------------------------------------------------------------------------------- 1 | import { useAppParams } from '@/hooks/useAppParams'; 2 | import useWS from '@/hooks/useWS'; 3 | import type { Notification } from '@openpanel/db'; 4 | import { BellIcon } from 'lucide-react'; 5 | import { toast } from 'sonner'; 6 | 7 | export function NotificationProvider() { 8 | const { projectId } = useAppParams(); 9 | 10 | if (!projectId) return null; 11 | 12 | return ; 13 | } 14 | 15 | export function InnerNotificationProvider({ 16 | projectId, 17 | }: { projectId: string }) { 18 | useWS(`/live/notifications/${projectId}`, (notification) => { 19 | toast(notification.title, { 20 | description: notification.message, 21 | icon: , 22 | }); 23 | }); 24 | 25 | return null; 26 | } 27 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/notifications/notifications.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useAppParams } from '@/hooks/useAppParams'; 4 | import { api } from '@/trpc/client'; 5 | import { NotificationsTable } from './table'; 6 | 7 | export function Notifications() { 8 | const { projectId } = useAppParams(); 9 | const query = api.notification.list.useQuery({ 10 | projectId, 11 | }); 12 | 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/overview/live-counter/index.tsx: -------------------------------------------------------------------------------- 1 | import withSuspense from '@/hocs/with-suspense'; 2 | 3 | import { eventBuffer } from '@openpanel/db'; 4 | 5 | import type { LiveCounterProps } from './live-counter'; 6 | import LiveCounter from './live-counter'; 7 | 8 | async function ServerLiveCounter(props: Omit) { 9 | const count = await eventBuffer.getActiveVisitorCount(props.projectId); 10 | return ; 11 | } 12 | 13 | export default withSuspense(ServerLiveCounter, () =>
); 14 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/overview/overview-chart-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { BarChartIcon, LineChartIcon } from 'lucide-react'; 2 | import type { Dispatch, SetStateAction } from 'react'; 3 | 4 | import type { IChartType } from '@openpanel/validation'; 5 | 6 | import { Button } from '../ui/button'; 7 | 8 | interface Props { 9 | chartType: IChartType; 10 | setChartType: Dispatch>; 11 | } 12 | export function OverviewChartToggle({ chartType, setChartType }: Props) { 13 | return ( 14 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/overview/overview-details-button.tsx: -------------------------------------------------------------------------------- 1 | import { ScanEyeIcon } from 'lucide-react'; 2 | 3 | import { Button, type ButtonProps } from '../ui/button'; 4 | 5 | type Props = Omit; 6 | 7 | const OverviewDetailsButton = (props: Props) => { 8 | return ( 9 | 12 | ); 13 | }; 14 | 15 | export default OverviewDetailsButton; 16 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/overview/overview-hydrate-options.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { getStorageItem } from '@/utils/storage'; 4 | import { useEffect, useRef } from 'react'; 5 | import { useOverviewOptions } from './useOverviewOptions'; 6 | 7 | export function OverviewHydrateOptions() { 8 | const { setRange, range } = useOverviewOptions(); 9 | const ref = useRef(false); 10 | 11 | useEffect(() => { 12 | if (!ref.current) { 13 | const range = getStorageItem('range', '7d'); 14 | if (range !== '7d') { 15 | setRange(range); 16 | } 17 | ref.current = true; 18 | } 19 | }, []); 20 | 21 | return null; 22 | } 23 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/overview/overview-range.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; 4 | import { TimeWindowPicker } from '@/components/time-window-picker'; 5 | 6 | export function OverviewRange() { 7 | const { range, setRange, setStartDate, setEndDate, endDate, startDate } = 8 | useOverviewOptions(); 9 | 10 | return ( 11 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/overview/overview-share/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | import withSuspense from '@/hocs/with-suspense'; 3 | 4 | import { getShareByProjectId } from '@openpanel/db'; 5 | 6 | import { OverviewShare } from './overview-share'; 7 | 8 | type Props = { 9 | projectId: string; 10 | }; 11 | 12 | const OverviewShareServer = async ({ projectId }: Props) => { 13 | const share = await getShareByProjectId(projectId); 14 | return ; 15 | }; 16 | 17 | export default withSuspense(OverviewShareServer, () => ( 18 | 19 | )); 20 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/overview/overview-top-events/index.tsx: -------------------------------------------------------------------------------- 1 | import { getConversionEventNames } from '@openpanel/db'; 2 | 3 | import type { OverviewTopEventsProps } from './overview-top-events'; 4 | import OverviewTopEvents from './overview-top-events'; 5 | 6 | export default async function OverviewTopEventsServer({ 7 | projectId, 8 | }: Omit) { 9 | const eventNames = await getConversionEventNames(projectId); 10 | return ( 11 | item.name)} 14 | /> 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/ping.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utils/cn'; 2 | import { Badge } from './ui/badge'; 3 | 4 | export function Ping({ className }: { className?: string }) { 5 | return ( 6 |
7 |
8 |
14 |
15 | ); 16 | } 17 | 18 | export function PingBadge({ 19 | children, 20 | className, 21 | }: { children: React.ReactNode; className?: string }) { 22 | return ( 23 | 24 | 25 | {children} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/react-virtualized-auto-sizer.tsx: -------------------------------------------------------------------------------- 1 | import AutoSizer from 'react-virtualized-auto-sizer'; 2 | 3 | export { AutoSizer }; 4 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/references/table.tsx: -------------------------------------------------------------------------------- 1 | import { formatDate, formatDateTime } from '@/utils/date'; 2 | import type { ColumnDef } from '@tanstack/react-table'; 3 | 4 | import type { IServiceReference } from '@openpanel/db'; 5 | 6 | export const columns: ColumnDef[] = [ 7 | { 8 | accessorKey: 'title', 9 | header: 'Title', 10 | }, 11 | { 12 | accessorKey: 'date', 13 | header: 'Date', 14 | cell({ row }) { 15 | const date = row.original.date; 16 | return
{formatDateTime(date)}
; 17 | }, 18 | }, 19 | { 20 | accessorKey: 'createdAt', 21 | header: 'Created at', 22 | cell({ row }) { 23 | const date = row.original.createdAt; 24 | return
{formatDate(date)}
; 25 | }, 26 | }, 27 | ]; 28 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/report-chart/common/error.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utils/cn'; 2 | import { ServerCrashIcon } from 'lucide-react'; 3 | import { useReportChartContext } from '../context'; 4 | 5 | export function ReportChartError() { 6 | const { isEditMode } = useReportChartContext(); 7 | return ( 8 |
14 | 18 |
19 | There was an error loading this chart. 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/report/sidebar/ReportFormula.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useDispatch, useSelector } from '@/redux'; 4 | 5 | import { InputEnter } from '@/components/ui/input-enter'; 6 | import { changeFormula } from '../reportSlice'; 7 | 8 | export function ReportFormula() { 9 | const formula = useSelector((state) => state.report.formula); 10 | const dispatch = useDispatch(); 11 | 12 | return ( 13 |
14 |

Formula

15 |
16 | { 20 | dispatch(changeFormula(value)); 21 | }} 22 | /> 23 |
24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/report/sidebar/filters/FiltersList.tsx: -------------------------------------------------------------------------------- 1 | import type { IChartEvent } from '@openpanel/validation'; 2 | 3 | import { FilterItem } from './FilterItem'; 4 | 5 | interface ReportEventFiltersProps { 6 | event: IChartEvent; 7 | } 8 | 9 | export function FiltersList({ event }: ReportEventFiltersProps) { 10 | return ( 11 |
12 |
13 | {event.filters.map((filter) => { 14 | return ; 15 | })} 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/sign-out-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { LogOutIcon } from 'lucide-react'; 4 | 5 | import { useLogout } from '@/hooks/useLogout'; 6 | import { Button } from './ui/button'; 7 | 8 | const SignOutButton = () => { 9 | const logout = useLogout(); 10 | return ( 11 | 14 | ); 15 | }; 16 | 17 | export default SignOutButton; 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utils/cn'; 2 | 3 | export function Skeleton({ className }: { className?: string }) { 4 | return
; 5 | } 6 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'; 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root; 6 | 7 | export { AspectRatio }; 8 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/ui/gradient-background.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utils/cn'; 2 | 3 | interface GradientBackgroundProps { 4 | children: React.ReactNode; 5 | className?: string; 6 | } 7 | 8 | export function GradientBackground({ 9 | children, 10 | className, 11 | ...props 12 | }: GradientBackgroundProps) { 13 | return ( 14 |
21 |
{children}
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/ui/input-with-toggle.tsx: -------------------------------------------------------------------------------- 1 | import type { Dispatch, SetStateAction } from 'react'; 2 | import AnimateHeight from '../animate-height'; 3 | import { Label } from './label'; 4 | import { Switch } from './switch'; 5 | 6 | type Props = { 7 | active: boolean; 8 | onActiveChange: (newValue: boolean) => void; 9 | label: string; 10 | children: React.ReactNode; 11 | }; 12 | 13 | export function InputWithToggle({ 14 | active, 15 | onActiveChange, 16 | label, 17 | children, 18 | }: Props) { 19 | return ( 20 |
21 |
22 | 23 | 24 |
25 | {children} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/ui/padding.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utils/cn'; 2 | 3 | const padding = 'p-4 lg:p-8'; 4 | 5 | export const Padding = ({ 6 | className, 7 | ...props 8 | }: React.HtmlHTMLAttributes) => { 9 | return
; 10 | }; 11 | 12 | export const Spacer = ({ 13 | className, 14 | ...props 15 | }: React.HtmlHTMLAttributes) => { 16 | return
; 17 | }; 18 | -------------------------------------------------------------------------------- /apps/dashboard/src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/utils/cn'; 2 | import * as React from 'react'; 3 | 4 | export type TextareaProps = React.TextareaHTMLAttributes; 5 | 6 | const Textarea = React.forwardRef( 7 | ({ className, ...props }, ref) => { 8 | return ( 9 |