├── .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 |

4 |
5 |
6 |
7 |
8 |
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 |
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 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/websites/backend/src/mail/components/Text.tsx:
--------------------------------------------------------------------------------
1 | import { Text as RawText, TextProps } from '@react-email/components';
2 |
3 | export const Text = (props: TextProps) => {
4 | return (
5 |
6 | );
7 | };
8 |
--------------------------------------------------------------------------------
/websites/backend/src/mail/mail.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MailService } from './mail.service';
3 |
4 | @Module({
5 | providers: [MailService],
6 | exports: [MailService],
7 | })
8 | export class MailModule {}
9 |
--------------------------------------------------------------------------------
/websites/backend/src/mail/mail.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { MailService } from './mail.service';
3 |
4 | describe('MailService', () => {
5 | let service: MailService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [MailService],
10 | }).compile();
11 |
12 | service = module.get(MailService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/websites/backend/src/mail/smtp-constants.ts:
--------------------------------------------------------------------------------
1 | export const Smtp = () => ({
2 | host: process.env.SMTP_HOST,
3 | port: process.env.SMTP_PORT,
4 | user: process.env.SMTP_USER,
5 | password: process.env.SMTP_PASSWORD,
6 | });
7 |
--------------------------------------------------------------------------------
/websites/backend/src/payment/constants.ts:
--------------------------------------------------------------------------------
1 | export const InitialCreditCount = 1;
2 | export const EventsPerCredits = 500;
3 |
--------------------------------------------------------------------------------
/websites/backend/src/payment/getEnv.ts:
--------------------------------------------------------------------------------
1 | export const getEnv = () => ({
2 | PrivateStripeKey: process.env.STRIPE_PRIVATE_KEY!,
3 | FrontendUrl: process.env.FRONTEND_URL!,
4 | ProductId: process.env.STRIPE_PRODUCT_ID!,
5 | WebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
6 | });
7 |
--------------------------------------------------------------------------------
/websites/backend/src/payment/types.ts:
--------------------------------------------------------------------------------
1 | export interface QueuedPayingHit {
2 | reduceBy: number;
3 | projectUuid: string;
4 | }
5 |
--------------------------------------------------------------------------------
/websites/backend/src/projects/errors.ts:
--------------------------------------------------------------------------------
1 | export class FlagAlreadyExists extends Error {
2 | constructor() {
3 | super('Flag already exists');
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/websites/backend/src/projects/types.ts:
--------------------------------------------------------------------------------
1 | export interface UserProject {
2 | userId: string;
3 | projectId: string;
4 | role: string;
5 | }
6 |
--------------------------------------------------------------------------------
/websites/backend/src/pubsub/concrete/InMemory.service.ts:
--------------------------------------------------------------------------------
1 | import { IPubsubService } from '../types';
2 |
3 | let channels: Record void>> = {};
4 |
5 | export class InMemoryService implements IPubsubService {
6 | constructor() {}
7 |
8 | async teardown() {
9 | channels = undefined;
10 | }
11 |
12 | async notifyChannel(channel: string, message: any) {
13 | if (channels[channel]) {
14 | channels[channel].forEach((cb) => cb(message));
15 | }
16 | }
17 | subscribe(channel: string, callback: (parsedMsg: T) => void) {
18 | if (!channels[channel]) {
19 | channels[channel] = [];
20 | }
21 |
22 | channels[channel].push(callback);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/websites/backend/src/pubsub/getEnv.ts:
--------------------------------------------------------------------------------
1 | export const getEnv = () => ({
2 | RedisUrl: process.env.REDIS_URL,
3 | });
4 |
--------------------------------------------------------------------------------
/websites/backend/src/pubsub/pubsub.module.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Module, OnModuleDestroy } from '@nestjs/common';
2 | import { MakePubsubService } from './pubsub.service.factory';
3 | import { IPubsubService } from './types';
4 |
5 | @Module({
6 | providers: [
7 | {
8 | provide: 'PubsubService',
9 | useFactory: MakePubsubService,
10 | },
11 | ],
12 | exports: ['PubsubService'],
13 | })
14 | export class PubsubModule implements OnModuleDestroy {
15 | constructor(
16 | @Inject('PubsubService') private readonly pubsubService: IPubsubService,
17 | ) {}
18 |
19 | async onModuleDestroy() {
20 | await this.pubsubService.teardown();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/websites/backend/src/pubsub/pubsub.service.factory.ts:
--------------------------------------------------------------------------------
1 | import { getEnv } from './getEnv';
2 |
3 | import { RedisService } from './concrete/redis.service';
4 | import { InMemoryService } from './concrete/InMemory.service';
5 | import { IPubsubService } from './types';
6 |
7 | export const MakePubsubService = (): IPubsubService => {
8 | const env = getEnv();
9 |
10 | if (env.RedisUrl) {
11 | return new RedisService();
12 | }
13 |
14 | return new InMemoryService();
15 | };
16 |
--------------------------------------------------------------------------------
/websites/backend/src/pubsub/types.ts:
--------------------------------------------------------------------------------
1 | export interface IPubsubService {
2 | notifyChannel: (channel: string, message: any) => Promise;
3 | subscribe: (channel: string, callback: (parsedMsg: T) => void) => void;
4 | teardown: () => Promise;
5 | }
6 |
--------------------------------------------------------------------------------
/websites/backend/src/queuing/concrete/InMemory.service.ts:
--------------------------------------------------------------------------------
1 | import { IQueuingService } from '../types';
2 |
3 | let topics: Record void>> = {};
4 |
5 | export class InMemoryService implements IQueuingService {
6 | async send(topic: string, message: any) {
7 | if (topics[topic]) {
8 | topics[topic].forEach((cb) => cb(message));
9 | }
10 | }
11 |
12 | async consume(
13 | topic: string,
14 | groupId: string,
15 | callback: (parsedMsg: T) => void,
16 | ) {
17 | if (!topics[topic]) {
18 | topics[topic] = [];
19 | }
20 |
21 | topics[topic].push(callback);
22 | }
23 |
24 | async teardown() {
25 | topics = undefined;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/websites/backend/src/queuing/concrete/MockService.ts:
--------------------------------------------------------------------------------
1 | import { IQueuingService } from '../types';
2 |
3 | export class MockService implements IQueuingService {
4 | async send() {}
5 |
6 | async consume() {}
7 |
8 | async teardown() {}
9 | }
10 |
--------------------------------------------------------------------------------
/websites/backend/src/queuing/getEnv.ts:
--------------------------------------------------------------------------------
1 | export const getEnv = () => ({
2 | KafkaBroker: process.env.KAFKA_BROKER,
3 | KafkaUser: process.env.KAFKA_USER,
4 | KafkaPassword: process.env.KAFKA_PASSWORD,
5 | });
6 |
--------------------------------------------------------------------------------
/websites/backend/src/queuing/queuing.module.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Module, OnModuleDestroy } from '@nestjs/common';
2 | import { MakeQueuingService } from './queuing.service.factory';
3 | import { IQueuingService } from './types';
4 |
5 | @Module({
6 | imports: [],
7 | providers: [
8 | {
9 | provide: 'QueueingService',
10 | useFactory: async () => await MakeQueuingService(),
11 | },
12 | ],
13 | exports: ['QueueingService'],
14 | })
15 | export class QueuingModule implements OnModuleDestroy {
16 | constructor(
17 | @Inject('QueueingService') private readonly queuingService: IQueuingService,
18 | ) {}
19 |
20 | async onModuleDestroy() {
21 | await this.queuingService.teardown();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/websites/backend/src/queuing/queuing.service.factory.ts:
--------------------------------------------------------------------------------
1 | import { getEnv } from './getEnv';
2 | import { InMemoryService } from './concrete/InMemory.service';
3 | import { IQueuingService } from './types';
4 | import { KafkaService } from './concrete/Kafka.service';
5 |
6 | export const MakeQueuingService = async (): Promise => {
7 | const env = getEnv();
8 |
9 | if (env.KafkaBroker && env.KafkaPassword && env.KafkaUser) {
10 | return KafkaService.create();
11 | }
12 |
13 | return new InMemoryService();
14 | };
15 |
--------------------------------------------------------------------------------
/websites/backend/src/queuing/topics.ts:
--------------------------------------------------------------------------------
1 | export const KafkaTopics = {
2 | AnalyticsHits: 'analytics_hits',
3 | FlagHits: 'flag_hits',
4 | PayingHits: 'paying_hits',
5 | };
6 |
--------------------------------------------------------------------------------
/websites/backend/src/queuing/types.ts:
--------------------------------------------------------------------------------
1 | export interface IQueuingService {
2 | send: (topic: string, message: any) => Promise;
3 | consume: (
4 | topic: string,
5 | groupId: string,
6 | callback: (parsedMsg: T) => void,
7 | ) => Promise;
8 | teardown: () => Promise;
9 | }
10 |
--------------------------------------------------------------------------------
/websites/backend/src/rule/comparators/comparatorFactory.ts:
--------------------------------------------------------------------------------
1 | import { ComparatorEnum } from './types';
2 | import { Comparator } from './comparators-types';
3 | import { contains } from './contains';
4 | import { equals } from './equals';
5 |
6 | export const ComparatorFactory = {
7 | create: (comparatorKey: ComparatorEnum): Comparator => {
8 | switch (comparatorKey) {
9 | default:
10 | case ComparatorEnum.Equals: {
11 | return equals;
12 | }
13 |
14 | case ComparatorEnum.Contains: {
15 | return contains;
16 | }
17 | }
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/websites/backend/src/rule/comparators/comparators-types.ts:
--------------------------------------------------------------------------------
1 | export type Comparator = (v1: unknown, v2: unknown) => boolean;
2 |
--------------------------------------------------------------------------------
/websites/backend/src/rule/comparators/contains.ts:
--------------------------------------------------------------------------------
1 | import { Comparator } from './comparators-types';
2 |
3 | export const contains: Comparator = (v1: string, v2: string) => {
4 | return v2.includes(v1);
5 | };
6 |
--------------------------------------------------------------------------------
/websites/backend/src/rule/comparators/equals.ts:
--------------------------------------------------------------------------------
1 | import { Comparator } from './comparators-types';
2 |
3 | export const equals: Comparator = (v1: string, v2: string) => {
4 | return v1 === v2;
5 | };
6 |
--------------------------------------------------------------------------------
/websites/backend/src/rule/comparators/types.ts:
--------------------------------------------------------------------------------
1 | export enum ComparatorEnum {
2 | Equals = 'eq',
3 | Contains = 'contains',
4 | }
5 |
--------------------------------------------------------------------------------
/websites/backend/src/rule/rule.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { RuleService } from './rule.service';
3 | import { ActivityLogModule } from '../activity-log/activity-log.module';
4 |
5 | @Module({
6 | imports: [ActivityLogModule],
7 | providers: [RuleService],
8 | exports: [RuleService],
9 | controllers: [],
10 | })
11 | export class RuleModule {}
12 |
--------------------------------------------------------------------------------
/websites/backend/src/rule/types.ts:
--------------------------------------------------------------------------------
1 | import { Segment } from '../segment/types';
2 | import { ComparatorEnum } from './comparators/types';
3 |
4 | export interface RuleType {
5 | fieldName: string;
6 | fieldComparator: ComparatorEnum;
7 | fieldValue: string;
8 | segmentUuid?: string;
9 | segment?: Segment;
10 | }
11 |
12 | export type RuleUpdateDto = {
13 | fieldName?: string;
14 | fieldComparator?: ComparatorEnum;
15 | fieldValue?: string;
16 | segmentUuid?: string;
17 | };
18 |
19 | export type FieldRecord = Record;
20 |
--------------------------------------------------------------------------------
/websites/backend/src/sdk/sdk.dto.ts:
--------------------------------------------------------------------------------
1 | import * as Joi from 'joi';
2 |
3 | export const SdkHitAnalyticsSchema = Joi.array().items({
4 | name: Joi.string().required(),
5 | url: Joi.string(),
6 | data: Joi.any(),
7 | referer: Joi.string(),
8 | viewportWidth: Joi.number(),
9 | viewportHeight: Joi.number(),
10 | posX: Joi.number(),
11 | posY: Joi.number(),
12 | selector: Joi.string(),
13 | clientKey: Joi.string(),
14 | secretKey: Joi.string(),
15 | domain: Joi.string(),
16 | visitorId: Joi.string(),
17 | });
18 |
--------------------------------------------------------------------------------
/websites/backend/src/sdk/types.ts:
--------------------------------------------------------------------------------
1 | import { DeviceInfo } from '../shared/utils/getDeviceInfo';
2 |
3 | export interface EventHit {
4 | name: string;
5 | data?: any;
6 | url?: string;
7 | referer?: string;
8 | viewportWidth?: number;
9 | viewportHeight?: number;
10 | posX?: number;
11 | posY?: number;
12 | selector?: string;
13 | }
14 |
15 | export interface QueuedEventHit extends EventHit, DeviceInfo {
16 | clientKey?: string; // queued event is processed "later" and so, one of the keys is required
17 | secretKey?: string;
18 | domain?: string;
19 | visitorId: string;
20 | sessionUuid: string;
21 | projectUuid: string;
22 | }
23 |
--------------------------------------------------------------------------------
/websites/backend/src/segment/segment.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { SegmentService } from './segment.service';
3 | import { SegmentController } from './segment.controller';
4 | import { DatabaseModule } from '../database/database.module';
5 | import { ProjectsModule } from '../projects/projects.module';
6 |
7 | @Module({
8 | providers: [SegmentService],
9 | controllers: [SegmentController],
10 | imports: [DatabaseModule, ProjectsModule],
11 | })
12 | export class SegmentModule {}
13 |
--------------------------------------------------------------------------------
/websites/backend/src/shared/decorators/Roles.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 |
3 | export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
4 |
--------------------------------------------------------------------------------
/websites/backend/src/shared/utils/getDeviceInfo.ts:
--------------------------------------------------------------------------------
1 | import { Request } from 'express';
2 | import * as uap from 'ua-parser-js';
3 |
4 | const defaultValue = 'Unknown';
5 |
6 | export interface DeviceInfo {
7 | os: string;
8 | browser: string;
9 | osVersion: string;
10 | browserVersion: string;
11 | }
12 | export const getDeviceInfo = (request: Request): DeviceInfo => {
13 | const ua = uap(request.headers['user-agent']);
14 | const browser = ua?.browser?.name || defaultValue;
15 | const browserVersion = ua?.browser?.version || defaultValue;
16 | const os = ua?.os?.name || defaultValue;
17 | const osVersion = ua?.os?.version || defaultValue;
18 |
19 | return { os, browser, osVersion, browserVersion };
20 | };
21 |
--------------------------------------------------------------------------------
/websites/backend/src/shared/utils/sleep.ts:
--------------------------------------------------------------------------------
1 | const sleepDelay = Number(process.env.SLEEP_DELAY || 2000);
2 |
3 | export const sleep = () =>
4 | new Promise((resolve) => setTimeout(resolve, sleepDelay));
5 |
--------------------------------------------------------------------------------
/websites/backend/src/strategy/strategy.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { StrategyController } from './strategy.controller';
3 | import { StrategyService } from './strategy.service';
4 | import { DatabaseModule } from '../database/database.module';
5 | import { ActivityLogModule } from '../activity-log/activity-log.module';
6 | import { FlagsModule } from '../flags/flags.module';
7 |
8 | @Module({
9 | imports: [DatabaseModule, ActivityLogModule, FlagsModule],
10 | controllers: [StrategyController],
11 | providers: [StrategyService],
12 | exports: [StrategyService],
13 | })
14 | export class StrategyModule {}
15 |
--------------------------------------------------------------------------------
/websites/backend/src/tokens/types.ts:
--------------------------------------------------------------------------------
1 | export interface RefreshTokenPayload {
2 | jti: number;
3 | sub: number;
4 | }
5 |
--------------------------------------------------------------------------------
/websites/backend/src/users/roles.ts:
--------------------------------------------------------------------------------
1 | export enum UserRoles {
2 | Admin = 'admin',
3 | User = 'user',
4 | }
5 |
--------------------------------------------------------------------------------
/websites/backend/src/users/status.ts:
--------------------------------------------------------------------------------
1 | export enum UserStatus {
2 | Active = 'Active',
3 | Pending = 'Pending',
4 | }
5 |
--------------------------------------------------------------------------------
/websites/backend/src/users/types.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | uuid: string;
3 | fullname: string;
4 | email: string;
5 | password: string;
6 | activationToken?: string;
7 | status: string;
8 | trialEnd?: Date;
9 | }
10 |
--------------------------------------------------------------------------------
/websites/backend/src/users/users.decorator.ts:
--------------------------------------------------------------------------------
1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2 |
3 | export const UserId = createParamDecorator(
4 | (data: unknown, ctx: ExecutionContext) => {
5 | const request = ctx.switchToHttp().getRequest();
6 |
7 | return request.user?.uuid;
8 | },
9 | );
10 |
--------------------------------------------------------------------------------
/websites/backend/src/users/users.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { UsersService } from './users.service';
3 | import { UsersController } from './users.controller';
4 | import { TokensModule } from '../tokens/tokens.module';
5 | import { MailModule } from '../mail/mail.module';
6 | import { DatabaseModule } from '../database/database.module';
7 |
8 | @Module({
9 | imports: [TokensModule, MailModule, DatabaseModule],
10 | providers: [UsersService],
11 | controllers: [UsersController],
12 | exports: [UsersService],
13 | })
14 | export class UsersModule {}
15 |
--------------------------------------------------------------------------------
/websites/backend/src/webhooks/types.ts:
--------------------------------------------------------------------------------
1 | import * as Joi from 'joi';
2 |
3 | export enum WebhookEvents {
4 | ACTIVATION = 'ACTIVATION',
5 | DEACTIVATION = 'DEACTIVATION',
6 | }
7 |
8 | export interface WebhookCreationDTO {
9 | endpoint: string;
10 | event: WebhookEvents;
11 | }
12 |
13 | export interface Webhook {
14 | endpoint: string;
15 | event: WebhookEvents;
16 | secret: string;
17 | uuid: string;
18 | }
19 |
20 | export const WebhookSchema = Joi.object({
21 | endpoint: Joi.string()
22 | .uri({
23 | scheme: ['http', 'https'],
24 | })
25 | .required(),
26 | event: Joi.string()
27 | .valid(WebhookEvents.ACTIVATION, WebhookEvents.DEACTIVATION)
28 | .required(),
29 | });
30 |
--------------------------------------------------------------------------------
/websites/backend/src/webhooks/utils.ts:
--------------------------------------------------------------------------------
1 | import got from 'got';
2 | import { FlagStatus } from '../flags/flags.status';
3 | import { Webhook, WebhookEvents } from './types';
4 |
5 | export const WebhooksEventsToFlagStatus = {
6 | [WebhookEvents.ACTIVATION]: FlagStatus.ACTIVATED,
7 | [WebhookEvents.DEACTIVATION]: FlagStatus.NOT_ACTIVATED,
8 | };
9 |
10 | export const post = (webhook: Webhook) => {
11 | return got.post(webhook.endpoint, {
12 | headers: {
13 | 'x-progressively-secret': webhook.secret,
14 | },
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/websites/backend/src/webhooks/webhooks.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ActivityLogModule } from '../activity-log/activity-log.module';
3 | import { DatabaseModule } from '../database/database.module';
4 | import { WebhooksController } from './webhooks.controller';
5 | import { WebhooksService } from './webhooks.service';
6 |
7 | @Module({
8 | imports: [DatabaseModule, ActivityLogModule],
9 | controllers: [WebhooksController],
10 | providers: [WebhooksService],
11 | exports: [WebhooksService],
12 | })
13 | export class WebhooksModule {}
14 |
--------------------------------------------------------------------------------
/websites/backend/src/websocket/types.ts:
--------------------------------------------------------------------------------
1 | import { FieldRecord } from '../rule/types';
2 | import { WebSocket as WS } from 'ws';
3 |
4 | export interface LocalWebsocket extends WS {
5 | __ROOMS: Array;
6 | __FIELDS: FieldRecord;
7 | isAlive: boolean;
8 | }
9 |
10 | export type Subscriber = (
11 | args: T,
12 | fields: FieldRecord,
13 | ) => Promise;
14 |
--------------------------------------------------------------------------------
/websites/backend/src/websocket/websocket.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { WebsocketGateway } from './websocket.gateway';
3 | import { PubsubModule } from '../pubsub/pubsub.module';
4 |
5 | @Module({
6 | providers: [WebsocketGateway],
7 | exports: [WebsocketGateway],
8 | imports: [PubsubModule],
9 | })
10 | export class WebsocketModule {}
11 |
--------------------------------------------------------------------------------
/websites/backend/test/helpers/authenticate.ts:
--------------------------------------------------------------------------------
1 | import * as request from 'supertest';
2 | import { INestApplication } from '@nestjs/common';
3 |
4 | export const authenticate = async (
5 | app: INestApplication,
6 | username = 'marvin.frachet@something.com',
7 | password = 'password',
8 | ) => {
9 | const {
10 | body: { access_token, refresh_token },
11 | } = await request(app.getHttpServer()).post('/auth/login').send({
12 | username,
13 | password,
14 | });
15 |
16 | expect(refresh_token).toBeTruthy();
17 |
18 | return access_token;
19 | };
20 |
--------------------------------------------------------------------------------
/websites/backend/test/helpers/create-project.ts:
--------------------------------------------------------------------------------
1 | import * as request from 'supertest';
2 | import { INestApplication } from '@nestjs/common';
3 | import { authenticate } from './authenticate';
4 |
5 | export const createProject = async (
6 | app: INestApplication,
7 | projectName,
8 | domain?: string,
9 | ) => {
10 | const access_token = await authenticate(app);
11 | const response = await request(app.getHttpServer())
12 | .post('/projects')
13 | .set('Authorization', `Bearer ${access_token}`)
14 | .send({
15 | name: projectName,
16 | domain,
17 | });
18 |
19 | return [response, access_token] as const;
20 | };
21 |
--------------------------------------------------------------------------------
/websites/backend/test/helpers/verify-auth-guard.ts:
--------------------------------------------------------------------------------
1 | import * as request from 'supertest';
2 | import { INestApplication } from '@nestjs/common';
3 |
4 | export const verifyAuthGuard = (
5 | app: INestApplication,
6 | url: string,
7 | method: 'get' | 'post' | 'put' | 'delete',
8 | ) => {
9 | return request(app.getHttpServer())
10 | [method](url)
11 | .set(
12 | 'Authorization',
13 | `Bearer fezofhoezhfozjefokgokrpegkpgkprekgzprkgpekzgpekrgpke`,
14 | )
15 | .expect(401)
16 | .expect({
17 | statusCode: 401,
18 | message: 'Unauthorized',
19 | });
20 | };
21 |
--------------------------------------------------------------------------------
/websites/backend/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts", "tsx"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(tsx|ts|js)$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/websites/backend/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/websites/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react-jsx",
4 | "module": "commonjs",
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true,
14 | },
15 | }
16 |
--------------------------------------------------------------------------------
/websites/documentation/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
--------------------------------------------------------------------------------
/websites/documentation/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/websites/documentation/.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/documentation/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "documentation",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro check && astro build",
9 | "preview": "astro preview",
10 | "astro": "astro"
11 | },
12 | "dependencies": {
13 | "@astrojs/starlight": "^0.24.2",
14 | "astro": "^4.10.2",
15 | "sharp": "^0.32.5",
16 | "@astrojs/check": "^0.7.0",
17 | "typescript": "^5.4.5"
18 | }
19 | }
--------------------------------------------------------------------------------
/websites/documentation/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/websites/documentation/src/assets/houston.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/progressively-crew/progressively/9692c65366aeb1c6399aa18a623dde7a55379c23/websites/documentation/src/assets/houston.webp
--------------------------------------------------------------------------------
/websites/documentation/src/content/config.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection } from 'astro:content';
2 | import { docsSchema } from '@astrojs/starlight/schema';
3 |
4 | export const collections = {
5 | docs: defineCollection({ schema: docsSchema() }),
6 | };
7 |
--------------------------------------------------------------------------------
/websites/documentation/src/content/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Progressively
3 | description: Get started with the tool
4 | template: splash
5 | hero:
6 | tagline: The Product Control Tower
7 | actions:
8 | - text: Get started
9 | link: /overview/what-is-progressively
10 | icon: right-arrow
11 | variant: primary
12 | ---
13 |
--------------------------------------------------------------------------------
/websites/documentation/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/websites/documentation/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict"
3 | }
--------------------------------------------------------------------------------
/websites/frontend/.env.example:
--------------------------------------------------------------------------------
1 | BACKEND_URL="http://localhost:4000"
2 | SESSION_SECRET="abcd"
3 | ALLOW_REGISTRATION=true
4 | OKTA_ISSUER=
5 | OKTA_CLIENT_ID=
6 | STRIPE_PUBLIC_KEY=
--------------------------------------------------------------------------------
/websites/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | .env
6 |
--------------------------------------------------------------------------------
/websites/frontend/README.md:
--------------------------------------------------------------------------------
1 | For documentation, make sure to check https://progressively.app.
2 |
--------------------------------------------------------------------------------
/websites/frontend/app/components/Background.tsx:
--------------------------------------------------------------------------------
1 | export interface BackgroundProps {
2 | children: React.ReactNode;
3 | spacing?: "S" | "M";
4 | }
5 |
6 | const spacingStyles = {
7 | S: "md:py-0.5 md:px-0.5",
8 | M: "md:py-4 md:px-4",
9 | };
10 |
11 | export const Background = ({ children, spacing = "M" }: BackgroundProps) => {
12 | const spacingStyle = spacingStyles[spacing];
13 |
14 | return (
15 |
18 | {children}
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/websites/frontend/app/components/BigButton.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | interface BigButtonProps {
4 | children: ReactNode;
5 | isLoading: boolean;
6 | }
7 |
8 | export const BigButton = ({ children, isLoading }: BigButtonProps) => {
9 | return (
10 |
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 | }
9 | className="w-full md:w-auto"
10 | {...props}
11 | >
12 | {children}
13 |
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 | }
9 | className="w-full md:w-auto"
10 | {...props}
11 | >
12 | {children}
13 |
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 | }
9 | className="w-full md:w-auto"
10 | {...props}
11 | >
12 | {children}
13 |
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 | }
10 | className="w-full md:w-auto"
11 | {...props}
12 | >
13 | {children}
14 |
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 ;
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 |
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 |
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 |
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 |
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 | }
--------------------------------------------------------------------------------