├── .changeset ├── README.md └── config.json ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ ├── clickhouse │ │ └── action.yml │ ├── monorepo-setup │ │ └── action.yml │ └── pnpm │ │ └── action.yml └── workflows │ ├── backend.yml │ ├── e2e.yml │ ├── example-astro.yml │ ├── example-nextjs.yml │ ├── example-node.yml │ ├── example-php.yml │ ├── example-rsc.yml │ ├── example-svelte.yml │ ├── fly.yml │ ├── publish-docker-image.yml │ ├── qwik.yml │ └── shared.yml ├── .gitignore ├── .nvmrc ├── Dockerfile.Backend ├── Dockerfile.Frontend ├── LICENSE.md ├── README.md ├── docker-compose.yml ├── example ├── astro │ ├── .gitignore │ ├── astro.config.mjs │ ├── e2e │ │ └── index.spec.ts │ ├── package.json │ ├── public │ │ ├── favicon.svg │ │ └── progressively.min.js │ ├── src │ │ ├── components │ │ │ └── Page.tsx │ │ ├── env.d.ts │ │ └── pages │ │ │ └── index.astro │ └── tsconfig.json ├── nextjs │ ├── .env │ ├── .eslintrc.json │ ├── .gitignore │ ├── e2e │ │ ├── anonymous.spec.ts │ │ └── index.spec.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── anonymous.tsx │ │ ├── api │ │ │ └── hello.ts │ │ └── index.tsx │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── styles │ │ ├── Home.module.css │ │ └── globals.css │ └── tsconfig.json ├── node │ ├── .gitignore │ ├── e2e │ │ └── index.spec.ts │ ├── index.js │ └── package.json ├── php │ ├── .gitignore │ ├── composer.json │ ├── e2e │ │ └── index.spec.ts │ ├── index.php │ └── package.json ├── playwright-helpers │ ├── .gitignore │ ├── cmd.ts │ ├── index.ts │ ├── package.json │ └── playwright.config.ts ├── qwik │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .prettierignore │ ├── .vscode │ │ └── extensions.json │ ├── e2e │ │ └── index.spec.ts │ ├── package.json │ ├── public │ │ ├── favicon.svg │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── components │ │ │ ├── header │ │ │ │ ├── header.css │ │ │ │ └── header.tsx │ │ │ ├── icons │ │ │ │ └── qwik.tsx │ │ │ └── router-head │ │ │ │ └── router-head.tsx │ │ ├── entry.dev.tsx │ │ ├── entry.preview.tsx │ │ ├── entry.ssr.tsx │ │ ├── global.css │ │ ├── root.tsx │ │ └── routes │ │ │ ├── index.tsx │ │ │ └── service-worker.ts │ ├── tsconfig.json │ └── vite.config.ts ├── rsc │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── components │ │ │ └── FlaggedComponent.server.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── e2e │ │ └── index.spec.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ ├── tailwind.config.js │ └── tsconfig.json └── svelte │ ├── .eslintignore │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── e2e │ └── index.spec.ts │ ├── package.json │ ├── src │ ├── app.d.ts │ ├── app.html │ └── routes │ │ ├── +page.server.js │ │ └── +page.svelte │ ├── static │ └── favicon.png │ ├── svelte.config.js │ ├── tsconfig.json │ └── vite.config.ts ├── fly.backend.toml ├── fly.frontend.toml ├── package.json ├── packages ├── analytics │ ├── .gitignore │ ├── .npmignore │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ │ ├── index.ts │ │ ├── isInteractive.ts │ │ ├── setup-navigation-listeners.ts │ │ ├── setup-qualitative-tracking.ts │ │ ├── setup.ts │ │ ├── show-qualitative-analytics.ts │ │ ├── types.ts │ │ └── vendor │ │ │ └── finder.ts │ └── tsconfig.json ├── database │ ├── .env.example │ ├── .gitignore │ ├── cli │ │ └── generate-user.js │ ├── clickhouse-client.ts │ ├── index.ts │ ├── package.json │ ├── prisma │ │ ├── migrations │ │ │ ├── 20230504055128_init │ │ │ │ └── migration.sql │ │ │ ├── 20230504064533_add_link_issue_flag │ │ │ │ └── migration.sql │ │ │ ├── 20230504072038_revert_link_issue_flag │ │ │ │ └── migration.sql │ │ │ ├── 20230510061801_plans_preparation │ │ │ │ └── migration.sql │ │ │ ├── 20230510112956_default_created_at_plan │ │ │ │ └── migration.sql │ │ │ ├── 20230512131424_revert_projet_count_env_count │ │ │ │ └── migration.sql │ │ │ ├── 20230517050329_add_stripe_user │ │ │ │ └── migration.sql │ │ │ ├── 20230517051315_add_stripe_transaction │ │ │ │ └── migration.sql │ │ │ ├── 20230517053300_shift_customer_id │ │ │ │ └── migration.sql │ │ │ ├── 20230517072350_add_status_to_plans │ │ │ │ └── migration.sql │ │ │ ├── 20230809064348_hits_to_events │ │ │ │ └── migration.sql │ │ │ ├── 20230919092817_init │ │ │ │ └── migration.sql │ │ │ ├── 20230919093105_optional_flagid_envid_event │ │ │ │ └── migration.sql │ │ │ ├── 20231116132622_init │ │ │ │ └── migration.sql │ │ │ ├── 20231122074756_move_event_to_flaghit │ │ │ │ └── migration.sql │ │ │ ├── 20231122090859_add_metric_hits │ │ │ │ └── migration.sql │ │ │ ├── 20231123134350_remove_segment_for_now │ │ │ │ └── migration.sql │ │ │ ├── 20231124062547_remove_metrics_and_use_events │ │ │ │ └── migration.sql │ │ │ ├── 20231124063019_attach_event_to_env │ │ │ │ └── migration.sql │ │ │ ├── 20231124072400_add_name_to_env_event │ │ │ │ └── migration.sql │ │ │ ├── 20231124074557_add_browser_os_in_event │ │ │ │ └── migration.sql │ │ │ ├── 20231124095955_add_url_on_event_hit │ │ │ │ └── migration.sql │ │ │ ├── 20231128162118_init │ │ │ │ └── migration.sql │ │ │ ├── 20231205132221_secret_key_domain_in_env │ │ │ │ └── migration.sql │ │ │ ├── 20231218133031_add_funnel_table │ │ │ │ └── migration.sql │ │ │ ├── 20231218133359_add_funnel_entry │ │ │ │ └── migration.sql │ │ │ ├── 20240112143736_add_event_value_funnel_entry │ │ │ │ └── migration.sql │ │ │ ├── 20240207122522_remove_scheduling_as_it_is_now │ │ │ │ └── migration.sql │ │ │ ├── 20240207151224_remove_env_and_flagenv │ │ │ │ └── migration.sql │ │ │ ├── 20240207152225_add_flag_to_projects │ │ │ │ └── migration.sql │ │ │ ├── 20240207154952_add_status_to_flag_model │ │ │ │ └── migration.sql │ │ │ ├── 20240210055726_add_scheduling_to_strategy │ │ │ │ └── migration.sql │ │ │ ├── 20240212124204_add_session_table │ │ │ │ └── migration.sql │ │ │ ├── 20240212150238_add_visitor_id_to_session │ │ │ │ └── migration.sql │ │ │ ├── 20240212150645_add_project_to_session │ │ │ │ └── migration.sql │ │ │ ├── 20240215150559_add_viewport_to_event │ │ │ │ └── migration.sql │ │ │ ├── 20240306092651_remove_event_table_for_clickhouse │ │ │ │ └── migration.sql │ │ │ ├── 20240314091742_remove_flag_hits_from_prisma │ │ │ │ └── migration.sql │ │ │ ├── 20240606150502_init │ │ │ │ └── migration.sql │ │ │ ├── 20240606185705_add_event_usage │ │ │ │ └── migration.sql │ │ │ ├── 20240606190250_add_credits_project │ │ │ │ └── migration.sql │ │ │ ├── 20240620074546_add_segments │ │ │ │ └── migration.sql │ │ │ ├── 20240620075022_add_segment_to_project │ │ │ │ └── migration.sql │ │ │ ├── 20240621121638_add_created_at_to_segment_rule │ │ │ │ └── migration.sql │ │ │ ├── 20240624143331_add_segment_to_rule │ │ │ │ └── migration.sql │ │ │ └── migration_lock.toml │ │ └── schema.prisma │ ├── scripts │ │ ├── prisma-migrate-diff.ts │ │ ├── prisma-migrate.ts │ │ ├── setup-clickhouse-run.ts │ │ └── setup-clickhouse.ts │ ├── seed.ts │ ├── seeds │ │ ├── activity.ts │ │ ├── cleanup-run.ts │ │ ├── events.ts │ │ ├── flaghits.ts │ │ ├── flags.ts │ │ ├── projects.ts │ │ ├── seed-run.ts │ │ ├── seed.ts │ │ └── users.ts │ └── tsconfig.json ├── load-testing │ ├── .gitignore │ ├── README.md │ ├── e2e │ │ └── resolution-checks.spec.ts │ ├── helpers │ │ ├── changeFlagStatus.ts │ │ ├── seed-run.ts │ │ └── seed.ts │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.js │ │ └── index.js │ └── tests │ │ ├── activated-strategy.ts │ │ └── run.ts ├── qualitative-analytics │ ├── .gitignore │ ├── .npmignore │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ │ ├── App.tsx │ │ ├── components │ │ │ ├── Events.tsx │ │ │ └── NumberValue.tsx │ │ ├── context │ │ │ └── AuthTokenContext.tsx │ │ ├── hooks │ │ │ └── useEvents.ts │ │ ├── index.tsx │ │ ├── types.ts │ │ └── utils │ │ │ ├── cleanupUrl.ts │ │ │ ├── executeWhenEl.ts │ │ │ ├── getEventsPerSelector.ts │ │ │ └── getPointColor.ts │ └── tsconfig.json ├── react-native │ ├── .gitignore │ ├── package.json │ ├── src │ │ ├── ProgressivelyContext.ts │ │ ├── ProgressivelyProvider.tsx │ │ ├── index.ts │ │ ├── loadFlags.ts │ │ ├── types.ts │ │ └── useFlags.ts │ └── tsconfig.json ├── react │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ │ ├── ProgressivelyContext.ts │ │ ├── ProgressivelyProvider.tsx │ │ ├── __tests__ │ │ │ └── index.test.tsx │ │ ├── index.tsx │ │ ├── types.ts │ │ └── useFlags.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── sdk-js │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── index.test.ts │ │ ├── index.ts │ │ └── types.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── server-side │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── rollup.config.mjs │ ├── src │ │ └── index.tsx │ └── tsconfig.json └── types │ ├── index.d.ts │ └── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts └── docker-compose-init.sh ├── turbo.json └── websites ├── backend ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── cli │ └── generate-bearer-token.js ├── nest-cli.json ├── package.json ├── src │ ├── activity-log │ │ ├── activity-log.module.ts │ │ ├── activity-log.service.ts │ │ └── types.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── okta.service.ts │ │ ├── strategies │ │ │ ├── jwt.guard.ts │ │ │ ├── jwt.strategy.ts │ │ │ ├── local.strategy.ts │ │ │ └── okta.strategy.ts │ │ └── types.ts │ ├── caching │ │ ├── caching.module.ts │ │ ├── caching.service.factory.ts │ │ ├── concrete │ │ │ ├── InMemory.service.ts │ │ │ └── redis.service.ts │ │ ├── constants.ts │ │ ├── getEnv.ts │ │ ├── keys.ts │ │ └── types.ts │ ├── crypto │ │ └── crypto.service.ts │ ├── database │ │ ├── database.module.ts │ │ └── prisma.service.ts │ ├── events │ │ ├── events.module.ts │ │ ├── events.service.ts │ │ └── types.ts │ ├── flags │ │ ├── flags.controller.ts │ │ ├── flags.dto.ts │ │ ├── flags.module.ts │ │ ├── flags.service.ts │ │ ├── flags.status.ts │ │ ├── guards │ │ │ └── hasFlagAccess.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── funnels │ │ ├── funnels.dto.ts │ │ ├── funnels.module.ts │ │ └── funnels.service.ts │ ├── jwtConstants.ts │ ├── logging.middleware.ts │ ├── mail │ │ ├── components │ │ │ ├── Link.tsx │ │ │ ├── Logo.tsx │ │ │ ├── PrimaryButton.tsx │ │ │ └── Text.tsx │ │ ├── emails │ │ │ ├── invite-member.tsx │ │ │ ├── registration.tsx │ │ │ └── reset-password.tsx │ │ ├── layouts │ │ │ └── Layout.tsx │ │ ├── mail.module.ts │ │ ├── mail.service.spec.ts │ │ ├── mail.service.ts │ │ └── smtp-constants.ts │ ├── main.ts │ ├── payment │ │ ├── constants.ts │ │ ├── getEnv.ts │ │ ├── payment.controller.ts │ │ ├── payment.module.ts │ │ ├── payment.service.ts │ │ └── types.ts │ ├── projects │ │ ├── errors.ts │ │ ├── guards │ │ │ └── hasProjectAccess.ts │ │ ├── projects.controller.ts │ │ ├── projects.dto.ts │ │ ├── projects.module.ts │ │ ├── projects.service.ts │ │ └── types.ts │ ├── pubsub │ │ ├── concrete │ │ │ ├── InMemory.service.ts │ │ │ └── redis.service.ts │ │ ├── getEnv.ts │ │ ├── pubsub.module.ts │ │ ├── pubsub.service.factory.ts │ │ └── types.ts │ ├── queuing │ │ ├── concrete │ │ │ ├── InMemory.service.ts │ │ │ ├── Kafka.service.ts │ │ │ └── MockService.ts │ │ ├── getEnv.ts │ │ ├── queuing.module.ts │ │ ├── queuing.service.factory.ts │ │ ├── topics.ts │ │ └── types.ts │ ├── rule │ │ ├── Rule.ts │ │ ├── comparators │ │ │ ├── comparatorFactory.ts │ │ │ ├── comparators-types.ts │ │ │ ├── contains.ts │ │ │ ├── equals.ts │ │ │ └── types.ts │ │ ├── rule.module.ts │ │ ├── rule.service.spec.ts │ │ ├── rule.service.ts │ │ └── types.ts │ ├── sdk │ │ ├── sdk.controller.ts │ │ ├── sdk.dto.ts │ │ ├── sdk.module.ts │ │ ├── sdk.service.spec.ts │ │ ├── sdk.service.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── segment │ │ ├── guards │ │ │ └── hasSegmentAccess.ts │ │ ├── segment.controller.ts │ │ ├── segment.module.ts │ │ ├── segment.service.ts │ │ └── types.ts │ ├── shared │ │ ├── decorators │ │ │ └── Roles.ts │ │ ├── pipes │ │ │ └── ValidationPipe.ts │ │ └── utils │ │ │ ├── getDeviceInfo.ts │ │ │ └── sleep.ts │ ├── strategy │ │ ├── guards │ │ │ └── hasStrategyAccess.ts │ │ ├── strategy.controller.ts │ │ ├── strategy.module.ts │ │ ├── strategy.service.ts │ │ └── types.ts │ ├── tokens │ │ ├── tokens.module.ts │ │ ├── tokens.service.ts │ │ └── types.ts │ ├── users │ │ ├── roles.ts │ │ ├── status.ts │ │ ├── types.ts │ │ ├── users.controller.ts │ │ ├── users.decorator.ts │ │ ├── users.dto.ts │ │ ├── users.module.ts │ │ └── users.service.ts │ ├── webhooks │ │ ├── guards │ │ │ └── hasWebhookAccess.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ ├── webhooks.controller.ts │ │ ├── webhooks.module.ts │ │ └── webhooks.service.ts │ └── websocket │ │ ├── rooms.ts │ │ ├── types.ts │ │ ├── websocket.gateway.ts │ │ └── websocket.module.ts ├── test │ ├── auth │ │ └── auth.e2e-spec.ts │ ├── flags │ │ ├── activity.e2e-spec.ts │ │ └── flags.e2e-spec.ts │ ├── helpers │ │ ├── TestLogger.ts │ │ ├── authenticate.ts │ │ ├── create-project.ts │ │ ├── createPasswordToken.ts │ │ ├── prepareApp.ts │ │ └── verify-auth-guard.ts │ ├── jest-e2e.json │ ├── projects │ │ └── projects.e2e-spec.ts │ ├── sdk │ │ └── sdk.e2e-spec.ts │ ├── segments │ │ └── segments.e2e-spec.ts │ ├── strategy │ │ └── strategy.e2e-spec.ts │ ├── users │ │ └── users.e2e-spec.ts │ └── webhooks │ │ └── webhooks.e2e-spec.ts ├── tsconfig.build.json └── tsconfig.json ├── documentation ├── .gitignore ├── .vscode │ ├── extensions.json │ └── launch.json ├── README.md ├── astro.config.mjs ├── package.json ├── public │ └── favicon.svg ├── src │ ├── assets │ │ └── houston.webp │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── configuration │ │ │ ├── environments-management.mdx │ │ │ ├── percentage-rollout.mdx │ │ │ └── targeting-rules.mdx │ │ │ ├── core-concepts │ │ │ ├── use-case-and-benefits.mdx │ │ │ └── what-are-feature-flags.mdx │ │ │ ├── index.mdx │ │ │ ├── observability │ │ │ ├── funnels.mdx │ │ │ └── web-analytics.mdx │ │ │ ├── overview │ │ │ ├── architecture.mdx │ │ │ ├── quick-start.mdx │ │ │ └── what-is-progressively.mdx │ │ │ └── sdks │ │ │ ├── analytics │ │ │ └── quantitative-analytics.mdx │ │ │ └── feature-flags │ │ │ ├── sdk-js.mdx │ │ │ ├── sdk-react.mdx │ │ │ └── sdk-server-side.mdx │ └── env.d.ts └── tsconfig.json ├── frontend ├── .env.example ├── .eslintrc.cjs ├── .gitignore ├── README.md ├── app │ ├── app.css │ ├── components │ │ ├── BackLink.tsx │ │ ├── Background.tsx │ │ ├── BigButton.tsx │ │ ├── BigStat.tsx │ │ ├── Boxes │ │ │ ├── ErrorBox.tsx │ │ │ ├── SuccessBox.tsx │ │ │ ├── TipBox.tsx │ │ │ └── WarningBox.tsx │ │ ├── Breadcrumbs │ │ │ ├── index.tsx │ │ │ └── types.ts │ │ ├── ButtonCopy.tsx │ │ ├── Buttons │ │ │ ├── Button.tsx │ │ │ ├── CreateButton.tsx │ │ │ ├── DashedButton.tsx │ │ │ ├── DeleteButton.tsx │ │ │ ├── EditButton.tsx │ │ │ ├── IconButton.tsx │ │ │ └── SubmitButton.tsx │ │ ├── Card.tsx │ │ ├── Checkbox.tsx │ │ ├── Codeblock.tsx │ │ ├── Container.tsx │ │ ├── Device.tsx │ │ ├── Dialog │ │ │ └── Dialog.tsx │ │ ├── Dl.tsx │ │ ├── EmptyState.tsx │ │ ├── Entity │ │ │ ├── Entity.tsx │ │ │ └── EntityField.tsx │ │ ├── ExternalLink.tsx │ │ ├── Fields │ │ │ ├── DateTimeInput.tsx │ │ │ ├── FormGroup.tsx │ │ │ ├── Label.tsx │ │ │ ├── PercentageField.tsx │ │ │ ├── Radio.tsx │ │ │ ├── RadioField.tsx │ │ │ ├── Select │ │ │ │ ├── RawSelect.tsx │ │ │ │ └── SelectField.tsx │ │ │ ├── TagInput.tsx │ │ │ └── TextInput.tsx │ │ ├── FocusTrap.tsx │ │ ├── FunnelChart │ │ │ └── FunnelChart.tsx │ │ ├── HStack.tsx │ │ ├── HideMobile.tsx │ │ ├── HorizontalNav.tsx │ │ ├── IconBox.tsx │ │ ├── Icons │ │ │ ├── EmptyBoxIcon.tsx │ │ │ ├── FlagIcon.tsx │ │ │ ├── HomeIcon.tsx │ │ │ ├── ProjectIcon.tsx │ │ │ ├── SettingsIcon.tsx │ │ │ ├── UserIcon.tsx │ │ │ └── VariantIcon.tsx │ │ ├── Inert │ │ │ ├── Inert.tsx │ │ │ ├── context │ │ │ │ └── InertContext.ts │ │ │ ├── hooks │ │ │ │ ├── useInert.ts │ │ │ │ └── useSetInert.ts │ │ │ └── providers │ │ │ │ └── InertProvider.tsx │ │ ├── InitialBox.tsx │ │ ├── LineChart │ │ │ ├── ActivePoint.tsx │ │ │ ├── LineChart.tsx │ │ │ ├── Tooltip.tsx │ │ │ ├── constants.ts │ │ │ ├── formatters.ts │ │ │ └── styles.ts │ │ ├── Link.tsx │ │ ├── Logo │ │ │ └── Logo.tsx │ │ ├── Main.tsx │ │ ├── MenuButton.tsx │ │ ├── Navbar.tsx │ │ ├── NumberValue.tsx │ │ ├── PageTitle.tsx │ │ ├── Progress.tsx │ │ ├── SearchBar.tsx │ │ ├── Section.tsx │ │ ├── Separator.tsx │ │ ├── SkipNav.tsx │ │ ├── Spacer.tsx │ │ ├── Spinner.tsx │ │ ├── Stack.tsx │ │ ├── Switch │ │ │ ├── Label.tsx │ │ │ ├── RawSwitch.tsx │ │ │ └── Switch.tsx │ │ ├── Table.tsx │ │ ├── Tabs.tsx │ │ ├── Tag.tsx │ │ ├── Tooltip │ │ │ ├── RawTooltip.tsx │ │ │ └── Tooltip.tsx │ │ ├── Typography.tsx │ │ ├── Ul.tsx │ │ └── VisuallyHidden.tsx │ ├── constants.ts │ ├── entry.client.tsx │ ├── entry.server.tsx │ ├── layouts │ │ ├── CreateEntityLayout.tsx │ │ ├── CreateEntityTitle.tsx │ │ ├── DashboardLayout.tsx │ │ ├── DeleteEntityLayout.tsx │ │ ├── DeleteEntityTitle.tsx │ │ ├── NotAuthenticatedLayout.tsx │ │ └── SearchLayout.tsx │ ├── modules │ │ ├── a11y │ │ │ └── utils │ │ │ │ ├── getFocusableNodes.ts │ │ │ │ └── keyboardKeys.ts │ │ ├── activity │ │ │ ├── components │ │ │ │ ├── ActivityDescription.tsx │ │ │ │ ├── ActivityIcon.tsx │ │ │ │ └── ActivityLogList.tsx │ │ │ ├── services │ │ │ │ └── getActivity.ts │ │ │ └── types.ts │ │ ├── analytics │ │ │ ├── components │ │ │ │ ├── HotSpotsLits.tsx │ │ │ │ └── SearchableCountTable.tsx │ │ │ └── helpers │ │ │ │ ├── getBrowserIcon.tsx │ │ │ │ └── getOSIcon.tsx │ │ ├── auth │ │ │ ├── hooks │ │ │ │ └── useOkta.ts │ │ │ ├── services │ │ │ │ ├── auth-guard.ts │ │ │ │ ├── authenticate.ts │ │ │ │ ├── get-okta-config.ts │ │ │ │ └── okta.ts │ │ │ ├── types.ts │ │ │ └── validators │ │ │ │ └── validate-signin-form.ts │ │ ├── billing │ │ │ └── services │ │ │ │ └── checkout.ts │ │ ├── events │ │ │ └── types.ts │ │ ├── flags │ │ │ ├── components │ │ │ │ ├── FlagList.tsx │ │ │ │ ├── FlagMenu.tsx │ │ │ │ ├── FlagStatus.tsx │ │ │ │ └── ToggleFlag.tsx │ │ │ ├── contexts │ │ │ │ ├── FlagContext.ts │ │ │ │ ├── FlagProvider.tsx │ │ │ │ └── useFlag.ts │ │ │ ├── form-actions │ │ │ │ └── toggleFlagAction.ts │ │ │ ├── services │ │ │ │ ├── activateFlag.ts │ │ │ │ ├── createFlag.ts │ │ │ │ ├── deleteFlag.ts │ │ │ │ ├── editFlag.ts │ │ │ │ ├── getFlagById.ts │ │ │ │ ├── getFlagHits.ts │ │ │ │ └── getFlagMetaTitle.ts │ │ │ ├── types.ts │ │ │ └── validators │ │ │ │ └── validateFlagShape.ts │ │ ├── forms │ │ │ └── utils │ │ │ │ ├── validateEmail.ts │ │ │ │ └── validatePassword.ts │ │ ├── funnels │ │ │ └── types.ts │ │ ├── instructions │ │ │ ├── helpers │ │ │ │ └── transform.ts │ │ │ └── samples │ │ │ │ ├── getAnalyticsSample.ts │ │ │ │ ├── getNodeSample.ts │ │ │ │ ├── getReactSample.ts │ │ │ │ ├── getSdkJsSample.ts │ │ │ │ └── types.ts │ │ ├── misc │ │ │ ├── components │ │ │ │ └── FormattedDate.tsx │ │ │ ├── hooks │ │ │ │ └── useHydrated.ts │ │ │ └── utils │ │ │ │ ├── calculateGrowthRate.ts │ │ │ │ ├── closestFocusable.ts │ │ │ │ ├── closestWithAttribute.ts │ │ │ │ ├── focusInTree.ts │ │ │ │ ├── formatDate.ts │ │ │ │ ├── formatDateAgo.ts │ │ │ │ ├── getEnvVar.ts │ │ │ │ ├── stringToColor.ts │ │ │ │ └── toPercentage.ts │ │ ├── payments │ │ │ ├── components │ │ │ │ └── CheckoutForm.tsx │ │ │ └── services │ │ │ │ ├── createCheckoutSession.ts │ │ │ │ └── getEventUsage.ts │ │ ├── projects │ │ │ ├── components │ │ │ │ ├── CountTable.tsx │ │ │ │ ├── InsightsFilters.tsx │ │ │ │ ├── ProjectList.tsx │ │ │ │ └── ProjectNavBar.tsx │ │ │ ├── contexts │ │ │ │ ├── ProjectContext.ts │ │ │ │ ├── ProjectProvider.tsx │ │ │ │ ├── ProjectsContext.ts │ │ │ │ ├── ProjectsProvider.tsx │ │ │ │ ├── useProject.ts │ │ │ │ └── useProjects.ts │ │ │ ├── reducers │ │ │ │ └── funnelCreationReducer.ts │ │ │ ├── services │ │ │ │ ├── addMemberToProject.ts │ │ │ │ ├── createFunnel.ts │ │ │ │ ├── createProject.ts │ │ │ │ ├── deleteFunnel.ts │ │ │ │ ├── deleteProject.ts │ │ │ │ ├── editProject.ts │ │ │ │ ├── getEventHotSpots.ts │ │ │ │ ├── getEventsForFields.ts │ │ │ │ ├── getEventsGroupedByDate.ts │ │ │ │ ├── getFunnels.ts │ │ │ │ ├── getFunnelsFields.ts │ │ │ │ ├── getGlobalMetric.ts │ │ │ │ ├── getPageViewsGroupedByDate.ts │ │ │ │ ├── getProject.ts │ │ │ │ ├── getProjectFlags.ts │ │ │ │ ├── getProjectMetaTitle.ts │ │ │ │ ├── getProjects.ts │ │ │ │ ├── removeMember.ts │ │ │ │ └── rotateSecretKey.ts │ │ │ ├── types.ts │ │ │ └── validators │ │ │ │ └── validateProjectName.ts │ │ ├── rules │ │ │ ├── services │ │ │ │ └── createEmptyRule.ts │ │ │ └── types.ts │ │ ├── segments │ │ │ ├── components │ │ │ │ └── SegmentItem.tsx │ │ │ ├── form-actions │ │ │ │ └── editSegmentAction.ts │ │ │ ├── hooks │ │ │ │ └── useDeleteSegment.ts │ │ │ ├── services │ │ │ │ ├── deleteSegment.ts │ │ │ │ ├── getSegments.ts │ │ │ │ └── upsertSegments.ts │ │ │ └── types.ts │ │ ├── strategy │ │ │ ├── components │ │ │ │ ├── StrategyFormFields │ │ │ │ │ ├── RuleList.tsx │ │ │ │ │ ├── StrategyFormFields.tsx │ │ │ │ │ └── VariantFields.tsx │ │ │ │ ├── StrategyList │ │ │ │ │ ├── StrategyList.tsx │ │ │ │ │ ├── StrategyListItem.tsx │ │ │ │ │ └── useDeleteStrategy.ts │ │ │ │ ├── StrategyRuleFormField.tsx │ │ │ │ └── WhenField.tsx │ │ │ ├── form-actions │ │ │ │ └── editStrategyAction.ts │ │ │ ├── services │ │ │ │ ├── createStrategy.ts │ │ │ │ ├── deleteStrategy.ts │ │ │ │ ├── editStrategies.ts │ │ │ │ └── getStrategies.ts │ │ │ └── types.ts │ │ ├── support │ │ │ └── components │ │ │ │ └── Intercom.tsx │ │ ├── user │ │ │ ├── actions │ │ │ │ └── registerFormAction.ts │ │ │ ├── components │ │ │ │ ├── RegisterForm.tsx │ │ │ │ ├── UserNav.tsx │ │ │ │ └── UserTable.tsx │ │ │ ├── contexts │ │ │ │ ├── UserContext.ts │ │ │ │ ├── UserProvider.tsx │ │ │ │ └── useUser.ts │ │ │ ├── services │ │ │ │ ├── changeFullname.ts │ │ │ │ ├── changePassword.ts │ │ │ │ ├── createUser.ts │ │ │ │ ├── forgotPassword.ts │ │ │ │ ├── getMe.ts │ │ │ │ └── resetPassword.ts │ │ │ ├── types.ts │ │ │ └── validators │ │ │ │ ├── validate-registration-form.ts │ │ │ │ └── validate-user-fullname.ts │ │ ├── variants │ │ │ ├── components │ │ │ │ └── VariantDot.tsx │ │ │ ├── form-actions │ │ │ │ └── addVariantAction.ts │ │ │ ├── services │ │ │ │ ├── createVariant.ts │ │ │ │ ├── deleteVariant.ts │ │ │ │ └── getVariants.ts │ │ │ └── types.ts │ │ └── webhooks │ │ │ ├── components │ │ │ ├── WebhookEvent.tsx │ │ │ └── WebhooksList.tsx │ │ │ ├── services │ │ │ ├── createWebhook.ts │ │ │ ├── deleteWebhook.ts │ │ │ └── getWebhooks.ts │ │ │ └── types.ts │ ├── root.tsx │ ├── routes │ │ ├── 401.tsx │ │ ├── 403.tsx │ │ ├── 404.tsx │ │ ├── _index.tsx │ │ ├── dashboard.onboarding.tsx │ │ ├── dashboard.profile._index.tsx │ │ ├── dashboard.profile.tsx │ │ ├── dashboard.projects.$id.analytics.custom-events.tsx │ │ ├── dashboard.projects.$id.analytics.hot-spots.tsx │ │ ├── dashboard.projects.$id.analytics.page-views.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.activity.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.audience.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.audience.variants.$variantId.delete.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.audience.variants.create.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.insights.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.settings.delete.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.settings.edit.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.settings.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.webhooks.$webhookId.delete.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.webhooks.create.tsx │ │ ├── dashboard.projects.$id.flags.$flagId.webhooks.tsx │ │ ├── dashboard.projects.$id.flags.all.create.tsx │ │ ├── dashboard.projects.$id.flags.all.tsx │ │ ├── dashboard.projects.$id.funnels.create.tsx │ │ ├── dashboard.projects.$id.funnels.tsx │ │ ├── dashboard.projects.$id.home.tsx │ │ ├── dashboard.projects.$id.segments.tsx │ │ ├── dashboard.projects.$id.settings.add-member.tsx │ │ ├── dashboard.projects.$id.settings.delete-member.$userId.tsx │ │ ├── dashboard.projects.$id.settings.delete.tsx │ │ ├── dashboard.projects.$id.settings.edit.tsx │ │ ├── dashboard.projects.$id.settings.tsx │ │ ├── dashboard.projects.$id.tsx │ │ ├── dashboard.projects.all.create.tsx │ │ ├── dashboard.projects.all.tsx │ │ ├── dashboard.tsx │ │ ├── dashboard.what-s-your-name.tsx │ │ ├── forgot-password.tsx │ │ ├── logout.tsx │ │ ├── oauth2.callback.tsx │ │ ├── register.tsx │ │ ├── reset-password.tsx │ │ ├── signin.tsx │ │ ├── signout.tsx │ │ └── welcome.tsx │ ├── sessions.ts │ └── tailwind.css ├── cypress.config.ts ├── cypress │ ├── e2e │ │ ├── forgot-password.spec.ts │ │ ├── index.spec.ts │ │ ├── onboarding.spec.ts │ │ ├── profile │ │ │ └── index.spec.ts │ │ ├── projects │ │ │ ├── all.spec.ts │ │ │ ├── create.spec.ts │ │ │ └── id │ │ │ │ ├── add-member.spec.ts │ │ │ │ ├── analytics.page-views.spec.ts │ │ │ │ ├── delete-member.$userId.spec.ts │ │ │ │ ├── delete.spec.ts │ │ │ │ ├── flags │ │ │ │ ├── create.spec.ts │ │ │ │ ├── flagId │ │ │ │ │ ├── activity.spec.ts │ │ │ │ │ ├── audience.spec.ts │ │ │ │ │ ├── delete.spec.ts │ │ │ │ │ ├── edit.spec.ts │ │ │ │ │ ├── insights.spec.ts │ │ │ │ │ ├── settings.spec.ts │ │ │ │ │ ├── variants │ │ │ │ │ │ ├── $variantId.delete.spec.ts │ │ │ │ │ │ └── create.spec.ts │ │ │ │ │ └── webhooks │ │ │ │ │ │ ├── $webhookId.delete.spec.ts │ │ │ │ │ │ ├── create.spec.ts │ │ │ │ │ │ └── index.spec.ts │ │ │ │ └── index.spec.ts │ │ │ │ ├── segments.spec.ts │ │ │ │ └── settings.spec.ts │ │ ├── register.spec.ts │ │ ├── reset-password.spec.ts │ │ ├── signin.spec.ts │ │ ├── welcome.spec.ts │ │ └── what-s-your-name.spec.ts │ ├── fixtures │ │ └── example.json │ ├── support │ │ ├── constants.ts │ │ ├── e2e.ts │ │ └── index.ts │ └── tsconfig.json ├── package.json ├── postcss.config.js ├── public │ └── favicon.png ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts └── marketing ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── README.md ├── astro.config.mjs ├── package.json ├── public ├── favicon.svg ├── logo.png ├── meta-img.png ├── progressively.min.js └── progressively.qualitative-analytics.min.js ├── src ├── components │ ├── AnimatedImage.tsx │ ├── Badge.tsx │ ├── Banner.astro │ ├── Browser.tsx │ ├── Button.astro │ ├── Card │ │ ├── Card.tsx │ │ ├── CardAsset.astro │ │ ├── CardContent.astro │ │ └── CardTitle.astro │ ├── Code.tsx │ ├── Icons │ │ ├── Check.astro │ │ ├── Discord.astro │ │ ├── Github.astro │ │ ├── Loader.astro │ │ └── Twitter.astro │ ├── Img.astro │ ├── Navbar.astro │ ├── PercentageField.tsx │ ├── Radio.tsx │ ├── Tabs.tsx │ ├── Text.astro │ └── Title.astro ├── env.d.ts ├── hooks │ └── useScroll.ts ├── layouts │ └── Layout.astro ├── pages │ └── index.astro ├── sections │ ├── AboveTheFold.astro │ ├── CatchySection.astro │ ├── Conclusion.astro │ ├── FeatureTabs.tsx │ ├── FeatureTabsWrapper.astro │ ├── Value.astro │ ├── assets │ │ ├── abovefold │ │ │ └── ff.png │ │ ├── catchy │ │ │ ├── catchy.png │ │ │ └── funnels.png │ │ ├── howitworks │ │ │ ├── audience.png │ │ │ ├── mfrachet.png │ │ │ ├── react.png │ │ │ └── welcome.png │ │ └── value │ │ │ ├── analytics.png │ │ │ ├── funnels.png │ │ │ ├── hot-zones.png │ │ │ └── page-views.png │ └── value │ │ ├── FeatureFlags.astro │ │ └── feature-flags │ │ ├── AttributeBased.tsx │ │ ├── BannerBrowser.tsx │ │ ├── Gradual.tsx │ │ ├── MultiVariants.tsx │ │ └── Scheduled.tsx └── support │ └── components │ └── Intercom.tsx ├── tailwind.config.mjs └── tsconfig.json /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/dist 3 | **/migrations 4 | **/*.db 5 | .env 6 | ./example 7 | /.cache 8 | *.log 9 | **.DS_Store 10 | **.cache 11 | **/public/build 12 | **/build -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/clickhouse/action.yml: -------------------------------------------------------------------------------- 1 | name: "Clickhouse setup" 2 | description: "Create the clickhouse instance" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - run: docker pull clickhouse/clickhouse-server 7 | shell: bash 8 | 9 | - run: | 10 | docker run -d -p 8123:8123 -p 19000:9000 --name clickhouse-server --ulimit nofile=262144:262144 clickhouse/clickhouse-server 11 | shell: bash 12 | 13 | - run: | 14 | until curl -s http://localhost:8123/ping --fail; do printf '.'; sleep 1; done 15 | shell: bash 16 | -------------------------------------------------------------------------------- /.github/actions/monorepo-setup/action.yml: -------------------------------------------------------------------------------- 1 | name: "Monorepo setup" 2 | description: "Resolve dependency caching and build + start the projects" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - uses: actions/setup-node@v2 7 | name: Install Node 20 8 | with: 9 | node-version: "20" 10 | 11 | - uses: ./.github/actions/pnpm 12 | 13 | - name: Creating .env files 14 | shell: bash 15 | run: mv ./websites/frontend/.env.example ./websites/frontend/.env && mv ./websites/backend/.env.example ./websites/backend/.env && mv ./packages/database/.env.example ./packages/database/.env 16 | -------------------------------------------------------------------------------- /.github/workflows/fly.yml: -------------------------------------------------------------------------------- 1 | name: Fly Deploy 2 | on: 3 | push: 4 | branches: 5 | - master # change to main if needed 6 | jobs: 7 | deploy: 8 | name: Deploy app 9 | runs-on: ubuntu-latest 10 | concurrency: deploy-group # optional: ensure only one action runs at a time 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - uses: superfly/flyctl-actions/setup-flyctl@master 15 | - run: flyctl deploy --remote-only --config fly.backend.toml && flyctl deploy --remote-only --config fly.frontend.toml 16 | env: 17 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/shared.yml: -------------------------------------------------------------------------------- 1 | name: Shared checks (build, lint, tests, bundlesize) 2 | on: 3 | pull_request: 4 | branches: ["master"] 5 | workflow_dispatch: 6 | 7 | jobs: 8 | static: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v2 13 | with: 14 | node-version: "20" 15 | 16 | - uses: actions/checkout@v2 17 | - uses: ./.github/actions/monorepo-setup 18 | - uses: ./.github/actions/clickhouse 19 | 20 | - name: Bootstrap projects 21 | shell: bash 22 | run: pnpm run setup 23 | 24 | - name: Shared checks 25 | shell: bash 26 | run: pnpm run ci:shared:checks 27 | -------------------------------------------------------------------------------- /.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 | # misc 9 | .DS_Store 10 | *.pem 11 | 12 | # debug 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | 18 | # local env files 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | */schema.prisma 24 | 25 | .turbo 26 | 27 | 28 | # tailwind generated styles on build 29 | .next 30 | .netlify 31 | websites/frontend/app/styles/app.css -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18.17.1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

The Product Control Tower

3 | Progressively 4 |
5 | 6 |
7 | 8 |
9 | Website | 10 | Get started | 11 | Documentation 12 |
13 | -------------------------------------------------------------------------------- /example/astro/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | # logs 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | pnpm-debug.log* 12 | 13 | 14 | # environment variables 15 | .env 16 | .env.production 17 | 18 | # macOS-specific files 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /example/astro/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import node from "@astrojs/node"; 3 | import react from "@astrojs/react"; 4 | 5 | export default defineConfig({ 6 | output: "server", 7 | adapter: node({ 8 | mode: "standalone", 9 | }), 10 | integrations: [react()], 11 | server: { 12 | port: 3002, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /example/astro/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/astro/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } -------------------------------------------------------------------------------- /example/nextjs/.env: -------------------------------------------------------------------------------- 1 | PROGRESSIVELY_ENV="valid-sdk-key" -------------------------------------------------------------------------------- /example/nextjs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /example/nextjs/.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /example/nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /example/nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | reactStrictMode: true, 6 | webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => { 7 | config.resolve.alias["react"] = path.dirname( 8 | require.resolve("./node_modules/react") 9 | ); 10 | 11 | return config; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /example/nextjs/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | 3 | function MyApp({ Component, pageProps }: any) { 4 | return ; 5 | } 6 | 7 | export default MyApp; 8 | -------------------------------------------------------------------------------- /example/nextjs/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /example/nextjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/example/nextjs/public/favicon.ico -------------------------------------------------------------------------------- /example/nextjs/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /example/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules", "cypress"] 20 | } 21 | -------------------------------------------------------------------------------- /example/node/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /example/php/.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | /.idea 3 | node_modules 4 | composer.lock -------------------------------------------------------------------------------- /example/php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "progressively/test-php", 3 | "description": "Example of the PHP sdk in action", 4 | "require": { 5 | "ext-curl": "*", 6 | "ext-json": "*", 7 | "progressively/sdk-php": "dev-master" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/playwright-helpers/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /playwright/.cache/ 5 | -------------------------------------------------------------------------------- /example/playwright-helpers/index.ts: -------------------------------------------------------------------------------- 1 | import type { BrowserContext } from "@playwright/test"; 2 | 3 | export * from "@playwright/test"; 4 | export const getCookie = async (name: string, ctx: BrowserContext) => { 5 | const cookies = await ctx.cookies(); 6 | 7 | return cookies.find((cookie) => cookie.name === name)?.value; 8 | }; 9 | -------------------------------------------------------------------------------- /example/qwik/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/.DS_Store 3 | *. 4 | .vscode/settings.json 5 | .history 6 | .yarn 7 | bazel-* 8 | bazel-bin 9 | bazel-out 10 | bazel-qwik 11 | bazel-testlogs 12 | dist 13 | dist-dev 14 | lib 15 | lib-types 16 | etc 17 | external 18 | node_modules 19 | temp 20 | tsc-out 21 | tsdoc-metadata.json 22 | target 23 | output 24 | rollup.config.js 25 | build 26 | .cache 27 | .vscode 28 | .rollup.cache 29 | dist 30 | tsconfig.tsbuildinfo 31 | vite.config.ts 32 | *.spec.tsx 33 | *.spec.ts 34 | pnpm-lock.yaml 35 | package-lock.json 36 | yarn.lock 37 | server 38 | -------------------------------------------------------------------------------- /example/qwik/.gitignore: -------------------------------------------------------------------------------- 1 | # Build 2 | /dist 3 | /lib 4 | /lib-types 5 | /server 6 | 7 | # Development 8 | node_modules 9 | 10 | # Cache 11 | .cache 12 | .mf 13 | .rollup.cache 14 | tsconfig.tsbuildinfo 15 | 16 | # Logs 17 | logs 18 | *.log 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | pnpm-debug.log* 23 | lerna-debug.log* 24 | 25 | # Editor 26 | .vscode/* 27 | !.vscode/extensions.json 28 | .idea 29 | .DS_Store 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | *.sw? 35 | 36 | # Yarn 37 | .yarn/* 38 | !.yarn/releases 39 | -------------------------------------------------------------------------------- /example/qwik/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/.DS_Store 3 | *. 4 | .vscode/settings.json 5 | .history 6 | .yarn 7 | bazel-* 8 | bazel-bin 9 | bazel-out 10 | bazel-qwik 11 | bazel-testlogs 12 | dist 13 | dist-dev 14 | lib 15 | lib-types 16 | etc 17 | external 18 | node_modules 19 | temp 20 | tsc-out 21 | tsdoc-metadata.json 22 | target 23 | output 24 | rollup.config.js 25 | build 26 | .cache 27 | .vscode 28 | .rollup.cache 29 | dist 30 | tsconfig.tsbuildinfo 31 | vite.config.ts 32 | *.spec.tsx 33 | *.spec.ts 34 | pnpm-lock.yaml 35 | package-lock.json 36 | yarn.lock 37 | server 38 | -------------------------------------------------------------------------------- /example/qwik/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "unifiedjs.vscode-mdx"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /example/qwik/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/web-manifest-combined.json", 3 | "name": "qwik-project-name", 4 | "short_name": "Welcome to Qwik", 5 | "start_url": ".", 6 | "display": "standalone", 7 | "background_color": "#fff", 8 | "description": "A Qwik project app." 9 | } 10 | -------------------------------------------------------------------------------- /example/qwik/public/robots.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/example/qwik/public/robots.txt -------------------------------------------------------------------------------- /example/qwik/src/components/header/header.css: -------------------------------------------------------------------------------- 1 | header { 2 | display: flex; 3 | background: white; 4 | border-bottom: 10px solid var(--qwik-dark-purple); 5 | } 6 | 7 | header .logo a { 8 | display: inline-block; 9 | padding: 10px 10px 7px 20px; 10 | } 11 | 12 | header ul { 13 | margin: 0; 14 | padding: 3px 10px 0 0; 15 | list-style: none; 16 | flex: 1; 17 | text-align: right; 18 | } 19 | 20 | header li { 21 | display: inline-block; 22 | margin: 0; 23 | padding: 0; 24 | } 25 | 26 | header li a { 27 | display: inline-block; 28 | padding: 15px 10px; 29 | text-decoration: none; 30 | } 31 | 32 | header li a:hover { 33 | text-decoration: underline; 34 | } 35 | -------------------------------------------------------------------------------- /example/qwik/src/entry.dev.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * WHAT IS THIS FILE? 3 | * 4 | * Development entry point using only client-side modules: 5 | * - Do not use this mode in production! 6 | * - No SSR 7 | * - No portion of the application is pre-rendered on the server. 8 | * - All of the application is running eagerly in the browser. 9 | * - More code is transferred to the browser than in SSR mode. 10 | * - Optimizer/Serialization/Deserialization code is not exercised! 11 | */ 12 | import { render, type RenderOptions } from '@builder.io/qwik'; 13 | import Root from './root'; 14 | 15 | export default function (opts: RenderOptions) { 16 | return render(document, , opts); 17 | } 18 | -------------------------------------------------------------------------------- /example/qwik/src/entry.preview.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * WHAT IS THIS FILE? 3 | * 4 | * It's the bundle entry point for `npm run preview`. 5 | * That is, serving your app built in production mode. 6 | * 7 | * Feel free to modify this file, but don't remove it! 8 | * 9 | * Learn more about Vite's preview command: 10 | * - https://vitejs.dev/config/preview-options.html#preview-options 11 | * 12 | */ 13 | import { createQwikCity } from '@builder.io/qwik-city/middleware/node'; 14 | import render from './entry.ssr'; 15 | import qwikCityPlan from '@qwik-city-plan'; 16 | 17 | /** 18 | * The default export is the QwikCity adapter used by Vite preview. 19 | */ 20 | export default createQwikCity({ render, qwikCityPlan }); 21 | -------------------------------------------------------------------------------- /example/qwik/src/global.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/example/qwik/src/global.css -------------------------------------------------------------------------------- /example/qwik/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { qwikVite } from "@builder.io/qwik/optimizer"; 3 | import { qwikCity } from "@builder.io/qwik-city/vite"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig(() => { 7 | return { 8 | plugins: [qwikCity(), qwikVite(), tsconfigPaths()], 9 | preview: { 10 | headers: { 11 | "Cache-Control": "public, max-age=600", 12 | }, 13 | }, 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /example/rsc/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /example/rsc/.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /example/rsc/app/components/FlaggedComponent.server.tsx: -------------------------------------------------------------------------------- 1 | import { Progressively } from "@progressively/server-side"; 2 | 3 | export async function FlaggedComponent() { 4 | const sdk = Progressively.init({ 5 | secretKey: "secret-key", 6 | websocketUrl: "ws://localhost:4000", 7 | apiUrl: "http://localhost:4000", 8 | fields: { 9 | email: "marvin.frachet@something.com", 10 | id: "1", 11 | }, 12 | }); 13 | 14 | const { 15 | data: { flags }, 16 | } = await sdk.loadFlags(); 17 | 18 | if (flags?.newHomepage) { 19 | return
New variant
; 20 | } 21 | 22 | return
Old variant
; 23 | } 24 | -------------------------------------------------------------------------------- /example/rsc/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/example/rsc/app/favicon.ico -------------------------------------------------------------------------------- /example/rsc/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /example/rsc/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import './globals.css' 2 | import type { Metadata } from 'next' 3 | import { Inter } from 'next/font/google' 4 | 5 | const inter = Inter({ subsets: ['latin'] }) 6 | 7 | export const metadata: Metadata = { 8 | title: 'Create Next App', 9 | description: 'Generated by create next app', 10 | } 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode 16 | }) { 17 | return ( 18 | 19 | {children} 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /example/rsc/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { FlaggedComponent } from "./components/FlaggedComponent.server"; 2 | 3 | export default function Home() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | 11 | export const dynamic = "force-dynamic"; 12 | -------------------------------------------------------------------------------- /example/rsc/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /example/rsc/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /example/rsc/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/rsc/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /example/svelte/.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /example/svelte/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 5 | plugins: ['svelte3', '@typescript-eslint'], 6 | ignorePatterns: ['*.cjs'], 7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 8 | settings: { 9 | 'svelte3/typescript': () => require('typescript') 10 | }, 11 | parserOptions: { 12 | sourceType: 'module', 13 | ecmaVersion: 2020 14 | }, 15 | env: { 16 | browser: true, 17 | es2017: true, 18 | node: true 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /example/svelte/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | 12 | -------------------------------------------------------------------------------- /example/svelte/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /example/svelte/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /example/svelte/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /example/svelte/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /example/svelte/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /example/svelte/src/routes/+page.server.js: -------------------------------------------------------------------------------- 1 | import { Progressively } from '@progressively/server-side'; 2 | 3 | /** @type {import('./$types').PageServerLoad} */ 4 | export async function load({ cookies }) { 5 | const id = cookies.get('progressively-id'); 6 | const sdk = Progressively.init({ 7 | secretKey: 'secret-key', 8 | clientKey: 'valid-sdk-key', 9 | websocketUrl: 'ws://localhost:4000', 10 | apiUrl: 'http://localhost:4000', 11 | fields: { 12 | id 13 | } 14 | }); 15 | 16 | const { data, userId } = await sdk.loadFlags(); 17 | 18 | cookies.set('progressively-id', userId); 19 | 20 | return data; 21 | } 22 | -------------------------------------------------------------------------------- /example/svelte/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/example/svelte/static/favicon.png -------------------------------------------------------------------------------- /example/svelte/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/kit/vite'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: adapter() 12 | } 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /example/svelte/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /example/svelte/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | -------------------------------------------------------------------------------- /fly.frontend.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for progressively on 2024-02-13T17:01:13+01:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = 'progressively-dashboard' 7 | primary_region = 'cdg' 8 | 9 | [build] 10 | dockerfile = 'Dockerfile.Frontend' 11 | 12 | [http_service] 13 | internal_port = 3000 14 | force_https = true 15 | auto_stop_machines = true 16 | auto_start_machines = true 17 | min_machines_running = 1 18 | processes = ['app'] 19 | 20 | [[vm]] 21 | cpu_kind = 'shared' 22 | cpus = 2 23 | memory_mb = 2048 24 | -------------------------------------------------------------------------------- /packages/analytics/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | src/coverage -------------------------------------------------------------------------------- /packages/analytics/.npmignore: -------------------------------------------------------------------------------- 1 | rollup.config.js 2 | README.md 3 | src 4 | tsconfig.json 5 | .gitignore -------------------------------------------------------------------------------- /packages/analytics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@progressively/analytics", 3 | "author": "mfrachet", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "build": "rollup -c rollup.config.mjs", 7 | "build:dev": "pnpm run build -- --environment BUILD:development" 8 | }, 9 | "dependencies": { 10 | "@progressively/types": "^1.0.0" 11 | }, 12 | "devDependencies": { 13 | "@rollup/plugin-commonjs": "^25.0.7", 14 | "@rollup/plugin-node-resolve": "^15.2.3", 15 | "@rollup/plugin-terser": "^0.4.0", 16 | "@rollup/plugin-typescript": "^8.3.2", 17 | "rollup": "^2.75.6", 18 | "typescript": "5.1.6" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/analytics/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import terser from "@rollup/plugin-terser"; 3 | import resolve from "@rollup/plugin-node-resolve"; 4 | import commonjs from "@rollup/plugin-commonjs"; 5 | 6 | export default () => { 7 | return { 8 | input: "src/index.ts", 9 | output: { 10 | file: "dist/progressively.min.js", 11 | format: "iife", 12 | }, 13 | plugins: [resolve(), commonjs(), typescript(), terser()], 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/analytics/src/setup-navigation-listeners.ts: -------------------------------------------------------------------------------- 1 | export const setupNavigationListeners = (callback: () => void) => { 2 | // Listen for popstate event (triggered by browser navigation buttons) 3 | window.addEventListener("popstate", callback); 4 | 5 | // Intercept history.pushState and history.replaceState to detect SPA navigation changes 6 | const originalPushState = history.pushState; 7 | const originalReplaceState = history.replaceState; 8 | 9 | history.pushState = function (...args) { 10 | originalPushState.apply(this, args); 11 | callback(); 12 | }; 13 | 14 | history.replaceState = function (...args) { 15 | originalReplaceState.apply(this, args); 16 | callback(); 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/analytics/src/show-qualitative-analytics.ts: -------------------------------------------------------------------------------- 1 | export const showQualitativeAnalytics = (endpoint: string) => { 2 | const script = document.createElement("script"); 3 | script.src = `https://progressively.app/progressively.qualitative-analytics.min.js`; 4 | //script.src = `http://localhost:4321/progressively.qualitative-analytics.min.js`; 5 | script.setAttribute("data-progressively-endpoint", endpoint); 6 | 7 | document.body.appendChild(script); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/analytics/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface TrackOpts { 2 | posX?: number; 3 | posY?: number; 4 | selector?: string; 5 | data?: any; 6 | } 7 | 8 | export type TrackFn = (eventName: string, opts?: TrackOpts) => Promise; 9 | 10 | export interface AnalyticsEvent { 11 | name: string; 12 | opts?: TrackOpts; 13 | } 14 | -------------------------------------------------------------------------------- /packages/analytics/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", // For older browsers, use "es5" 4 | "module": "ESNext", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "noEmit": true, 12 | "jsx": "react" 13 | }, 14 | "include": ["src/**/*"] 15 | } -------------------------------------------------------------------------------- /packages/database/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://admin:admin@localhost:5432/progressively" 2 | SHADOW_DATABASE_URL="postgresql://admin:admin@localhost:5432/progressively" 3 | CLICKHOUSE_HOST="http://localhost:8123" 4 | CLICKHOUSE_USER="default" 5 | CLICKHOUSE_PASSWORD="" -------------------------------------------------------------------------------- /packages/database/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | *.env 37 | 38 | backward.sql -------------------------------------------------------------------------------- /packages/database/clickhouse-client.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@clickhouse/client"; 2 | 3 | export const getClient = () => { 4 | const client = createClient({ 5 | host: process.env.CLICKHOUSE_HOST!, 6 | username: process.env.CLICKHOUSE_USER!, 7 | password: process.env.CLICKHOUSE_PASSWORD!, 8 | }); 9 | 10 | return client; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@prisma/client"; 2 | export * from "./clickhouse-client"; 3 | export type * from "@clickhouse/client"; 4 | 5 | export const Tables = { 6 | Events: "events", 7 | }; 8 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230504064533_add_link_issue_flag/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Flag" ADD COLUMN "issueLink" TEXT, 3 | ALTER COLUMN "description" DROP NOT NULL; 4 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230504072038_revert_link_issue_flag/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `issueLink` on the `Flag` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Flag" DROP COLUMN "issueLink"; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230510061801_plans_preparation/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "trialEnd" TIMESTAMP(3); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Plan" ( 6 | "uuid" TEXT NOT NULL, 7 | "projectCount" INTEGER NOT NULL, 8 | "environmentCount" INTEGER NOT NULL, 9 | "evaluationCount" INTEGER NOT NULL, 10 | "createdAt" TIMESTAMP(3) NOT NULL, 11 | "userUuid" TEXT, 12 | 13 | CONSTRAINT "Plan_pkey" PRIMARY KEY ("uuid") 14 | ); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "Plan" ADD CONSTRAINT "Plan_userUuid_fkey" FOREIGN KEY ("userUuid") REFERENCES "User"("uuid") ON DELETE SET NULL ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230510112956_default_created_at_plan/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Plan" ALTER COLUMN "createdAt" SET DEFAULT CURRENT_TIMESTAMP; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230512131424_revert_projet_count_env_count/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `environmentCount` on the `Plan` table. All the data in the column will be lost. 5 | - You are about to drop the column `projectCount` on the `Plan` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Plan" DROP COLUMN "environmentCount", 10 | DROP COLUMN "projectCount"; 11 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230517050329_add_stripe_user/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "StripeUser" ( 3 | "uuid" TEXT NOT NULL, 4 | "customerId" TEXT NOT NULL, 5 | "userUuid" TEXT NOT NULL, 6 | 7 | CONSTRAINT "StripeUser_pkey" PRIMARY KEY ("uuid") 8 | ); 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "StripeUser" ADD CONSTRAINT "StripeUser_userUuid_fkey" FOREIGN KEY ("userUuid") REFERENCES "User"("uuid") ON DELETE RESTRICT ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230517051315_add_stripe_transaction/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "StripeTransaction" ( 3 | "uuid" TEXT NOT NULL, 4 | "customerId" TEXT NOT NULL, 5 | "sessionId" TEXT NOT NULL, 6 | "stripedCreatedAt" INTEGER NOT NULL, 7 | "stripeInvoiceId" TEXT NOT NULL, 8 | "subscriptionId" TEXT NOT NULL, 9 | 10 | CONSTRAINT "StripeTransaction_pkey" PRIMARY KEY ("uuid") 11 | ); 12 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230517053300_shift_customer_id/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `StripeUser` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | - You are about to drop the column `uuid` on the `StripeUser` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "StripeUser" DROP CONSTRAINT "StripeUser_pkey", 10 | DROP COLUMN "uuid", 11 | ADD CONSTRAINT "StripeUser_pkey" PRIMARY KEY ("customerId"); 12 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230517072350_add_status_to_plans/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Plan" ADD COLUMN "status" TEXT NOT NULL DEFAULT 'INACTIVE'; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20230919093105_optional_flagid_envid_event/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "Event" DROP CONSTRAINT "Event_flagEnvironmentFlagId_flagEnvironmentEnvironmentId_fkey"; 3 | 4 | -- AlterTable 5 | ALTER TABLE "Event" ALTER COLUMN "flagEnvironmentFlagId" DROP NOT NULL, 6 | ALTER COLUMN "flagEnvironmentEnvironmentId" DROP NOT NULL; 7 | 8 | -- AddForeignKey 9 | ALTER TABLE "Event" ADD CONSTRAINT "Event_flagEnvironmentFlagId_flagEnvironmentEnvironmentId_fkey" FOREIGN KEY ("flagEnvironmentFlagId", "flagEnvironmentEnvironmentId") REFERENCES "FlagEnvironment"("flagId", "environmentId") ON DELETE SET NULL ON UPDATE CASCADE; 10 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231122090859_add_metric_hits/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "MetricHit" ( 3 | "uuid" TEXT NOT NULL, 4 | "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "visitorId" TEXT NOT NULL, 6 | "data" TEXT, 7 | "pMetricUuid" TEXT NOT NULL, 8 | 9 | CONSTRAINT "MetricHit_pkey" PRIMARY KEY ("uuid") 10 | ); 11 | 12 | -- AddForeignKey 13 | ALTER TABLE "MetricHit" ADD CONSTRAINT "MetricHit_pMetricUuid_fkey" FOREIGN KEY ("pMetricUuid") REFERENCES "PMetric"("uuid") ON DELETE RESTRICT ON UPDATE CASCADE; 14 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231123134350_remove_segment_for_now/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `segmentUuid` on the `Rule` table. All the data in the column will be lost. 5 | - You are about to drop the `Segment` table. If the table is not empty, all the data it contains will be lost. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "Rule" DROP CONSTRAINT "Rule_segmentUuid_fkey"; 10 | 11 | -- DropForeignKey 12 | ALTER TABLE "Segment" DROP CONSTRAINT "Segment_flagEnvironmentFlagId_flagEnvironmentEnvironmentId_fkey"; 13 | 14 | -- AlterTable 15 | ALTER TABLE "Rule" DROP COLUMN "segmentUuid"; 16 | 17 | -- DropTable 18 | DROP TABLE "Segment"; 19 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231124063019_attach_event_to_env/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Event" ADD COLUMN "environmentUuid" TEXT; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "Event" ADD CONSTRAINT "Event_environmentUuid_fkey" FOREIGN KEY ("environmentUuid") REFERENCES "Environment"("uuid") ON DELETE SET NULL ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231124072400_add_name_to_env_event/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `name` to the `Event` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Event" ADD COLUMN "name" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231124074557_add_browser_os_in_event/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `browser` to the `Event` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `os` to the `Event` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Event" ADD COLUMN "browser" TEXT NOT NULL, 10 | ADD COLUMN "os" TEXT NOT NULL; 11 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231124095955_add_url_on_event_hit/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `url` to the `Event` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Event" ADD COLUMN "url" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231128162118_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Event" ADD COLUMN "referer" TEXT; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231205132221_secret_key_domain_in_env/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The required column `secretKey` was added to the `Environment` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Environment" ADD COLUMN "domain" TEXT, 9 | ADD COLUMN "secretKey" TEXT NOT NULL; 10 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20231218133031_add_funnel_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Funnel" ( 3 | "uuid" TEXT NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, 6 | "flagEnvironmentFlagId" TEXT NOT NULL, 7 | "flagEnvironmentEnvironmentId" TEXT NOT NULL, 8 | 9 | CONSTRAINT "Funnel_pkey" PRIMARY KEY ("uuid") 10 | ); 11 | 12 | -- AddForeignKey 13 | ALTER TABLE "Funnel" ADD CONSTRAINT "Funnel_flagEnvironmentFlagId_flagEnvironmentEnvironmentId_fkey" FOREIGN KEY ("flagEnvironmentFlagId", "flagEnvironmentEnvironmentId") REFERENCES "FlagEnvironment"("flagId", "environmentId") ON DELETE RESTRICT ON UPDATE CASCADE; 14 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240112143736_add_event_value_funnel_entry/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "FunnelEntry" ADD COLUMN "eventValue" TEXT; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240207122522_remove_scheduling_as_it_is_now/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Schedule` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "Schedule" DROP CONSTRAINT "Schedule_flagEnvironmentFlagId_flagEnvironmentEnvironmentI_fkey"; 9 | 10 | -- DropTable 11 | DROP TABLE "Schedule"; 12 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240207152225_add_flag_to_projects/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Flag" ADD COLUMN "projectUuid" TEXT; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "Flag" ADD CONSTRAINT "Flag_projectUuid_fkey" FOREIGN KEY ("projectUuid") REFERENCES "Project"("uuid") ON DELETE SET NULL ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240207154952_add_status_to_flag_model/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Flag" ADD COLUMN "status" TEXT DEFAULT 'NOT_ACTIVATED'; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240210055726_add_scheduling_to_strategy/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Strategy" ADD COLUMN "whenPredicate" TEXT NOT NULL DEFAULT 'ALWAYS', 3 | ADD COLUMN "whenTimestamp" TIMESTAMP(3); 4 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240212124204_add_session_table/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Event" ADD COLUMN "sessionUuid" TEXT; 3 | 4 | -- CreateTable 5 | CREATE TABLE "Session" ( 6 | "uuid" TEXT NOT NULL, 7 | "startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | 9 | CONSTRAINT "Session_pkey" PRIMARY KEY ("uuid") 10 | ); 11 | 12 | -- AddForeignKey 13 | ALTER TABLE "Event" ADD CONSTRAINT "Event_sessionUuid_fkey" FOREIGN KEY ("sessionUuid") REFERENCES "Session"("uuid") ON DELETE SET NULL ON UPDATE CASCADE; 14 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240212150238_add_visitor_id_to_session/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `visitorId` to the `Session` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Session" ADD COLUMN "visitorId" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240212150645_add_project_to_session/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `projectUuid` to the `Session` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Session" ADD COLUMN "projectUuid" TEXT NOT NULL; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "Session" ADD CONSTRAINT "Session_projectUuid_fkey" FOREIGN KEY ("projectUuid") REFERENCES "Project"("uuid") ON DELETE RESTRICT ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240215150559_add_viewport_to_event/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Event" ADD COLUMN "viewportHeight" INTEGER, 3 | ADD COLUMN "viewportWidth" INTEGER; 4 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240306092651_remove_event_table_for_clickhouse/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `Event` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "Event" DROP CONSTRAINT "Event_projectUuid_fkey"; 9 | 10 | -- DropForeignKey 11 | ALTER TABLE "Event" DROP CONSTRAINT "Event_sessionUuid_fkey"; 12 | 13 | -- DropTable 14 | DROP TABLE "Event"; 15 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240314091742_remove_flag_hits_from_prisma/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `FlagHit` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "FlagHit" DROP CONSTRAINT "FlagHit_flagUuid_fkey"; 9 | 10 | -- DropTable 11 | DROP TABLE "FlagHit"; 12 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240606185705_add_event_usage/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "EventUsage" ( 3 | "projectUuid" TEXT NOT NULL, 4 | "eventsCount" INTEGER NOT NULL, 5 | 6 | CONSTRAINT "EventUsage_pkey" PRIMARY KEY ("projectUuid") 7 | ); 8 | 9 | -- AddForeignKey 10 | ALTER TABLE "EventUsage" ADD CONSTRAINT "EventUsage_projectUuid_fkey" FOREIGN KEY ("projectUuid") REFERENCES "Project"("uuid") ON DELETE RESTRICT ON UPDATE CASCADE; 11 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240606190250_add_credits_project/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Project" ADD COLUMN "credits" INTEGER NOT NULL DEFAULT 0; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240620075022_add_segment_to_project/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Segment" ADD COLUMN "projectUuid" TEXT; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "Segment" ADD CONSTRAINT "Segment_projectUuid_fkey" FOREIGN KEY ("projectUuid") REFERENCES "Project"("uuid") ON DELETE SET NULL ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240621121638_add_created_at_to_segment_rule/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "SegmentRule" ADD COLUMN "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP; 3 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/20240624143331_add_segment_to_rule/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Rule" ADD COLUMN "segmentUuid" TEXT; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "Rule" ADD CONSTRAINT "Rule_segmentUuid_fkey" FOREIGN KEY ("segmentUuid") REFERENCES "Segment"("uuid") ON DELETE SET NULL ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /packages/database/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /packages/database/scripts/setup-clickhouse-run.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | 3 | import { setupClickhouse } from "./setup-clickhouse"; 4 | 5 | setupClickhouse(); 6 | -------------------------------------------------------------------------------- /packages/database/seed.ts: -------------------------------------------------------------------------------- 1 | export * from "./seeds/seed"; 2 | -------------------------------------------------------------------------------- /packages/database/seeds/cleanup-run.ts: -------------------------------------------------------------------------------- 1 | import { cleanupDb } from "./seed"; 2 | 3 | cleanupDb().then(() => { 4 | console.log("[Postgres] Cleanup finished"); 5 | }); 6 | -------------------------------------------------------------------------------- /packages/database/seeds/seed-run.ts: -------------------------------------------------------------------------------- 1 | import { seedDb } from "./seed"; 2 | 3 | seedDb().then(() => console.log("[Postgres] Seeded")); 4 | -------------------------------------------------------------------------------- /packages/database/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2020", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "types": [ "node" ], 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/load-testing/.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | /test-results/ 25 | /playwright-report/ 26 | /playwright/.cache/ 27 | -------------------------------------------------------------------------------- /packages/load-testing/helpers/seed-run.ts: -------------------------------------------------------------------------------- 1 | import { seedDb } from "./seed"; 2 | 3 | seedDb(10); 4 | -------------------------------------------------------------------------------- /packages/load-testing/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/packages/load-testing/public/favicon.ico -------------------------------------------------------------------------------- /packages/load-testing/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/packages/load-testing/public/logo192.png -------------------------------------------------------------------------------- /packages/load-testing/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/packages/load-testing/public/logo512.png -------------------------------------------------------------------------------- /packages/load-testing/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/load-testing/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/load-testing/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import App from "./App"; 5 | 6 | const root = ReactDOM.createRoot(document.getElementById("root")); 7 | root.render( 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /packages/load-testing/tests/run.ts: -------------------------------------------------------------------------------- 1 | import { runner } from "./helpers/runner"; 2 | 3 | async function run() { 4 | await runner.run("./activated-strategy"); 5 | } 6 | 7 | run(); 8 | -------------------------------------------------------------------------------- /packages/qualitative-analytics/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | src/coverage -------------------------------------------------------------------------------- /packages/qualitative-analytics/.npmignore: -------------------------------------------------------------------------------- 1 | rollup.config.js 2 | README.md 3 | src 4 | tsconfig.json 5 | .gitignore -------------------------------------------------------------------------------- /packages/qualitative-analytics/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useState, useEffect } from "react"; 3 | import { AuthTokenProvider } from "./context/AuthTokenContext"; 4 | import { Events } from "./components/Events"; 5 | 6 | export const App = () => { 7 | const [authToken, setAuthToken] = useState(); 8 | 9 | useEffect(() => { 10 | const token = window.location.hash.split("#__progressively=")?.[1]; 11 | 12 | if (token) { 13 | setAuthToken(token); 14 | } 15 | }, []); 16 | 17 | if (!authToken) return null; 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/qualitative-analytics/src/components/NumberValue.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | export const NumberValue = ({ value }: { value: number }) => { 4 | const [formattedValue, setFormattedValue] = useState("0"); 5 | 6 | useEffect(() => { 7 | const formatter = new Intl.NumberFormat("en-US", { 8 | maximumFractionDigits: 1, 9 | }); 10 | setFormattedValue(formatter.format(value)); 11 | }, [value]); 12 | 13 | return <>{formattedValue}; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/qualitative-analytics/src/context/AuthTokenContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, ReactNode } from "react"; 2 | 3 | export const AuthTokenContext = createContext(""); 4 | 5 | export const useAuthToken = () => useContext(AuthTokenContext); 6 | 7 | export const AuthTokenProvider = ({ 8 | children, 9 | token, 10 | }: { 11 | children: ReactNode; 12 | token: string; 13 | }) => { 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /packages/qualitative-analytics/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { App } from "./App"; 4 | 5 | const scriptEl = window.document.currentScript; 6 | const endpoint = scriptEl?.getAttribute("data-progressively-endpoint"); 7 | 8 | (window as any).__progressivelyEndpoint = endpoint; 9 | 10 | const root = document.createElement("div"); 11 | root.id = "__progressively-qualititive-analytics"; 12 | document.body.appendChild(root); 13 | ReactDOM.render(, root); 14 | -------------------------------------------------------------------------------- /packages/qualitative-analytics/src/types.ts: -------------------------------------------------------------------------------- 1 | export type ProgressivelyEventSelector = { 2 | selector: string; 3 | eventCount: number; 4 | }; 5 | -------------------------------------------------------------------------------- /packages/qualitative-analytics/src/utils/cleanupUrl.ts: -------------------------------------------------------------------------------- 1 | export const cleanUrl = (url: string) => { 2 | const urlObj = new URL(url); 3 | 4 | // Remove the __progressivelyProjectId search parameter if it exists 5 | urlObj.searchParams.delete("__progressivelyProjectId"); 6 | 7 | // Remove the #__progressively hash if it exists 8 | if (urlObj.hash.indexOf("#__progressively") !== -1) { 9 | urlObj.hash = ""; 10 | } 11 | 12 | return urlObj.toString(); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/qualitative-analytics/src/utils/executeWhenEl.ts: -------------------------------------------------------------------------------- 1 | export const executeWhenEl = ( 2 | selector: string, 3 | fn: (e: HTMLElement) => void 4 | ) => { 5 | const el = document.querySelector(selector); 6 | if (el) return fn(el as HTMLElement); 7 | 8 | let attempts = 0; 9 | const maxAttempts = 10; 10 | const interval = 500; // in milliseconds 11 | 12 | const intervalId = setInterval(() => { 13 | attempts++; 14 | const element = document.querySelector(selector); 15 | 16 | if (element) { 17 | fn(element as HTMLElement); 18 | clearInterval(intervalId); 19 | } else if (attempts >= maxAttempts) { 20 | clearInterval(intervalId); 21 | } 22 | }, interval); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/qualitative-analytics/src/utils/getPointColor.ts: -------------------------------------------------------------------------------- 1 | export const getPointColor = (sizeRatio: number) => { 2 | if (sizeRatio < 0.2) return "#86efac"; 3 | if (sizeRatio < 0.33) return "#fef08a"; 4 | if (sizeRatio < 0.66) return "#fdba74"; 5 | return "#f87171"; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/qualitative-analytics/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", // For older browsers, use "es5" 4 | "module": "ESNext", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "isolatedModules": true, 11 | "noEmit": true, 12 | "jsx": "react", 13 | "allowJs": true, 14 | }, 15 | "include": ["src/**/*"] 16 | } -------------------------------------------------------------------------------- /packages/react-native/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | lib 6 | coverage -------------------------------------------------------------------------------- /packages/react-native/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@progressively/react-native", 3 | "author": "mfrachet", 4 | "version": "1.0.2", 5 | "types": "./src/index.ts", 6 | "main": "./src/index.ts", 7 | "private": false, 8 | "peerDependencies": { 9 | "react": "18.2.0", 10 | "react-native": "0.71.3" 11 | }, 12 | "devDependencies": { 13 | "@types/base-64": "^1.0.0", 14 | "@types/react": "^18.2.42", 15 | "react": "18.2.0", 16 | "react-native": "0.71.3", 17 | "typescript": "5.1.6" 18 | }, 19 | "dependencies": { 20 | "@progressively/types": "^1.0.0", 21 | "base-64": "^1.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/react-native/src/ProgressivelyContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { FlagDict, StateMachineConstants } from "./types"; 3 | 4 | export interface ProgressivelyContextType { 5 | flags: FlagDict; 6 | isLoading: boolean; 7 | status: StateMachineConstants; 8 | error?: Error; 9 | } 10 | 11 | export const ProgressivelyContext = createContext({ 12 | flags: {}, 13 | isLoading: false, 14 | status: "idle", 15 | error: undefined, 16 | }); 17 | -------------------------------------------------------------------------------- /packages/react-native/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useFlags"; 2 | export * from "./ProgressivelyProvider"; 3 | export * from "./types"; 4 | -------------------------------------------------------------------------------- /packages/react-native/src/loadFlags.ts: -------------------------------------------------------------------------------- 1 | export function loadFlags(apiUrl: string, sdkParams: string) { 2 | let response: Response; 3 | 4 | return fetch(`${apiUrl}/sdk/${sdkParams}}`, { 5 | credentials: "include", 6 | }) 7 | .then((res) => { 8 | response = res; 9 | 10 | if (!res.ok) { 11 | throw new Error("Request couldn't succeed"); 12 | } 13 | 14 | return response.json(); 15 | }) 16 | .then((flags) => { 17 | const userId = response?.headers?.get("X-progressively-id"); 18 | 19 | return { flags, response, userId }; 20 | }) 21 | .catch((error) => { 22 | return { flags: {}, response, error, userId: undefined }; 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-native/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Fields, FlagDict } from "@progressively/types"; 2 | 3 | export interface SDKOptions { 4 | fields?: Fields; 5 | apiUrl?: string; 6 | websocketUrl?: string; 7 | flags?: FlagDict; 8 | headers?: RequestInit["headers"]; 9 | } 10 | export type LoadFlagsReturnType = { 11 | flags: FlagDict; 12 | response?: Response; 13 | error?: Error; 14 | userId?: string | null; 15 | }; 16 | 17 | export { Fields, FlagDict }; 18 | 19 | export type StateMachineConstants = "idle" | "loading" | "success" | "failure"; 20 | -------------------------------------------------------------------------------- /packages/react-native/src/useFlags.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ProgressivelyContext } from "./ProgressivelyContext"; 3 | 4 | export const useFlags = () => useContext(ProgressivelyContext); 5 | -------------------------------------------------------------------------------- /packages/react-native/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "jsx": "react", 6 | "module": "commonjs", 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true 11 | }, 12 | "exclude": [], 13 | "include": ["./src/**/*"] 14 | } -------------------------------------------------------------------------------- /packages/react/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | lib 6 | coverage -------------------------------------------------------------------------------- /packages/react/.npmignore: -------------------------------------------------------------------------------- 1 | rollup.config.js 2 | README.md 3 | ./src 4 | tsconfig.json 5 | .gitignore -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | For documentation, make sure to check https://progressively.app. 2 | -------------------------------------------------------------------------------- /packages/react/src/ProgressivelyContext.ts: -------------------------------------------------------------------------------- 1 | import { Fields, FlagDict } from "@progressively/sdk-js"; 2 | import { createContext } from "react"; 3 | import { StateMachineConstants } from "./types"; 4 | 5 | export interface ProgressivelyContextType { 6 | flags: FlagDict; 7 | isLoading: boolean; 8 | status: StateMachineConstants; 9 | error?: Error; 10 | setFields: (newFields: Fields) => void; 11 | } 12 | 13 | export const ProgressivelyContext = createContext({ 14 | flags: {}, 15 | isLoading: false, 16 | status: "idle", 17 | error: undefined, 18 | setFields: () => { 19 | return {}; 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /packages/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from "./useFlags"; 2 | export * from "./ProgressivelyProvider"; 3 | export * from "./types"; 4 | -------------------------------------------------------------------------------- /packages/react/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Fields, FlagDict } from "@progressively/sdk-js"; 2 | import React from "react"; 3 | 4 | export interface ProgressivelyProviderProps { 5 | clientKey: string; 6 | flags?: FlagDict; 7 | fields?: Fields; 8 | apiUrl?: string; 9 | websocketUrl?: string; 10 | children?: React.ReactNode; 11 | } 12 | 13 | export type StateMachineConstants = "idle" | "loading" | "success" | "failure"; 14 | 15 | export type SetFieldsType = (fields: Fields) => void; 16 | -------------------------------------------------------------------------------- /packages/react/src/useFlags.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ProgressivelyContext } from "./ProgressivelyContext"; 3 | 4 | export const useFlags = () => useContext(ProgressivelyContext); 5 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "types", 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "types": ["vitest/globals", "@testing-library/jest-dom"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/react/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: "jsdom", 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/sdk-js/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | src/coverage -------------------------------------------------------------------------------- /packages/sdk-js/.npmignore: -------------------------------------------------------------------------------- 1 | rollup.config.js 2 | README.md 3 | src 4 | tsconfig.json 5 | .gitignore -------------------------------------------------------------------------------- /packages/sdk-js/README.md: -------------------------------------------------------------------------------- 1 | For documentation, make sure to check https://progressively.app. 2 | -------------------------------------------------------------------------------- /packages/sdk-js/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: "jsdom", 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/server-side/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | lib 6 | coverage -------------------------------------------------------------------------------- /packages/server-side/.npmignore: -------------------------------------------------------------------------------- 1 | rollup.config.js 2 | README.md 3 | src 4 | tsconfig.json 5 | .gitignore -------------------------------------------------------------------------------- /packages/server-side/README.md: -------------------------------------------------------------------------------- 1 | For documentation, make sure to check https://progressively.app. 2 | -------------------------------------------------------------------------------- /packages/server-side/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import terser from "@rollup/plugin-terser"; 3 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 4 | 5 | export default () => { 6 | return { 7 | input: "src/index.tsx", 8 | output: [ 9 | { 10 | file: "lib/cjs/index.cjs.js", 11 | format: "cjs", 12 | name: "progressively-react", 13 | }, 14 | { 15 | file: "lib/esm/index.mjs", 16 | format: "es", 17 | }, 18 | ], 19 | plugins: [ 20 | nodeResolve(), 21 | typescript({ 22 | tsconfig: "./tsconfig.json", 23 | }), 24 | terser(), 25 | ], 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/server-side/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "types", 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export type FlagDict = { [key: string]: boolean | string }; 2 | 3 | export type Fields = Record< 4 | string, 5 | string | number | boolean | null | undefined 6 | >; 7 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@progressively/types", 3 | "author": "mfrachet", 4 | "version": "1.0.0", 5 | "types": "./index.d.ts", 6 | "main": "index.d.ts" 7 | } 8 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "websites/*" 4 | - "example/*" 5 | - "!example/reactnative" 6 | -------------------------------------------------------------------------------- /scripts/docker-compose-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # only useful to create the DB tables in docker-compose.yml 4 | pnpm run db:prepare 5 | pnpm run start:backend -------------------------------------------------------------------------------- /websites/backend/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | *.env -------------------------------------------------------------------------------- /websites/backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /websites/backend/README.md: -------------------------------------------------------------------------------- 1 | For documentation, make sure to check https://progressively.app. 2 | -------------------------------------------------------------------------------- /websites/backend/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "plugins": ["@nestjs/swagger"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /websites/backend/src/activity-log/activity-log.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DatabaseModule } from '../database/database.module'; 3 | import { ActivityLogService } from './activity-log.service'; 4 | 5 | @Module({ 6 | imports: [DatabaseModule], 7 | providers: [ActivityLogService], 8 | exports: [ActivityLogService], 9 | }) 10 | export class ActivityLogModule {} 11 | -------------------------------------------------------------------------------- /websites/backend/src/activity-log/activity-log.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PrismaService } from '../database/prisma.service'; 3 | import { Activity } from './types'; 4 | 5 | @Injectable() 6 | export class ActivityLogService { 7 | constructor(private prisma: PrismaService) {} 8 | 9 | register(activity: Activity) { 10 | return this.prisma.activityLog.create({ 11 | data: { 12 | concernedEntity: activity.concernedEntity, 13 | type: activity.type, 14 | userUuid: activity.userId, 15 | flagUuid: activity.flagId, 16 | data: activity.data, 17 | }, 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /websites/backend/src/activity-log/types.ts: -------------------------------------------------------------------------------- 1 | export type ActivityType = 2 | | 'change-flag-status' 3 | | 'change-flag-percentage' 4 | | 'change-variants-percentage' 5 | | 'create-webhook' 6 | | 'create-strategy' 7 | | 'create-variant' 8 | | 'delete-webhook' 9 | | 'delete-variant' 10 | | 'delete-strategy' 11 | | 'edit-strategy'; 12 | 13 | export type ActivityEntity = 'flag'; 14 | 15 | export interface Activity { 16 | userId: string; 17 | flagId: string; 18 | type: ActivityType; 19 | concernedEntity: ActivityEntity; 20 | data?: string; 21 | } 22 | -------------------------------------------------------------------------------- /websites/backend/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService {} 5 | -------------------------------------------------------------------------------- /websites/backend/src/auth/strategies/jwt.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { OktaService } from '../okta.service'; 4 | 5 | const guards = OktaService.getOktaConfig().isOktaActivated 6 | ? ['bearer', 'jwt'] 7 | : 'jwt'; 8 | 9 | @Injectable() 10 | export class JwtAuthGuard extends AuthGuard(guards) {} 11 | -------------------------------------------------------------------------------- /websites/backend/src/caching/caching.module.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Module, OnModuleDestroy } from '@nestjs/common'; 2 | import { MakeCachingService } from './caching.service.factory'; 3 | import { ICachingService } from './types'; 4 | 5 | @Module({ 6 | providers: [ 7 | { 8 | provide: 'CachingService', 9 | useFactory: MakeCachingService, 10 | }, 11 | ], 12 | exports: ['CachingService'], 13 | }) 14 | export class CachingModule implements OnModuleDestroy { 15 | constructor( 16 | @Inject('CachingService') private readonly cachingService: ICachingService, 17 | ) {} 18 | 19 | async onModuleDestroy() { 20 | await this.cachingService.teardown(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /websites/backend/src/caching/caching.service.factory.ts: -------------------------------------------------------------------------------- 1 | import { ICachingService } from './types'; 2 | import { InMemoryService } from './concrete/InMemory.service'; 3 | import { RedisService } from './concrete/redis.service'; 4 | import { getEnv } from './getEnv'; 5 | 6 | export const MakeCachingService = (): ICachingService => { 7 | const env = getEnv(); 8 | 9 | if (env.RedisCachingUrl) { 10 | return new RedisService(); 11 | } 12 | 13 | return new InMemoryService(); 14 | }; 15 | -------------------------------------------------------------------------------- /websites/backend/src/caching/concrete/InMemory.service.ts: -------------------------------------------------------------------------------- 1 | import { ICachingService } from '../types'; 2 | 3 | export class InMemoryService implements ICachingService { 4 | private dict: Record; 5 | constructor() { 6 | this.dict = {}; 7 | } 8 | teardown() { 9 | return Promise.resolve(); 10 | } 11 | 12 | get(k: string) { 13 | return Promise.resolve(this.dict[k]); 14 | } 15 | 16 | async set(k: string, v: any) { 17 | this.dict[k] = v; 18 | } 19 | 20 | async del(k: string) { 21 | this.dict[k] = undefined; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /websites/backend/src/caching/constants.ts: -------------------------------------------------------------------------------- 1 | export const Time = { 2 | FiveHours: 60 * 60 * 5, 3 | HalfAnHour: 60 * 30, 4 | }; 5 | 6 | export type TimeType = keyof typeof Time; 7 | -------------------------------------------------------------------------------- /websites/backend/src/caching/getEnv.ts: -------------------------------------------------------------------------------- 1 | export const getEnv = () => ({ 2 | RedisCachingUrl: process.env.REDIS_CACHING_URL, 3 | }); 4 | -------------------------------------------------------------------------------- /websites/backend/src/caching/types.ts: -------------------------------------------------------------------------------- 1 | import { TimeType } from './constants'; 2 | 3 | export interface ICachingService { 4 | set: (k: string, v: any, timeInS?: TimeType) => Promise; 5 | get: (k: string) => Promise; 6 | teardown: () => Promise; 7 | del: (k: string) => Promise; 8 | } 9 | -------------------------------------------------------------------------------- /websites/backend/src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PrismaService } from './prisma.service'; 3 | import { getClient } from '@progressively/database'; 4 | 5 | @Module({ 6 | providers: [ 7 | PrismaService, 8 | { 9 | provide: 'ClickhouseService', 10 | useFactory: () => getClient(), 11 | }, 12 | ], 13 | exports: [PrismaService, 'ClickhouseService'], 14 | }) 15 | export class DatabaseModule {} 16 | -------------------------------------------------------------------------------- /websites/backend/src/events/events.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { EventsService } from './events.service'; 3 | import { DatabaseModule } from '../database/database.module'; 4 | import { CachingModule } from '../caching/caching.module'; 5 | 6 | @Module({ 7 | imports: [DatabaseModule, CachingModule], 8 | providers: [EventsService], 9 | exports: [EventsService], 10 | }) 11 | export class EventsModule {} 12 | -------------------------------------------------------------------------------- /websites/backend/src/events/types.ts: -------------------------------------------------------------------------------- 1 | export interface Variant { 2 | uuid: string; 3 | rolloutPercentage: number; 4 | isControl: boolean; 5 | value: string; 6 | } 7 | 8 | export const ReservedEventName = { 9 | PageView: 'Page View', 10 | }; 11 | 12 | export type Timeframe = 7 | 30 | 90; 13 | export const Timeframes: Array = ['7', '30', '90']; 14 | -------------------------------------------------------------------------------- /websites/backend/src/flags/flags.status.ts: -------------------------------------------------------------------------------- 1 | export enum FlagStatus { 2 | ACTIVATED = 'ACTIVATED', 3 | NOT_ACTIVATED = 'NOT_ACTIVATED', 4 | INACTIVE = 'INACTIVE', 5 | } 6 | -------------------------------------------------------------------------------- /websites/backend/src/flags/utils.ts: -------------------------------------------------------------------------------- 1 | import { FlagStatus } from './flags.status'; 2 | 3 | export const strToFlagStatus = (strStatus: string): FlagStatus | undefined => { 4 | let status: FlagStatus; 5 | 6 | if (strStatus === FlagStatus.ACTIVATED) { 7 | status = FlagStatus.ACTIVATED; 8 | } else if (strStatus === FlagStatus.INACTIVE) { 9 | status = FlagStatus.INACTIVE; 10 | } else if (strStatus === FlagStatus.NOT_ACTIVATED) { 11 | status = FlagStatus.NOT_ACTIVATED; 12 | } 13 | 14 | return status; 15 | }; 16 | -------------------------------------------------------------------------------- /websites/backend/src/funnels/funnels.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FunnelsService } from './funnels.service'; 3 | import { DatabaseModule } from '../database/database.module'; 4 | import { EventsModule } from '../events/events.module'; 5 | import { CachingModule } from '../caching/caching.module'; 6 | 7 | @Module({ 8 | imports: [DatabaseModule, EventsModule, CachingModule], 9 | providers: [FunnelsService], 10 | exports: [FunnelsService], 11 | }) 12 | export class FunnelsModule {} 13 | -------------------------------------------------------------------------------- /websites/backend/src/jwtConstants.ts: -------------------------------------------------------------------------------- 1 | export const jwtConstants = () => ({ 2 | AccessTokenSecret: process.env.ACCESS_TOKEN_SECRET, 3 | AccessTokenExpire: parseInt(process.env.ACCESS_TOKEN_EXPIRES), 4 | RefreshTokenSecret: process.env.REFRESH_TOKEN_SECRET, 5 | RefreshTokenExpire: parseInt(process.env.REFRESH_TOKEN_EXPIRES), 6 | }); 7 | -------------------------------------------------------------------------------- /websites/backend/src/mail/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import { LinkProps, Link as RawLink } from '@react-email/components'; 2 | 3 | export const Link = (props: LinkProps) => { 4 | return ; 5 | }; 6 | -------------------------------------------------------------------------------- /websites/backend/src/mail/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import { Img } from '@react-email/components'; 2 | 3 | export const Logo = (props: any) => { 4 | return ( 5 | Cat 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /websites/backend/src/mail/components/PrimaryButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@react-email/components'; 2 | import { ReactNode } from 'react'; 3 | 4 | export interface ButtonProps { 5 | href: string; 6 | children: ReactNode; 7 | } 8 | 9 | export const PrimaryButton = (props: ButtonProps) => { 10 | return ( 11 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Breadcrumbs/types.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export interface Crumb { 4 | link: string; 5 | label: string; 6 | forceNotCurrent?: boolean; 7 | icon?: React.ReactNode; 8 | isRoot?: boolean; 9 | isProject?: boolean; 10 | isFlag?: boolean; 11 | menuItems?: Array<{ 12 | label: string; 13 | href: string; 14 | }>; 15 | menuLabel?: string; 16 | } 17 | 18 | export type Crumbs = Array; 19 | 20 | export interface DesktopNavProps { 21 | crumbs: Crumbs; 22 | } 23 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Buttons/CreateButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps } from "./Button"; 2 | import { AiOutlinePlus } from "react-icons/ai"; 3 | 4 | export const CreateButton = ({ children, ...props }: ButtonProps) => { 5 | return ( 6 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Buttons/DeleteButton.tsx: -------------------------------------------------------------------------------- 1 | import { FiTrash } from "react-icons/fi"; 2 | import { Button, ButtonProps } from "./Button"; 3 | 4 | export const DeleteButton = ({ children, ...props }: ButtonProps) => { 5 | return ( 6 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Buttons/EditButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps } from "./Button"; 2 | import { AiOutlineEdit } from "react-icons/ai"; 3 | 4 | export const EditButton = ({ children, ...props }: ButtonProps) => { 5 | return ( 6 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Buttons/SubmitButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonProps } from "./Button"; 2 | import { FiEdit } from "react-icons/fi"; 3 | 4 | export const SubmitButton = ({ children, ...props }: ButtonProps) => { 5 | return ( 6 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Container.tsx: -------------------------------------------------------------------------------- 1 | export interface ContainerProps { 2 | children: React.ReactNode; 3 | } 4 | 5 | export const Container = ({ children }: ContainerProps) => { 6 | return
{children}
; 7 | }; 8 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Dl.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "./Typography"; 2 | 3 | export interface DlProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | export const Dl = ({ children }: DlProps) => { 8 | return ( 9 |
{children}
10 | ); 11 | }; 12 | 13 | export const Dt = ({ children }: DlProps) => { 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | }; 20 | 21 | export const Dd = ({ children }: DlProps) => { 22 | return {children}; 23 | }; 24 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Entity/EntityField.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from "../Typography"; 2 | 3 | export interface EntityFieldProps { 4 | name: string; 5 | value: React.ReactNode; 6 | } 7 | 8 | export const EntityField = ({ name, value }: EntityFieldProps) => { 9 | return ( 10 |
11 | {name} 12 | 13 | {value} 14 | 15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /websites/frontend/app/components/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import { FiExternalLink } from "react-icons/fi"; 2 | import { Link } from "./Link"; 3 | 4 | export interface ExternalLinkProps { 5 | href: string; 6 | children: React.ReactNode; 7 | } 8 | 9 | export const ExternalLink = ({ href, children }: ExternalLinkProps) => { 10 | return ( 11 | 12 |
13 | {children} 14 |
15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Fields/FormGroup.tsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "../Stack"; 2 | 3 | export interface FormGroupProps { 4 | children: React.ReactNode; 5 | } 6 | export const FormGroup = ({ children }: FormGroupProps) => { 7 | return {children}; 8 | }; 9 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Fields/Label.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | 3 | export interface LabelProps extends HTMLAttributes { 4 | children: React.ReactNode; 5 | as?: "legend" | "span" | undefined; 6 | htmlFor?: string; 7 | className?: string; 8 | } 9 | 10 | export const Label = ({ 11 | children, 12 | as: asComponent, 13 | htmlFor, 14 | className, 15 | ...props 16 | }: LabelProps) => { 17 | const Component = asComponent || "label"; 18 | 19 | return ( 20 | 25 | {children} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Fields/Radio.tsx: -------------------------------------------------------------------------------- 1 | export const Radio = (props: React.HTMLAttributes) => { 2 | const classes = 3 | "custom-radio appearance-none m-0 w-4 h-4 border border-gray-200 hover:border-gray-400 cursor-pointer rounded-full flex items-center justify-center before:content-[''] before:h-2 before:w-2 before:rounded-full checked:before:h-2 checked:before:w-2"; 4 | 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /websites/frontend/app/components/HideMobile.tsx: -------------------------------------------------------------------------------- 1 | export interface WithAs extends React.HTMLAttributes { 2 | as?: any; 3 | } 4 | 5 | export const HideDesktop = ({ 6 | as: Component = "span", 7 | className, 8 | ...props 9 | }: WithAs) => { 10 | return ; 11 | }; 12 | 13 | export const HideTablet = ({ 14 | as: Component = "span", 15 | className, 16 | ...props 17 | }: WithAs) => { 18 | return ; 19 | }; 20 | 21 | export const HideMobile = ({ as: Component = "span", ...props }: WithAs) => { 22 | return ; 23 | }; 24 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Icons/FlagIcon.tsx: -------------------------------------------------------------------------------- 1 | export { TbFlag3 as FlagIcon } from "react-icons/tb"; 2 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Icons/HomeIcon.tsx: -------------------------------------------------------------------------------- 1 | export { BiHome as HomeIcon } from "react-icons/bi"; 2 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Icons/ProjectIcon.tsx: -------------------------------------------------------------------------------- 1 | export { GoProject as ProjectIcon } from "react-icons/go"; 2 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Icons/SettingsIcon.tsx: -------------------------------------------------------------------------------- 1 | export { BiCog as SettingsIcon } from "react-icons/bi"; 2 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Icons/UserIcon.tsx: -------------------------------------------------------------------------------- 1 | import { TbUser } from "react-icons/tb"; 2 | 3 | export const UserIcon = () => ; 4 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Icons/VariantIcon.tsx: -------------------------------------------------------------------------------- 1 | export { AiOutlineAppstore as VariantIcon } from "react-icons/ai"; 2 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Inert/Inert.tsx: -------------------------------------------------------------------------------- 1 | import { useInert } from "./hooks/useInert"; 2 | 3 | export interface InertWhenNavOpenedProps { 4 | children: React.ReactNode; 5 | className?: string; 6 | } 7 | 8 | export const Inert = ({ children, ...props }: InertWhenNavOpenedProps) => { 9 | const { inert } = useInert(); 10 | 11 | return ( 12 |
17 | {children} 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Inert/context/InertContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export interface InertContextType { 4 | setInert: (x: boolean) => void; 5 | inert: boolean; 6 | } 7 | 8 | export const InertContext = createContext({ 9 | inert: false, 10 | setInert: () => {}, 11 | }); 12 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Inert/hooks/useInert.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { InertContext } from "../context/InertContext"; 3 | 4 | export const useInert = () => useContext(InertContext); 5 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Inert/hooks/useSetInert.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { useInert } from "./useInert"; 3 | 4 | export const useSetInert = () => { 5 | const { setInert } = useInert(); 6 | 7 | useEffect(() => { 8 | setInert(true); 9 | 10 | return () => { 11 | setInert(false); 12 | }; 13 | // eslint-disable-next-line react-hooks/exhaustive-deps 14 | }, []); 15 | }; 16 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Inert/providers/InertProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { InertContext } from "../context/InertContext"; 3 | 4 | export interface InerProviderProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | export const InertProvider = ({ children }: InerProviderProps) => { 9 | const [inert, setInert] = useState(false); 10 | 11 | useEffect(() => { 12 | document.body.style.overflow = inert ? "hidden" : "revert"; 13 | }, [inert]); 14 | 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /websites/frontend/app/components/LineChart/constants.ts: -------------------------------------------------------------------------------- 1 | export const LegendOffset = 72; 2 | export const DarkTextColor = "rgba(255, 255, 255, 0.59)"; 3 | export const LightTextColor = "rgba(0, 0, 0, 0.44)"; 4 | export const DashedGridSize = "2 3"; 5 | export const DarkDashedGridColor = "rgba(255, 255, 255, 0.1)"; 6 | export const LightDashedGridColor = "rgba(0, 0, 0, 0.1)"; 7 | export const LegendFontSize = 14; 8 | export const AxisTickSize = 0; 9 | export const AxisTickPadding = 16; 10 | -------------------------------------------------------------------------------- /websites/frontend/app/components/LineChart/formatters.ts: -------------------------------------------------------------------------------- 1 | export const formatDate = ( 2 | x: any, 3 | options: Intl.DateTimeFormatOptions = { 4 | month: "2-digit", 5 | day: "2-digit", 6 | year: "2-digit", 7 | } 8 | ) => { 9 | const formatter = new Intl.DateTimeFormat(undefined, options); 10 | return formatter.format(new Date(x)); 11 | }; 12 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | import { SkipNavContent } from "./SkipNav"; 3 | 4 | export const Main = (props: HTMLAttributes) => ( 5 | 6 |
7 | 8 | ); 9 | -------------------------------------------------------------------------------- /websites/frontend/app/components/NumberValue.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const NumberValue = ({ value }: { value: number }) => { 4 | const [formattedValue, setFormattedValue] = useState("0"); 5 | 6 | useEffect(() => { 7 | const formatter = new Intl.NumberFormat("en-US", { 8 | notation: "compact", 9 | maximumFractionDigits: 1, 10 | }); 11 | setFormattedValue(formatter.format(value)); 12 | }, [value]); 13 | 14 | return <>{formattedValue}; 15 | }; 16 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Separator.tsx: -------------------------------------------------------------------------------- 1 | export interface SeparatorProps { 2 | className?: string; 3 | } 4 | 5 | export const Separator = ({ className }: SeparatorProps) => ( 6 |
7 | ); 8 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLAttributes } from "react"; 2 | import { CgSpinner } from "react-icons/cg"; 3 | 4 | export const Spinner = ({ 5 | className, 6 | ...props 7 | }: HTMLAttributes) => { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Switch/Label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as LabelPrimitive from "@radix-ui/react-label"; 3 | 4 | const Label = React.forwardRef< 5 | React.ElementRef, 6 | React.ComponentPropsWithoutRef 7 | >((props, ref) => ( 8 | 15 | )); 16 | Label.displayName = LabelPrimitive.Root.displayName; 17 | 18 | export { Label }; 19 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Tooltip as RawTooltip, 3 | TooltipContent, 4 | TooltipProvider, 5 | TooltipTrigger, 6 | } from "./RawTooltip"; 7 | 8 | export interface TooltipProps { 9 | children: React.ReactNode; 10 | tooltip: React.ReactNode; 11 | } 12 | 13 | export const Tooltip = ({ children, tooltip }: TooltipProps) => { 14 | return ( 15 | 16 | 17 | {children} 18 | {tooltip} 19 | 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Typography.tsx: -------------------------------------------------------------------------------- 1 | import React, { ElementType, forwardRef } from "react"; 2 | 3 | export interface TypographyProps 4 | extends React.HTMLAttributes { 5 | as?: ElementType; 6 | } 7 | 8 | export const Typography = forwardRef( 9 | ({ className = "", as: asHTML, ...props }: TypographyProps, ref) => { 10 | const Root = asHTML || "p"; 11 | 12 | return ( 13 | 14 | ); 15 | } 16 | ); 17 | 18 | Typography.displayName = "Typography"; 19 | -------------------------------------------------------------------------------- /websites/frontend/app/components/Ul.tsx: -------------------------------------------------------------------------------- 1 | export interface UlProps { 2 | children: React.ReactNode; 3 | } 4 | 5 | export const Ul = ({ children }: UlProps) => { 6 | return
    {children}
; 7 | }; 8 | 9 | export interface LiProps { 10 | children: React.ReactNode; 11 | id?: string; 12 | } 13 | 14 | export const Li = ({ children, id }: LiProps) => { 15 | return
  • {children}
  • ; 16 | }; 17 | -------------------------------------------------------------------------------- /websites/frontend/app/components/VisuallyHidden.tsx: -------------------------------------------------------------------------------- 1 | const vhStyles = { 2 | border: "0px", 3 | clip: "rect(0 0 0 0)", 4 | height: "1px", 5 | margin: "-1px", 6 | overflow: "hidden", 7 | padding: "0px", 8 | position: "absolute", 9 | width: "1px", 10 | }; 11 | 12 | export interface VisuallyHiddenProps { 13 | children: React.ReactNode; 14 | id?: string; 15 | } 16 | export const VisuallyHidden = ({ children, id }: VisuallyHiddenProps) => { 17 | return ( 18 |
    19 | {children} 20 |
    21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /websites/frontend/app/constants.ts: -------------------------------------------------------------------------------- 1 | export const Constants = { 2 | BackendUrl: process.env.BACKEND_URL || "http://localhost:4000", 3 | }; 4 | -------------------------------------------------------------------------------- /websites/frontend/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * By default, Remix will handle hydrating your app on the client for you. 3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ 4 | * For more information, see https://remix.run/file-conventions/entry.client 5 | */ 6 | 7 | import { RemixBrowser } from "@remix-run/react"; 8 | import { startTransition, StrictMode } from "react"; 9 | import { hydrateRoot } from "react-dom/client"; 10 | 11 | startTransition(() => { 12 | hydrateRoot( 13 | document, 14 | 15 | 16 | 17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /websites/frontend/app/layouts/CreateEntityTitle.tsx: -------------------------------------------------------------------------------- 1 | export interface CreateEntityTitleProps { 2 | children: React.ReactNode; 3 | } 4 | export const CreateEntityTitle = ({ children }: CreateEntityTitleProps) => { 5 | return

    {children}

    ; 6 | }; 7 | -------------------------------------------------------------------------------- /websites/frontend/app/layouts/DeleteEntityTitle.tsx: -------------------------------------------------------------------------------- 1 | export interface DeleteEntityTitleProps { 2 | children: React.ReactNode; 3 | } 4 | export const DeleteEntityTitle = ({ children }: DeleteEntityTitleProps) => { 5 | return

    {children}

    ; 6 | }; 7 | -------------------------------------------------------------------------------- /websites/frontend/app/layouts/SearchLayout.tsx: -------------------------------------------------------------------------------- 1 | export interface SearchLayoutProps { 2 | children: React.ReactNode; 3 | actions?: React.ReactNode; 4 | } 5 | 6 | export const SearchLayout = ({ children, actions }: SearchLayoutProps) => { 7 | return ( 8 |
    9 |
    {children}
    10 | {actions} 11 |
    12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/a11y/utils/getFocusableNodes.ts: -------------------------------------------------------------------------------- 1 | export const getFocusableNodes = (node: HTMLElement) => { 2 | const nodes = [ 3 | ...node.querySelectorAll( 4 | 'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"])' 5 | ), 6 | ]; 7 | return nodes.filter((node) => { 8 | if (node.hasAttribute("disabled")) return false; 9 | 10 | return node.getAttribute("tabindex") !== "-1"; 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/a11y/utils/keyboardKeys.ts: -------------------------------------------------------------------------------- 1 | export const KeyboardKeys = { 2 | DOWN: "ArrowDown", 3 | UP: "ArrowUp", 4 | RIGHT: "ArrowRight", 5 | LEFT: "ArrowLeft", 6 | ESCAPE: "Escape", 7 | ENTER: "Enter", 8 | SPACE: " ", 9 | TAB: "Tab", 10 | END: "End", 11 | HOME: "Home", 12 | DELETE: "Delete", 13 | PAGE_UP: "PageUp", 14 | PAGE_DOWN: "PageDown", 15 | BACKSPACE: "Backspace", 16 | CLEAR: "Clear", 17 | }; 18 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/activity/services/getActivity.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | import { Activity } from "../types"; 3 | 4 | export const getActivity = ( 5 | flagId: string, 6 | accessToken: string 7 | ): Promise> => { 8 | return fetch(`${Constants.BackendUrl}/flags/${flagId}/activity`, { 9 | headers: { 10 | Authorization: `Bearer ${accessToken}`, 11 | }, 12 | }).then((res) => { 13 | if (!res.ok) { 14 | throw new Error("Woops! Something went wrong in the server."); 15 | } 16 | return res.json(); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/activity/types.ts: -------------------------------------------------------------------------------- 1 | export type ActivityType = 2 | | "change-flag-status" 3 | | "change-flag-percentage" 4 | | "change-variants-percentage" 5 | | "create-webhook" 6 | | "create-variant" 7 | | "create-strategy" 8 | | "delete-webhook" 9 | | "delete-variant" 10 | | "delete-rule" 11 | | "delete-strategy" 12 | | "edit-strategy"; 13 | 14 | export type ActivityEntity = "flag"; 15 | 16 | export interface Activity { 17 | concernedEntity: ActivityEntity; 18 | data?: any; 19 | flagUuid: string; 20 | id: number; 21 | type: ActivityType; 22 | userUuid: string; 23 | utc: string; 24 | user: { 25 | fullname: string; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/auth/hooks/useOkta.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { OktaserviceClientSide } from "../services/okta"; 3 | import { OktaConfig } from "../types"; 4 | 5 | export const useOkta = (oktaConfig: OktaConfig) => { 6 | const [okta, setOkta] = useState>(); 7 | 8 | useEffect(() => { 9 | const oktaSrv = OktaserviceClientSide(oktaConfig); 10 | setOkta(oktaSrv); 11 | }, []); 12 | 13 | return okta; 14 | }; 15 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/auth/services/authenticate.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const authenticate = (username: string, password: string) => 4 | fetch(`${Constants.BackendUrl}/auth/login`, { 5 | method: "POST", 6 | body: JSON.stringify({ username, password }), 7 | headers: { "Content-Type": "application/json" }, 8 | }); 9 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/auth/services/get-okta-config.ts: -------------------------------------------------------------------------------- 1 | import { OktaConfig } from "../types"; 2 | 3 | export const getOktaConfig = () => { 4 | const oktaConfig: OktaConfig = { 5 | issuer: String(process.env.OKTA_ISSUER), 6 | clientId: String(process.env.OKTA_CLIENT_ID), 7 | isOktaActivated: Boolean( 8 | process.env.OKTA_ISSUER && process.env.OKTA_CLIENT_ID 9 | ), 10 | }; 11 | 12 | return oktaConfig; 13 | }; 14 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/auth/types.ts: -------------------------------------------------------------------------------- 1 | export type AuthCredentials = { 2 | email: string; 3 | password: string; 4 | }; 5 | 6 | export interface OktaConfig { 7 | issuer: string; 8 | clientId: string; 9 | isOktaActivated: boolean; 10 | } 11 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/auth/validators/validate-signin-form.ts: -------------------------------------------------------------------------------- 1 | import { validateEmail } from "../../forms/utils/validateEmail"; 2 | import { validatePassword } from "../../forms/utils/validatePassword"; 3 | import { AuthCredentials } from "../types"; 4 | 5 | export const validateSigninForm = (values: Partial) => { 6 | const errors: Partial = {}; 7 | 8 | const emailError = validateEmail(values.email); 9 | const passwordError = validatePassword(values.password); 10 | 11 | if (emailError) { 12 | errors.email = emailError; 13 | } 14 | 15 | if (passwordError) { 16 | errors.password = passwordError; 17 | } 18 | 19 | return errors; 20 | }; 21 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/billing/services/checkout.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const checkout = (priceId: string, accessToken: string) => 4 | fetch(`${Constants.BackendUrl}/checkout`, { 5 | method: "POST", 6 | body: JSON.stringify({ priceId }), 7 | headers: { 8 | Authorization: `Bearer ${accessToken}`, 9 | "Content-Type": "application/json", 10 | }, 11 | }).then((res) => { 12 | if (!res.ok) { 13 | throw new Error("Woops! Something went wrong in the server."); 14 | } 15 | 16 | return res.json(); 17 | }); 18 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/events/types.ts: -------------------------------------------------------------------------------- 1 | export const ReservedEventName = { 2 | PageView: "Page View", 3 | }; 4 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/components/FlagStatus.tsx: -------------------------------------------------------------------------------- 1 | import { Tag } from "~/components/Tag"; 2 | import { FlagStatus as FlagStatusRaw } from "../types"; 3 | 4 | export interface FlagStatusProps { 5 | value: FlagStatusRaw; 6 | } 7 | 8 | export const FlagStatus = ({ value }: FlagStatusProps) => { 9 | if (value === FlagStatusRaw.ACTIVATED) { 10 | return ( 11 | 12 | Activated 13 | 14 | ); 15 | } 16 | 17 | if (value === FlagStatusRaw.NOT_ACTIVATED) { 18 | return ( 19 | 20 | Not activated 21 | 22 | ); 23 | } 24 | 25 | return Inactive; 26 | }; 27 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/contexts/FlagContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { Flag } from "../types"; 3 | 4 | export interface FlagContextType { 5 | flag: Flag; 6 | } 7 | 8 | export const FlagContext = createContext<{ flag: Flag }>({ flag: {} }); 9 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/contexts/FlagProvider.tsx: -------------------------------------------------------------------------------- 1 | import { Flag } from "../types"; 2 | import { FlagContext } from "./FlagContext"; 3 | 4 | export interface FlagProviderProps { 5 | children: React.ReactNode; 6 | flag: Flag; 7 | } 8 | 9 | export const FlagProvider = ({ children, flag }: FlagProviderProps) => { 10 | return ( 11 | {children} 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/contexts/useFlag.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { FlagContext } from "./FlagContext"; 3 | 4 | export const useFlag = () => useContext(FlagContext); 5 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/form-actions/toggleFlagAction.ts: -------------------------------------------------------------------------------- 1 | import { Params } from "@remix-run/react"; 2 | import { activateFlag } from "../services/activateFlag"; 3 | import { FlagStatus } from "../types"; 4 | 5 | export const toggleFlagAction = ( 6 | formData: FormData, 7 | params: Params, 8 | authCookie: string 9 | ) => { 10 | const nextStatus = formData.get("nextStatus"); 11 | const flagId = formData.get("flagId"); 12 | 13 | if (nextStatus && flagId) { 14 | return activateFlag(String(flagId), nextStatus as FlagStatus, authCookie); 15 | } 16 | 17 | return null; 18 | }; 19 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/services/activateFlag.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | import { FlagStatus } from "../types"; 3 | 4 | export const activateFlag = ( 5 | flagId: string, 6 | status: FlagStatus, 7 | accessToken: string 8 | ) => 9 | fetch(`${Constants.BackendUrl}/flags/${flagId}`, { 10 | method: "PUT", 11 | body: JSON.stringify({ status }), 12 | headers: { 13 | Authorization: `Bearer ${accessToken}`, 14 | "Content-Type": "application/json", 15 | }, 16 | }).then((res) => res.json()); 17 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/services/createFlag.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const createFlag = ( 4 | projectId: string, 5 | name: string, 6 | description: string, 7 | accessToken: string 8 | ) => 9 | fetch(`${Constants.BackendUrl}/projects/${projectId}/flags`, { 10 | method: "POST", 11 | body: JSON.stringify({ name, description }), 12 | headers: { 13 | Authorization: `Bearer ${accessToken}`, 14 | "Content-Type": "application/json", 15 | }, 16 | }).then((res) => { 17 | if (!res.ok) { 18 | throw new Error("The flag name is already used."); 19 | } 20 | return res.json(); 21 | }); 22 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/services/deleteFlag.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const deleteFlag = (flagId: string, accessToken: string) => 4 | fetch(`${Constants.BackendUrl}/flags/${flagId}`, { 5 | method: "DELETE", 6 | headers: { 7 | Authorization: `Bearer ${accessToken}`, 8 | "Content-Type": "application/json", 9 | }, 10 | }).then((res) => { 11 | if (!res.ok) { 12 | throw new Error("You are not authorized to remove this flag."); 13 | } 14 | return res.json(); 15 | }); 16 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/services/editFlag.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const editFlag = ( 4 | projectId: string, 5 | flagId: string, 6 | name: string, 7 | description: string, 8 | accessToken: string 9 | ) => 10 | fetch(`${Constants.BackendUrl}/projects/${projectId}/flags/${flagId}`, { 11 | method: "PUT", 12 | body: JSON.stringify({ name, description }), 13 | headers: { 14 | Authorization: `Bearer ${accessToken}`, 15 | "Content-Type": "application/json", 16 | }, 17 | }).then((res) => { 18 | if (!res.ok) { 19 | throw new Error("The flag name is already used."); 20 | } 21 | return res.json(); 22 | }); 23 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/services/getFlagById.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getFlagById = async (flagId: string, accessToken: string) => { 4 | return fetch(`${Constants.BackendUrl}/flags/${flagId}`, { 5 | headers: { 6 | Authorization: `Bearer ${accessToken}`, 7 | }, 8 | }).then((res) => res.json()); 9 | }; 10 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/services/getFlagHits.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getFlagHits = async ( 4 | flagId: string, 5 | timeframe: number, 6 | accessToken: string 7 | ) => { 8 | const url = new URL(`${Constants.BackendUrl}/flags/${flagId}/hits`); 9 | 10 | url.searchParams.set("timeframe", String(timeframe)); 11 | 12 | return fetch(url, { 13 | headers: { 14 | Authorization: `Bearer ${accessToken}`, 15 | }, 16 | }).then((res) => res.json()); 17 | }; 18 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/services/getFlagMetaTitle.ts: -------------------------------------------------------------------------------- 1 | import { Flag } from "../types"; 2 | 3 | export const getFlagMetaTitle = (matches: any): string => { 4 | const flag: Flag | undefined = matches.find( 5 | (match: any) => match.id === "routes/dashboard.projects.$id.flags.$flagId" 6 | )?.data?.flag; 7 | 8 | return flag?.name || ""; 9 | }; 10 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/flags/validators/validateFlagShape.ts: -------------------------------------------------------------------------------- 1 | import { CreateFlagDTO } from "../types"; 2 | 3 | export const validateFlagShape = (values: CreateFlagDTO) => { 4 | const errors: Partial = {}; 5 | 6 | if (!values.name) { 7 | errors.name = "The name field is required, make sure to have one."; 8 | } 9 | 10 | if (!values.description) { 11 | errors.description = 12 | "The description field is required, make sure to have one."; 13 | } 14 | 15 | return errors; 16 | }; 17 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/forms/utils/validateEmail.ts: -------------------------------------------------------------------------------- 1 | export const validateEmail = (email?: string): string | undefined => { 2 | if (!email) { 3 | return "The email field is required."; 4 | } 5 | 6 | if (!/^[\w%+.-]+@[\d.a-z-]+\.[a-z]{2,}$/i.test(email)) { 7 | return `The provided email address is not valid. It should look like "jane.doe@domain.com".`; 8 | } 9 | 10 | return undefined; 11 | }; 12 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/funnels/types.ts: -------------------------------------------------------------------------------- 1 | export interface Funnel { 2 | uuid: string; 3 | name: string; 4 | createdAt: Date; 5 | projectUuid: string; 6 | } 7 | 8 | export interface FunnelChart { 9 | uuid: string; 10 | name: string; 11 | funnelsEntries: Array<{ 12 | name: string; 13 | count: number; 14 | }>; 15 | } 16 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/instructions/helpers/transform.ts: -------------------------------------------------------------------------------- 1 | import { codeToHtml } from "shiki"; 2 | 3 | export const transform = async (code: string) => { 4 | const html = await codeToHtml(code, { 5 | lang: "javascript", 6 | theme: "slack-ochin", 7 | }); 8 | 9 | return html; 10 | }; 11 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/instructions/samples/getAnalyticsSample.ts: -------------------------------------------------------------------------------- 1 | import { transform } from "../helpers/transform"; 2 | import { SampleReturn } from "./types"; 3 | 4 | export const setupAnalytics = async (clientKey: string): SampleReturn => { 5 | const installation = ``; 6 | const rawCode = ` 15 | `; 16 | 17 | return { rawCode, html: await transform(rawCode), installation }; 18 | }; 19 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/instructions/samples/getSdkJsSample.ts: -------------------------------------------------------------------------------- 1 | import { transform } from "../helpers/transform"; 2 | import { SampleReturn } from "./types"; 3 | 4 | export const setupSdkJs = async (clientKey: string): SampleReturn => { 5 | const installation = `$ pnpm add @progressively/sdk-js`; 6 | const rawCode = `import { Progressively } from "@progressively/sdk-js"; 7 | 8 | const sdk = Progressively.init("${clientKey}"); 9 | const { flags } = await sdk.loadFlags(); 10 | `; 11 | 12 | return { rawCode, html: await transform(rawCode), installation }; 13 | }; 14 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/instructions/samples/types.ts: -------------------------------------------------------------------------------- 1 | export interface Sample { 2 | html: string; 3 | rawCode: string; 4 | installation: string; 5 | } 6 | 7 | export type SampleReturn = Promise; 8 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/misc/components/FormattedDate.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { formatDate } from "../utils/formatDate"; 3 | 4 | export const FormattedDate = ({ utc }: { utc: string }) => { 5 | const [formatted, setFormatted] = useState(); 6 | 7 | useEffect(() => { 8 | setFormatted(formatDate(utc)); 9 | }, []); 10 | 11 | if (!formatted) return null; 12 | 13 | return ; 14 | }; 15 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/misc/hooks/useHydrated.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | // Putting this as global, 4 | // We want this to resolve the fastest 5 | // When it's true, it means that the component has already hydrated 6 | let hydrated = false; 7 | 8 | export const useHydrated = () => { 9 | const [isHydrated, setIsHydrated] = useState(hydrated); 10 | 11 | useEffect(() => { 12 | hydrated = true; 13 | setIsHydrated(true); 14 | }, []); 15 | 16 | return isHydrated; 17 | }; 18 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/misc/utils/calculateGrowthRate.ts: -------------------------------------------------------------------------------- 1 | export const calculateGrowthRate = ( 2 | initialValue: number, 3 | finalValue: number 4 | ) => { 5 | if (initialValue === 0) { 6 | return 0; 7 | } 8 | 9 | const rateOfChange = Math.round( 10 | ((finalValue - initialValue) / initialValue) * 100 11 | ); 12 | 13 | return rateOfChange; 14 | }; 15 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/misc/utils/closestFocusable.ts: -------------------------------------------------------------------------------- 1 | export const closestFocusable = (el: HTMLElement, selector: string) => { 2 | const nextRow = el.querySelector(selector) as HTMLElement | undefined; 3 | 4 | if (!nextRow) return null; 5 | 6 | if (nextRow.getAttribute("tabindex")) { 7 | return nextRow; 8 | } 9 | 10 | return nextRow.querySelector("[tabindex]") as HTMLElement | null; 11 | }; 12 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/misc/utils/closestWithAttribute.ts: -------------------------------------------------------------------------------- 1 | export const closestWithAttribute = (el: HTMLElement, attribute: string) => { 2 | if (el.getAttribute(attribute)) { 3 | return el; 4 | } 5 | 6 | return el.closest(`[${attribute}]`); 7 | }; 8 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/misc/utils/formatDate.ts: -------------------------------------------------------------------------------- 1 | export const formatDate = (utc: string) => { 2 | const options = { 3 | year: "numeric", 4 | month: "numeric", 5 | day: "numeric", 6 | hour: "numeric", 7 | minute: "numeric", 8 | second: "numeric", 9 | hour12: false, 10 | }; 11 | 12 | return new Intl.DateTimeFormat("default", options).format(new Date(utc)); 13 | }; 14 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/misc/utils/formatDateAgo.ts: -------------------------------------------------------------------------------- 1 | import { format } from "timeago.js"; 2 | 3 | export const formatDateAgo = (utc: string) => { 4 | return format(utc); 5 | }; 6 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/misc/utils/getEnvVar.ts: -------------------------------------------------------------------------------- 1 | export const getEnvVar = (envKey: string) => process.env[envKey]; 2 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/misc/utils/toPercentage.ts: -------------------------------------------------------------------------------- 1 | export const toPercentage = (n: number) => { 2 | return (n * 100).toFixed(2); 3 | }; 4 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/payments/components/CheckoutForm.tsx: -------------------------------------------------------------------------------- 1 | import { Form } from "@remix-run/react"; 2 | import { TextInput } from "~/components/Fields/TextInput"; 3 | import { Button } from "~/components/Buttons/Button"; 4 | 5 | export const CheckoutForm = () => { 6 | return ( 7 |
    8 | 9 | 10 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/payments/services/getEventUsage.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getEventUsage = async (projectId: string, accessToken: string) => 4 | fetch(`${Constants.BackendUrl}/payments/${projectId}/usage`, { 5 | headers: { 6 | Authorization: `Bearer ${accessToken}`, 7 | }, 8 | }).then((res) => res.json()); 9 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/contexts/ProjectContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { Project } from "../types"; 3 | 4 | export interface ProjectContextType { 5 | project: Project; 6 | userRole: string; 7 | } 8 | 9 | export const ProjectContext = createContext({}); 10 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/contexts/ProjectProvider.tsx: -------------------------------------------------------------------------------- 1 | import { Project } from "../types"; 2 | import { ProjectContext } from "./ProjectContext"; 3 | 4 | export interface ProjectProviderProps { 5 | children: React.ReactNode; 6 | project: Project; 7 | userRole: string; 8 | } 9 | 10 | export const ProjectProvider = ({ 11 | children, 12 | project, 13 | userRole, 14 | }: ProjectProviderProps) => { 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/contexts/ProjectsContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { Project } from "../types"; 3 | 4 | export interface ProjectsContextType { 5 | projects: Array; 6 | } 7 | 8 | export const ProjectsContext = createContext({ 9 | projects: [], 10 | }); 11 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/contexts/ProjectsProvider.tsx: -------------------------------------------------------------------------------- 1 | import { Project } from "../types"; 2 | import { ProjectsContext } from "./ProjectsContext"; 3 | 4 | export interface ProjectsProviderProps { 5 | children: React.ReactNode; 6 | projects: Array; 7 | } 8 | 9 | export const ProjectsProvider = ({ 10 | children, 11 | projects, 12 | }: ProjectsProviderProps) => { 13 | return ( 14 | 15 | {children} 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/contexts/useProject.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ProjectContext } from "./ProjectContext"; 3 | 4 | export const useProject = () => useContext(ProjectContext); 5 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/contexts/useProjects.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ProjectsContext } from "./ProjectsContext"; 3 | 4 | export const useProjects = () => useContext(ProjectsContext); 5 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/addMemberToProject.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const addMemberToProject = ( 4 | projectId: string, 5 | memberEmail: string, 6 | accessToken: string 7 | ) => 8 | fetch(`${Constants.BackendUrl}/projects/${projectId}/members`, { 9 | method: "POST", 10 | body: JSON.stringify({ email: memberEmail }), 11 | headers: { 12 | Authorization: `Bearer ${accessToken}`, 13 | "Content-Type": "application/json", 14 | }, 15 | }).then((res) => { 16 | if (!res.ok) { 17 | return res.json().then((res) => { 18 | throw new Error(res.message); 19 | }); 20 | } 21 | 22 | return res.json(); 23 | }); 24 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/createProject.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const createProject = ( 4 | name: string, 5 | domain: string, 6 | accessToken: string 7 | ) => 8 | fetch(`${Constants.BackendUrl}/projects`, { 9 | method: "POST", 10 | body: JSON.stringify({ name, domain }), 11 | headers: { 12 | Authorization: `Bearer ${accessToken}`, 13 | "Content-Type": "application/json", 14 | }, 15 | }).then((res) => res.json()); 16 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/deleteFunnel.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const deleteFunnel = ( 4 | projectId: string, 5 | funnelId: string, 6 | authCookie: string 7 | ) => 8 | fetch(`${Constants.BackendUrl}/projects/${projectId}/funnels/${funnelId}`, { 9 | method: "DELETE", 10 | headers: { 11 | Authorization: `Bearer ${authCookie}`, 12 | "Content-Type": "application/json", 13 | }, 14 | }).then((res) => res.json()); 15 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/deleteProject.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const deleteProject = (projectId: string, accessToken: string) => 4 | fetch(`${Constants.BackendUrl}/projects/${projectId}`, { 5 | method: "DELETE", 6 | headers: { 7 | Authorization: `Bearer ${accessToken}`, 8 | "Content-Type": "application/json", 9 | }, 10 | }).then((res) => { 11 | if (!res.ok) { 12 | throw new Error("You are not authorized to remove this project."); 13 | } 14 | return res.json(); 15 | }); 16 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/editProject.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const editProject = ( 4 | projectId: string, 5 | name: string, 6 | domain: string, 7 | accessToken: string 8 | ) => 9 | fetch(`${Constants.BackendUrl}/projects/${projectId}`, { 10 | method: "PUT", 11 | body: JSON.stringify({ name, domain }), 12 | headers: { 13 | Authorization: `Bearer ${accessToken}`, 14 | "Content-Type": "application/json", 15 | }, 16 | }).then((res) => res.json()); 17 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/getEventHotSpots.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getEventHotSpots = async ( 4 | projectId: string, 5 | timeframe: number, 6 | accessToken: string 7 | ): Promise> => { 8 | const url = new URL( 9 | `${Constants.BackendUrl}/projects/${projectId}/events/hot-spots` 10 | ); 11 | 12 | url.searchParams.set("timeframe", String(timeframe)); 13 | 14 | return fetch(url, { 15 | headers: { 16 | Authorization: `Bearer ${accessToken}`, 17 | }, 18 | }).then((res) => res.json()); 19 | }; 20 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/getEventsGroupedByDate.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getEventsGroupedByDate = async ( 4 | projectId: string, 5 | timeframe: number, 6 | accessToken: string 7 | ): Promise> => { 8 | const url = new URL( 9 | `${Constants.BackendUrl}/projects/${projectId}/events/count` 10 | ); 11 | 12 | url.searchParams.set("timeframe", String(timeframe)); 13 | 14 | return fetch(url, { 15 | headers: { 16 | Authorization: `Bearer ${accessToken}`, 17 | }, 18 | }).then((res) => res.json()); 19 | }; 20 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/getFunnels.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getFunnels = async ( 4 | projectId: string, 5 | timeframe: number, 6 | accessToken: string 7 | ) => { 8 | const url = new URL(`${Constants.BackendUrl}/projects/${projectId}/funnels`); 9 | 10 | url.searchParams.set("timeframe", String(timeframe)); 11 | 12 | return fetch(url, { 13 | headers: { 14 | Authorization: `Bearer ${accessToken}`, 15 | }, 16 | }).then((res) => res.json()); 17 | }; 18 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/getFunnelsFields.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getFunnelsFields = async ( 4 | projectId: string, 5 | timeframe: number, 6 | accessToken: string 7 | ) => { 8 | const url = new URL( 9 | `${Constants.BackendUrl}/projects/${projectId}/funnels/fields` 10 | ); 11 | 12 | url.searchParams.set("timeframe", String(timeframe)); 13 | 14 | return fetch(url, { 15 | headers: { 16 | Authorization: `Bearer ${accessToken}`, 17 | }, 18 | }).then((res) => res.json()); 19 | }; 20 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/getGlobalMetric.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getGlobalMetric = async ( 4 | projectId: string, 5 | timeframe: number, 6 | accessToken: string 7 | ): Promise<{ 8 | pageViews: number; 9 | uniqueVisitors: number; 10 | bounceRate: number; 11 | }> => { 12 | const url = new URL( 13 | `${Constants.BackendUrl}/projects/${projectId}/metrics/global` 14 | ); 15 | 16 | url.searchParams.set("timeframe", String(timeframe)); 17 | 18 | return fetch(url, { 19 | headers: { 20 | Authorization: `Bearer ${accessToken}`, 21 | }, 22 | }).then((res) => res.json()); 23 | }; 24 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/getPageViewsGroupedByDate.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getPageViewsGroupedByDate = async ( 4 | projectId: string, 5 | timeframe: number, 6 | accessToken: string 7 | ): Promise> => { 8 | const url = new URL( 9 | `${Constants.BackendUrl}/projects/${projectId}/events/page-views` 10 | ); 11 | 12 | url.searchParams.set("timeframe", String(timeframe)); 13 | 14 | return fetch(url, { 15 | headers: { 16 | Authorization: `Bearer ${accessToken}`, 17 | }, 18 | }).then((res) => res.json()); 19 | }; 20 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/getProject.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getProject = ( 4 | projectId: string, 5 | accessToken: string, 6 | populate?: boolean 7 | ) => { 8 | const url = new URL(`${Constants.BackendUrl}/projects/${projectId}`); 9 | 10 | if (populate) { 11 | url.searchParams.set("populate", "true"); 12 | } 13 | 14 | return fetch(url.toString(), { 15 | headers: { Authorization: `Bearer ${accessToken}` }, 16 | }).then((res) => { 17 | if (res.ok) { 18 | return res.json(); 19 | } 20 | throw res; 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/getProjectFlags.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getProjectFlags = async (projectId: string, accessToken: string) => 4 | fetch(`${Constants.BackendUrl}/projects/${projectId}/flags`, { 5 | headers: { 6 | Authorization: `Bearer ${accessToken}`, 7 | }, 8 | }).then((res) => res.json()); 9 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/getProjectMetaTitle.ts: -------------------------------------------------------------------------------- 1 | import { Project } from "../types"; 2 | 3 | export const getProjectMetaTitle = (matches: any): string => { 4 | const project: Project | undefined = matches.find( 5 | (match: any) => match.id === "routes/dashboard.projects.$id" 6 | )?.data?.project; 7 | 8 | return project?.name || ""; 9 | }; 10 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/getProjects.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getProjects = (accessToken: string) => { 4 | return fetch(`${Constants.BackendUrl}/projects`, { 5 | headers: { 6 | Authorization: `Bearer ${accessToken}`, 7 | }, 8 | }).then((res) => { 9 | if (!res.ok) { 10 | throw new Error("Woops! Something went wrong in the server."); 11 | } 12 | return res.json(); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/removeMember.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const removeMember = ( 4 | projectId: string, 5 | memberId: string, 6 | accessToken: string 7 | ) => 8 | fetch(`${Constants.BackendUrl}/projects/${projectId}/members/${memberId}`, { 9 | method: "DELETE", 10 | headers: { 11 | Authorization: `Bearer ${accessToken}`, 12 | "Content-Type": "application/json", 13 | }, 14 | }).then((res) => res.json()); 15 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/services/rotateSecretKey.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const rotateSecretKey = (projectId: string, accessToken: string) => 4 | fetch(`${Constants.BackendUrl}/projects/${projectId}/rotate`, { 5 | method: "POST", 6 | headers: { 7 | Authorization: `Bearer ${accessToken}`, 8 | "Content-Type": "application/json", 9 | }, 10 | }).then((res) => { 11 | if (!res.ok) { 12 | throw new Error("You are not authorized to remove this project."); 13 | } 14 | return res.json(); 15 | }); 16 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/projects/validators/validateProjectName.ts: -------------------------------------------------------------------------------- 1 | import { CreateProjectDTO } from "../types"; 2 | 3 | export const validateProjectName = (values: CreateProjectDTO) => { 4 | const errors: Partial = {}; 5 | 6 | if (!values.name) { 7 | errors.name = "The name field is required, make sure to have one."; 8 | } 9 | 10 | if (!values.domain) { 11 | errors.domain = "The domain field is required, make sure to have one."; 12 | } 13 | 14 | return errors; 15 | }; 16 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/rules/services/createEmptyRule.ts: -------------------------------------------------------------------------------- 1 | import { ComparatorEnum, Rule } from "../types"; 2 | 3 | let id = 0; 4 | 5 | export const createEmptyRule = (): Rule => { 6 | id++; 7 | return { 8 | uuid: String(id), //this is a fake id only for usage with KEYS in React 9 | fieldName: "", 10 | fieldValue: "", 11 | fieldComparator: ComparatorEnum.Equals, 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/rules/types.ts: -------------------------------------------------------------------------------- 1 | import { Segment } from "../segments/types"; 2 | 3 | export interface RuleType { 4 | fieldName: string; 5 | fieldComparator: ComparatorEnum; 6 | fieldValue: string; 7 | } 8 | 9 | export enum ComparatorEnum { 10 | Equals = "eq", 11 | Contains = "contains", 12 | } 13 | 14 | export type Rule = RuleType & { 15 | uuid: string; 16 | segment?: Segment; 17 | segmentUuid: string; 18 | }; 19 | 20 | export type RuleUpdateDto = { 21 | fieldName?: string; 22 | fieldComparator?: ComparatorEnum; 23 | fieldValue?: string; 24 | segmentUuid?: string; 25 | }; 26 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/segments/hooks/useDeleteSegment.ts: -------------------------------------------------------------------------------- 1 | import { useNavigation } from "@remix-run/react"; 2 | import { Segment } from "../types"; 3 | 4 | export const useDeleteSegment = (segment: Segment) => { 5 | const navigation = useNavigation(); 6 | 7 | const type = navigation?.formData?.get("_type"); 8 | const deleteSegmentFormId = `delete-segment-${segment.uuid}`; 9 | 10 | const isDeletingSegment = 11 | type === "delete-segment" && 12 | navigation?.formData?.get("uuid")?.toString() === segment.uuid; 13 | 14 | return { isDeletingSegment, deleteSegmentFormId }; 15 | }; 16 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/segments/services/deleteSegment.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const deleteSegment = (segmentId: string, accessToken: string) => 4 | fetch(`${Constants.BackendUrl}/segments/${segmentId}`, { 5 | method: "DELETE", 6 | headers: { 7 | Authorization: `Bearer ${accessToken}`, 8 | "Content-Type": "application/json", 9 | }, 10 | }).then((res) => { 11 | if (!res.ok) { 12 | throw new Error("You are not authorized to remove this segment."); 13 | } 14 | return res.json(); 15 | }); 16 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/segments/services/getSegments.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getSegments = (projectId: string, accessToken: string) => { 4 | return fetch(`${Constants.BackendUrl}/projects/${projectId}/segments`, { 5 | headers: { 6 | Authorization: `Bearer ${accessToken}`, 7 | }, 8 | }).then((res) => { 9 | if (!res.ok) { 10 | throw new Error("Woops! Something went wrong in the server."); 11 | } 12 | return res.json(); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/segments/types.ts: -------------------------------------------------------------------------------- 1 | import { ComparatorEnum, RuleUpdateDto } from "../rules/types"; 2 | 3 | export interface SegmentRule { 4 | uuid: string; 5 | fieldComparator: ComparatorEnum; 6 | fieldName: string; 7 | fieldValue: string; 8 | createdAt: string; 9 | } 10 | 11 | export interface Segment { 12 | uuid: string; 13 | name: string; 14 | createdAt: string; 15 | projectUuid: string; 16 | userUuid: string; 17 | segmentRules: Array; 18 | } 19 | 20 | export interface SegmentUpsertDTO { 21 | uuid?: string; 22 | name: string; 23 | segmentRules: Array; 24 | } 25 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/strategy/components/StrategyList/useDeleteStrategy.ts: -------------------------------------------------------------------------------- 1 | import { useNavigation } from "@remix-run/react"; 2 | import { Strategy } from "../../types"; 3 | 4 | export const useDeleteStrategy = (strategy: Strategy) => { 5 | const navigation = useNavigation(); 6 | 7 | const type = navigation?.formData?.get("_type"); 8 | const deleteStrategyFormId = `delete-strategy-${strategy.uuid}`; 9 | 10 | const isDeletingStrategy = 11 | type === "delete-strategy" && 12 | navigation?.formData?.get("uuid")?.toString() === strategy.uuid; 13 | 14 | return { isDeletingStrategy, deleteStrategyFormId }; 15 | }; 16 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/strategy/services/createStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const createStrategy = (flagId: string, accessToken: string) => 4 | fetch(`${Constants.BackendUrl}/flags/${flagId}/strategies`, { 5 | method: "POST", 6 | headers: { 7 | Authorization: `Bearer ${accessToken}`, 8 | "Content-Type": "application/json", 9 | }, 10 | }).then((res) => { 11 | if (!res.ok) { 12 | throw new Error( 13 | "Woops! Something went wrong when trying to create the strategy." 14 | ); 15 | } 16 | 17 | return res.json(); 18 | }); 19 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/strategy/services/deleteStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const deleteStrategy = (strategyId: string, accessToken: string) => 4 | fetch(`${Constants.BackendUrl}/strategies/${strategyId}`, { 5 | method: "DELETE", 6 | headers: { 7 | Authorization: `Bearer ${accessToken}`, 8 | "Content-Type": "application/json", 9 | }, 10 | }).then((res) => { 11 | if (!res.ok) { 12 | throw new Error("You are not authorized to remove this strategy."); 13 | } 14 | return res.json(); 15 | }); 16 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/strategy/services/getStrategies.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getStrategies = (flagId: string, accessToken: string) => { 4 | return fetch(`${Constants.BackendUrl}/flags/${flagId}/strategies`, { 5 | headers: { 6 | Authorization: `Bearer ${accessToken}`, 7 | }, 8 | }).then((res) => { 9 | if (!res.ok) { 10 | throw new Error("Woops! Something went wrong in the server."); 11 | } 12 | return res.json(); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/support/components/Intercom.tsx: -------------------------------------------------------------------------------- 1 | import { IntercomProvider } from "react-use-intercom"; 2 | 3 | const INTERCOM_APP_ID = "lnfovtpj"; 4 | 5 | export const Intercom = () => { 6 | return ; 7 | }; 8 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/user/contexts/UserContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { User } from "../types"; 3 | 4 | export interface UserContextType { 5 | user: User; 6 | } 7 | 8 | export const UserContext = createContext({}); 9 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/user/contexts/UserProvider.tsx: -------------------------------------------------------------------------------- 1 | import { User } from "../types"; 2 | import { UserContext } from "./UserContext"; 3 | 4 | export interface UserProviderProps { 5 | children: React.ReactNode; 6 | user: User; 7 | } 8 | 9 | export const UserProvider = ({ children, user }: UserProviderProps) => { 10 | return ( 11 | {children} 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/user/contexts/useUser.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { UserContext } from "./UserContext"; 3 | 4 | export const useUser = () => useContext(UserContext); 5 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/user/services/changeFullname.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const changeFullname = (fullname: string, accessToken: string) => 4 | fetch(`${Constants.BackendUrl}/users/me`, { 5 | method: "PUT", 6 | body: JSON.stringify({ fullname }), 7 | headers: { 8 | Authorization: `Bearer ${accessToken}`, 9 | "Content-Type": "application/json", 10 | }, 11 | }).then((res) => res.json()); 12 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/user/services/createUser.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const createUser = ( 4 | fullname: string, 5 | email: string, 6 | password: string 7 | ) => { 8 | return fetch(`${Constants.BackendUrl}/auth/register`, { 9 | method: "POST", 10 | body: JSON.stringify({ fullname, email, password }), 11 | headers: { "Content-Type": "application/json" }, 12 | }).then((res) => { 13 | if (!res.ok) { 14 | if (res.status === 400) { 15 | throw new Error("This email is already used."); 16 | } 17 | 18 | throw new Error("Woops! Something went wrong in the server."); 19 | } 20 | 21 | return res.json(); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/user/services/forgotPassword.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const forgotPassword = (email: string) => { 4 | return fetch(`${Constants.BackendUrl}/users/forgot-password`, { 5 | method: "POST", 6 | body: JSON.stringify({ email }), 7 | headers: { "Content-Type": "application/json" }, 8 | }).then((res) => { 9 | if (!res.ok) { 10 | if (res.status === 400) { 11 | throw new Error("The email is required"); 12 | } 13 | 14 | throw new Error("Woops! Something went wrong in the server."); 15 | } 16 | 17 | return res.json(); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/user/services/resetPassword.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const resetPassword = (password: string, token: string) => { 4 | return fetch(`${Constants.BackendUrl}/users/reset-password`, { 5 | method: "POST", 6 | body: JSON.stringify({ password, token }), 7 | headers: { "Content-Type": "application/json" }, 8 | }).then((res) => { 9 | if (!res.ok) { 10 | if (res.status === 400) { 11 | throw new Error("An information is missing"); 12 | } 13 | 14 | throw new Error("Woops! Something went wrong in the server."); 15 | } 16 | 17 | return res.json(); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/user/types.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | fullname: string; 3 | email: string; 4 | uuid: string; 5 | trialEnd?: string; 6 | } 7 | 8 | export interface RegisterCredentials { 9 | fullname: string; 10 | email: string; 11 | password: string; 12 | confirmPassword: string; 13 | } 14 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/user/validators/validate-user-fullname.ts: -------------------------------------------------------------------------------- 1 | export const validateUserFullname = (fullname: string) => { 2 | const errors: Partial<{ fullname: string }> = {}; 3 | 4 | if (!fullname) { 5 | errors.fullname = "The fullname field is required, make sure to have one."; 6 | } 7 | 8 | return errors; 9 | }; 10 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/variants/components/VariantDot.tsx: -------------------------------------------------------------------------------- 1 | import { stringToColor } from "~/modules/misc/utils/stringToColor"; 2 | 3 | export interface VariantDotProps { 4 | variant: string; 5 | size?: "M" | "L"; 6 | } 7 | 8 | const sizeClasses = { 9 | M: "h-4 w-4", 10 | L: "h-6 w-6", 11 | }; 12 | 13 | export const VariantDot = ({ variant, size = "M" }: VariantDotProps) => { 14 | const color = stringToColor(variant, 75); 15 | const sizeClass = sizeClasses[size]; 16 | 17 | return ( 18 |
    19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/variants/services/deleteVariant.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const deleteVariant = ( 4 | flagId: string, 5 | variantId: string, 6 | accessToken: string 7 | ) => 8 | fetch(`${Constants.BackendUrl}/flags/${flagId}/variants/${variantId}`, { 9 | method: "DELETE", 10 | headers: { 11 | Authorization: `Bearer ${accessToken}`, 12 | "Content-Type": "application/json", 13 | }, 14 | }).then((res) => { 15 | if (!res.ok) { 16 | throw new Error("You are not authorized to remove this variant."); 17 | } 18 | return res.json(); 19 | }); 20 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/variants/services/getVariants.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getVariants = (flagId: string, accessToken: string) => { 4 | return fetch(`${Constants.BackendUrl}/flags/${flagId}/variants`, { 5 | headers: { 6 | Authorization: `Bearer ${accessToken}`, 7 | }, 8 | }).then((res) => { 9 | if (!res.ok) { 10 | throw new Error("Woops! Something went wrong in the server."); 11 | } 12 | return res.json(); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/variants/types.ts: -------------------------------------------------------------------------------- 1 | export interface Variant { 2 | uuid: string; 3 | isControl: boolean; 4 | value: string; 5 | rolloutPercentage: number; 6 | } 7 | 8 | export type VariantCreateDTO = Omit; 9 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/webhooks/components/WebhookEvent.tsx: -------------------------------------------------------------------------------- 1 | import { Tag } from "~/components/Tag"; 2 | import { WebhookEvents } from "../types"; 3 | 4 | export interface WebhookEventProps { 5 | value: WebhookEvents; 6 | } 7 | 8 | export const WebhookEvent = ({ value }: WebhookEventProps) => { 9 | if (value === WebhookEvents.ACTIVATION) { 10 | return Flag activation; 11 | } 12 | 13 | if (value === WebhookEvents.DEACTIVATION) { 14 | return Flag deactivation; 15 | } 16 | 17 | return null; 18 | }; 19 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/webhooks/services/deleteWebhook.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const deleteWebhook = (webhookId: string, accessToken: string) => 4 | fetch(`${Constants.BackendUrl}/webhooks/${webhookId}`, { 5 | method: "DELETE", 6 | headers: { 7 | Authorization: `Bearer ${accessToken}`, 8 | "Content-Type": "application/json", 9 | }, 10 | }).then((res) => { 11 | if (!res.ok) { 12 | throw new Error("You are not authorized to remove this webhook."); 13 | } 14 | return res.json(); 15 | }); 16 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/webhooks/services/getWebhooks.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "~/constants"; 2 | 3 | export const getWebhooks = (flagId: string, accessToken: string) => { 4 | return fetch(`${Constants.BackendUrl}/flags/${flagId}/webhooks`, { 5 | headers: { 6 | Authorization: `Bearer ${accessToken}`, 7 | }, 8 | }).then((res) => { 9 | if (!res.ok) { 10 | throw new Error("Woops! Something went wrong in the server."); 11 | } 12 | return res.json(); 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /websites/frontend/app/modules/webhooks/types.ts: -------------------------------------------------------------------------------- 1 | export enum WebhookEvents { 2 | ACTIVATION = "ACTIVATION", 3 | DEACTIVATION = "DEACTIVATION", 4 | } 5 | 6 | export interface WebhookCreationDTO { 7 | endpoint: string; 8 | event: WebhookEvents; 9 | } 10 | 11 | export interface Webhook { 12 | endpoint: string; 13 | event: WebhookEvents; 14 | secret: string; 15 | uuid: string; 16 | } 17 | -------------------------------------------------------------------------------- /websites/frontend/app/routes/_index.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderFunction, redirect } from "@remix-run/node"; 2 | import { getSession } from "~/sessions"; 3 | 4 | export const loader: LoaderFunction = async ({ request }) => { 5 | const session = await getSession(request.headers.get("Cookie")); 6 | const authCookie = session.get("auth-cookie"); 7 | 8 | if (!authCookie) { 9 | throw redirect("/signin"); 10 | } 11 | 12 | return redirect("/dashboard/projects/all"); 13 | }; 14 | 15 | export default function Homepage() { 16 | return null; 17 | } 18 | -------------------------------------------------------------------------------- /websites/frontend/app/routes/dashboard.profile.tsx: -------------------------------------------------------------------------------- 1 | import { MetaFunction } from "@remix-run/node"; 2 | import { Outlet } from "@remix-run/react"; 3 | 4 | export const meta: MetaFunction = () => { 5 | return [ 6 | { 7 | title: "Progressively | Profile", 8 | }, 9 | ]; 10 | }; 11 | 12 | export default function ProfilePage() { 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /websites/frontend/app/routes/signout.tsx: -------------------------------------------------------------------------------- 1 | import { LoaderFunction, redirect } from "@remix-run/node"; 2 | import { destroySession, getSession } from "~/sessions"; 3 | 4 | export const loader: LoaderFunction = async ({ request }) => { 5 | const session = await getSession(request.headers.get("Cookie")); 6 | 7 | return redirect("/signin", { 8 | headers: { 9 | "Set-Cookie": await destroySession(session), 10 | }, 11 | }); 12 | }; 13 | 14 | export default function SignoutPage() { 15 | return null; 16 | } 17 | -------------------------------------------------------------------------------- /websites/frontend/app/sessions.ts: -------------------------------------------------------------------------------- 1 | import { createCookieSessionStorage } from "@remix-run/node"; 2 | 3 | const sessionSecret = process.env.SESSION_SECRET || "abcd"; 4 | 5 | const { getSession, commitSession, destroySession } = 6 | createCookieSessionStorage({ 7 | // a Cookie from `createCookie` or the CookieOptions to create one 8 | cookie: { 9 | name: "__session", 10 | httpOnly: true, 11 | secure: process.env.NODE_ENV !== "development", 12 | maxAge: 86_400, 13 | path: "/", 14 | sameSite: "lax", 15 | secrets: [sessionSecret], 16 | }, 17 | }); 18 | 19 | export { getSession, commitSession, destroySession }; 20 | -------------------------------------------------------------------------------- /websites/frontend/app/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /websites/frontend/cypress/e2e/index.spec.ts: -------------------------------------------------------------------------------- 1 | describe("/", () => { 2 | describe("with db filled", () => { 3 | before(cy.seed); 4 | after(cy.cleanupDb); 5 | 6 | it("shows the signin page", () => { 7 | cy.visit("/"); 8 | cy.url().should("contain", "/signin"); 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /websites/frontend/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /websites/frontend/cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress", "node", "@testing-library/cypress"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /websites/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /websites/frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/frontend/public/favicon.png -------------------------------------------------------------------------------- /websites/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { vitePlugin as remix } from "@remix-run/dev"; 2 | import { defineConfig } from "vite"; 3 | import tsconfigPaths from "vite-tsconfig-paths"; 4 | 5 | export default defineConfig({ 6 | ssr: { 7 | noExternal: [/^d3.*$/, /^@nivo.*$/], 8 | }, 9 | server: { 10 | port: 3000, 11 | }, 12 | plugins: [ 13 | remix({ 14 | future: { 15 | v3_fetcherPersist: true, 16 | v3_relativeSplatPath: true, 17 | v3_throwAbortReason: true, 18 | }, 19 | }), 20 | tsconfigPaths(), 21 | ], 22 | }); 23 | -------------------------------------------------------------------------------- /websites/marketing/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | 23 | .vercel/ 24 | .netlify/ -------------------------------------------------------------------------------- /websites/marketing/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /websites/marketing/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /websites/marketing/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import tailwind from "@astrojs/tailwind"; 3 | import react from "@astrojs/react"; 4 | 5 | import netlify from "@astrojs/netlify"; 6 | 7 | // https://astro.build/config 8 | export default defineConfig({ 9 | integrations: [tailwind(), react()], 10 | output: "server", 11 | adapter: netlify(), 12 | }); 13 | -------------------------------------------------------------------------------- /websites/marketing/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/public/logo.png -------------------------------------------------------------------------------- /websites/marketing/public/meta-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/public/meta-img.png -------------------------------------------------------------------------------- /websites/marketing/src/components/Badge.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | const badgeClass = { 4 | indigo: "bg-indigo-100 text-indigo-500 text-lg", 5 | emerald: "bg-emerald-100 text-emerald-500 text-lg", 6 | pink: "bg-pink-100 text-pink-500 text-lg", 7 | }; 8 | 9 | export interface BadgeProps { 10 | scheme: "indigo" | "emerald" | "pink"; 11 | children: ReactNode; 12 | } 13 | 14 | export const Badge = ({ children, scheme }: BadgeProps) => { 15 | const className = badgeClass[scheme]; 16 | 17 | return ( 18 |
    19 | {children} 20 |
    21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /websites/marketing/src/components/Banner.astro: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /websites/marketing/src/components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | const variantStyles = { 4 | success: "bg-gradient-to-b from-emerald-100 to-emerald-50", 5 | default: "bg-white", 6 | dark: "bg-gray-800", 7 | }; 8 | 9 | export interface CardProps { 10 | children: ReactNode; 11 | variant?: "success" | "default" | "dark"; 12 | } 13 | 14 | export const Card = ({ variant, children }: CardProps) => { 15 | const sharedStyles = 16 | "shrink-0 rounded-3xl border border-gray-100 w-[320px] md:w-[460px]"; 17 | const styles = `${sharedStyles} ${variantStyles[variant || "default"]}`; 18 | 19 | return
    {children}
    ; 20 | }; 21 | -------------------------------------------------------------------------------- /websites/marketing/src/components/Card/CardAsset.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | colorScheme: "indigo" | "pink" | "emerald" | "sky"; 4 | } 5 | 6 | const { colorScheme } = Astro.props; 7 | 8 | const classes = { 9 | indigo: "bg-gradient-to-br from-indigo-100 to-indigo-200", 10 | pink: "bg-gradient-to-br from-pink-100 to-pink-200", 11 | emerald: "bg-gradient-to-br from-emerald-100 to-emerald-200", 12 | sky: "bg-gradient-to-br from-sky-100 to-sky-200", 13 | }; 14 | 15 | const className = classes[colorScheme]; 16 | --- 17 | 18 |
    19 |
    20 | 21 |
    22 |
    23 | -------------------------------------------------------------------------------- /websites/marketing/src/components/Card/CardContent.astro: -------------------------------------------------------------------------------- 1 |
    2 | -------------------------------------------------------------------------------- /websites/marketing/src/components/Card/CardTitle.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Title from "../Title.astro"; 3 | 4 | export interface Props { 5 | theme?: "light" | "dark"; 6 | } 7 | const { theme } = Astro.props; 8 | --- 9 | 10 |
    11 | <slot /> 12 |
    13 | -------------------------------------------------------------------------------- /websites/marketing/src/components/Code.tsx: -------------------------------------------------------------------------------- 1 | export interface CodeProps { 2 | html: string; 3 | } 4 | export const Code = ({ html }: CodeProps) => { 5 | return ( 6 |
    10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /websites/marketing/src/components/Icons/Check.astro: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /websites/marketing/src/components/Icons/Twitter.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | className?: string; 4 | } 5 | 6 | const { className } = Astro.props; 7 | --- 8 | 9 | 22 | -------------------------------------------------------------------------------- /websites/marketing/src/components/Img.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Image } from "astro:assets"; 3 | 4 | const { src, alt, ...rest } = Astro.props; 5 | --- 6 | 7 | {alt} 8 | -------------------------------------------------------------------------------- /websites/marketing/src/components/Radio.tsx: -------------------------------------------------------------------------------- 1 | export const Radio = (props: any) => { 2 | const classes = 3 | "custom-radio appearance-none m-0 w-4 h-4 border border-gray-200 hover:border-gray-400 cursor-pointer rounded-full flex items-center justify-center before:content-[''] before:h-2 before:w-2 before:rounded-full checked:before:h-2 checked:before:w-2 checked:before:bg-indigo-300"; 4 | 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /websites/marketing/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /websites/marketing/src/hooks/useScroll.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | 3 | export const useScroll = (tickFn: (scrollFraction: number) => void) => { 4 | const tickFnRef = useRef(tickFn); 5 | 6 | tickFnRef.current = tickFn; 7 | 8 | useEffect(() => { 9 | const setupScroll = () => { 10 | const scrollPosition = window.scrollY; 11 | tickFnRef.current(scrollPosition); 12 | }; 13 | 14 | const handleScroll = () => requestAnimationFrame(setupScroll); 15 | 16 | document.addEventListener("scroll", handleScroll); 17 | 18 | return () => { 19 | document.removeEventListener("scroll", handleScroll); 20 | }; 21 | }, []); 22 | }; 23 | -------------------------------------------------------------------------------- /websites/marketing/src/sections/assets/abovefold/ff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/src/sections/assets/abovefold/ff.png -------------------------------------------------------------------------------- /websites/marketing/src/sections/assets/catchy/catchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/src/sections/assets/catchy/catchy.png -------------------------------------------------------------------------------- /websites/marketing/src/sections/assets/catchy/funnels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/src/sections/assets/catchy/funnels.png -------------------------------------------------------------------------------- /websites/marketing/src/sections/assets/howitworks/audience.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/src/sections/assets/howitworks/audience.png -------------------------------------------------------------------------------- /websites/marketing/src/sections/assets/howitworks/mfrachet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/src/sections/assets/howitworks/mfrachet.png -------------------------------------------------------------------------------- /websites/marketing/src/sections/assets/howitworks/react.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/src/sections/assets/howitworks/react.png -------------------------------------------------------------------------------- /websites/marketing/src/sections/assets/howitworks/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/src/sections/assets/howitworks/welcome.png -------------------------------------------------------------------------------- /websites/marketing/src/sections/assets/value/analytics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/src/sections/assets/value/analytics.png -------------------------------------------------------------------------------- /websites/marketing/src/sections/assets/value/funnels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/src/sections/assets/value/funnels.png -------------------------------------------------------------------------------- /websites/marketing/src/sections/assets/value/hot-zones.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/src/sections/assets/value/hot-zones.png -------------------------------------------------------------------------------- /websites/marketing/src/sections/assets/value/page-views.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/marketing/src/sections/assets/value/page-views.png -------------------------------------------------------------------------------- /websites/marketing/src/support/components/Intercom.tsx: -------------------------------------------------------------------------------- 1 | import { IntercomProvider } from "react-use-intercom"; 2 | 3 | const INTERCOM_APP_ID = "lnfovtpj"; 4 | 5 | export const Intercom = () => { 6 | // @ts-ignore 7 | return ; 8 | }; 9 | -------------------------------------------------------------------------------- /websites/marketing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "react" 6 | } 7 | } --------------------------------------------------------------------------------