├── .nvmrc
├── packages
├── core
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── src
│ │ ├── shared
│ │ │ ├── index.ts
│ │ │ ├── interfaces.ts
│ │ │ ├── constants.ts
│ │ │ └── types.ts
│ │ ├── helpers.ts
│ │ └── defineConfig.ts
│ ├── tsup.config.ts
│ ├── tests
│ │ ├── mocks
│ │ │ ├── server.ts
│ │ │ └── handlers.ts
│ │ └── setup.ts
│ └── vite.config.ts
├── next
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── tests
│ │ ├── mocks
│ │ │ ├── server.ts
│ │ │ └── handlers.ts
│ │ ├── setup.ts
│ │ └── cache.test.ts
│ ├── vite.config.ts
│ ├── tsup.config.ts
│ └── src
│ │ └── cache.ts
├── node
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── tsup.config.ts
│ ├── tests
│ │ ├── mocks
│ │ │ ├── server.ts
│ │ │ └── handlers.ts
│ │ ├── setup.ts
│ │ └── node.test.ts
│ ├── vite.config.ts
│ ├── src
│ │ ├── utils
│ │ │ └── MemoryStorage.ts
│ │ └── index.ts
│ ├── fix-exports.js
│ └── CHANGELOG.md
├── cli
│ ├── .gitignore
│ ├── src
│ │ ├── schemas.ts
│ │ ├── consts.ts
│ │ ├── sharedOptions.ts
│ │ ├── push.ts
│ │ ├── auth.ts
│ │ ├── init.ts
│ │ └── check.ts
│ ├── tests
│ │ ├── mocks
│ │ │ ├── server.ts
│ │ │ └── handlers.ts
│ │ ├── abby.config.stub.ts
│ │ └── setup.ts
│ ├── vite.config.ts
│ ├── tsup.config.ts
│ ├── tsconfig.json
│ └── CHANGELOG.md
├── react
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── src
│ │ └── index.ts
│ ├── tests
│ │ ├── mocks
│ │ │ ├── server.ts
│ │ │ └── handlers.ts
│ │ ├── setup.ts
│ │ └── helpers.test.ts
│ ├── vite.config.ts
│ └── tsup.config.ts
├── remix
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── tests
│ │ ├── mocks
│ │ │ ├── server.ts
│ │ │ └── handlers.ts
│ │ ├── setup.ts
│ │ ├── ssr.test.tsx
│ │ └── cache.test.ts
│ ├── vite.config.ts
│ ├── tsup.config.ts
│ └── src
│ │ └── cache.ts
├── devtools
│ ├── .vscode
│ │ └── extensions.json
│ ├── src
│ │ ├── vite-env.d.ts
│ │ ├── components
│ │ │ ├── CloseIcon.svelte
│ │ │ └── ChevronIcon.svelte
│ │ ├── index.ts
│ │ └── lib
│ │ │ ├── types.ts
│ │ │ └── storage.ts
│ ├── tsconfig.node.json
│ ├── svelte.config.js
│ ├── .gitignore
│ ├── .storybook
│ │ ├── preview.ts
│ │ └── main.ts
│ ├── vite.config.ts
│ └── tsconfig.json
├── tsconfig
│ ├── README.md
│ ├── package.json
│ ├── react-library.json
│ ├── base.json
│ ├── nextjs.json
│ └── angular-library.json
├── svelte
│ ├── tsconfig.node.json
│ ├── src
│ │ ├── index.ts
│ │ ├── tests
│ │ │ ├── mocks
│ │ │ │ ├── server.ts
│ │ │ │ └── handlers.ts
│ │ │ ├── abby.ts
│ │ │ ├── setupTest.ts
│ │ │ ├── pages
│ │ │ │ └── +test.svelte
│ │ │ └── withAbby.test.ts
│ │ └── lib
│ │ │ ├── AbbyDevtools.svelte
│ │ │ └── AbbyProvider.svelte
│ ├── .gitignore
│ ├── svelte.config.js
│ └── tsconfig.json
└── angular
│ ├── tsconfig.spec.json
│ ├── tsconfig.lib.prod.json
│ ├── ng-package.json
│ ├── tsconfig.lib.json
│ ├── tsconfig.json
│ ├── src
│ ├── public-api.ts
│ └── lib
│ │ ├── abby-logger.service.ts
│ │ ├── get-variant.pipe.ts
│ │ ├── get-remote-config.pipe.ts
│ │ └── devtools.component.ts
│ ├── .gitignore
│ ├── angular.json
│ └── karma.conf.js
├── pnpm-workspace.yaml
├── .vscode
├── extensions.json
└── settings.json
├── apps
├── docs
│ ├── .eslintrc.json
│ ├── public
│ │ ├── favicon.ico
│ │ └── vercel.svg
│ ├── CHANGELOG.md
│ ├── tsconfig.json
│ ├── pages
│ │ ├── integrations
│ │ │ └── _meta.json
│ │ ├── reference
│ │ │ └── _meta.json
│ │ ├── _app.mdx
│ │ ├── a-b-testing.mdx
│ │ ├── remote-config.mdx
│ │ ├── environments.mdx
│ │ ├── _meta.json
│ │ └── index.mdx
│ ├── .gitignore
│ ├── next.config.js
│ └── package.json
├── web
│ ├── public
│ │ ├── og.png
│ │ ├── favicon.ico
│ │ ├── favicon.png
│ │ ├── abby_next.png
│ │ ├── screenshot.png
│ │ ├── Mona-Sans.woff2
│ │ ├── videos
│ │ │ ├── hero.mp4
│ │ │ ├── devtools.mp4
│ │ │ └── devtools_new.mp4
│ │ ├── img
│ │ │ ├── abby-next.png
│ │ │ ├── abby-react.png
│ │ │ ├── cut-the-bs.png
│ │ │ ├── feature-flags-next.png
│ │ │ ├── featureflags-for-pms.png
│ │ │ └── modern-feature-flags.png
│ │ └── FragmentMono-Regular.ttf
│ ├── postcss.config.cjs
│ ├── src
│ │ ├── server
│ │ │ ├── services
│ │ │ │ ├── IntegrationService.ts
│ │ │ │ ├── RequestService.ts
│ │ │ │ ├── RequestCache.ts
│ │ │ │ ├── ProjectService.ts
│ │ │ │ └── InviteService.ts
│ │ │ ├── common
│ │ │ │ ├── memory-cache.ts
│ │ │ │ ├── integrations.ts
│ │ │ │ ├── ratelimit.ts
│ │ │ │ ├── auth.ts
│ │ │ │ ├── stripe.ts
│ │ │ │ ├── get-server-auth-session.ts
│ │ │ │ ├── getRequestOrigin.ts
│ │ │ │ └── config-cache.ts
│ │ │ ├── trpc
│ │ │ │ ├── router
│ │ │ │ │ ├── auth.ts
│ │ │ │ │ ├── project-user.ts
│ │ │ │ │ ├── _app.ts
│ │ │ │ │ └── apikey.ts
│ │ │ │ ├── helpers.ts
│ │ │ │ └── trpc.ts
│ │ │ └── db
│ │ │ │ ├── redis.ts
│ │ │ │ └── client.ts
│ │ ├── types
│ │ │ ├── flags.ts
│ │ │ ├── next-auth.d.ts
│ │ │ └── plausible-events.ts
│ │ ├── lib
│ │ │ ├── hooks
│ │ │ │ ├── useProjectId.ts
│ │ │ │ └── useQueryParam.ts
│ │ │ ├── utils.ts
│ │ │ ├── abby.tsx
│ │ │ ├── shiki
│ │ │ │ └── languages
│ │ │ │ │ └── jinja-html.tmLanguage.json
│ │ │ ├── graphs.ts
│ │ │ ├── tracking.ts
│ │ │ ├── logsnag.ts
│ │ │ └── environment-styles.ts
│ │ ├── pages
│ │ │ ├── api
│ │ │ │ ├── [[...route]].ts
│ │ │ │ ├── trpc
│ │ │ │ │ └── [trpc].ts
│ │ │ │ └── checkout
│ │ │ │ │ └── index.ts
│ │ │ ├── invites
│ │ │ │ └── index.tsx
│ │ │ ├── _document.tsx
│ │ │ ├── checkout
│ │ │ │ └── index.tsx
│ │ │ ├── _error.jsx
│ │ │ ├── imprint.mdx
│ │ │ ├── projects
│ │ │ │ └── [projectId]
│ │ │ │ │ └── remote-config.tsx
│ │ │ └── redeem.tsx
│ │ ├── components
│ │ │ ├── Logo.tsx
│ │ │ ├── DashboardButton.tsx
│ │ │ ├── Divider.tsx
│ │ │ ├── JSONEditor.tsx
│ │ │ ├── DashboardHeader.tsx
│ │ │ ├── AsyncCodeExample.tsx
│ │ │ ├── Test
│ │ │ │ ├── Serves.tsx
│ │ │ │ └── Metrics.tsx
│ │ │ ├── Toggle.tsx
│ │ │ ├── ui
│ │ │ │ ├── label.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── environment-badge.tsx
│ │ │ │ ├── popover.tsx
│ │ │ │ ├── switch.tsx
│ │ │ │ └── tooltip.tsx
│ │ │ ├── Button.tsx
│ │ │ ├── Input.tsx
│ │ │ ├── EventCounter.tsx
│ │ │ ├── SignupButton.tsx
│ │ │ ├── Feature.tsx
│ │ │ ├── DashboardSection.tsx
│ │ │ ├── RadioGroup.tsx
│ │ │ ├── FlagIcon.tsx
│ │ │ ├── Progress.tsx
│ │ │ ├── LoadingSpinner.tsx
│ │ │ └── Tooltip.tsx
│ │ ├── api
│ │ │ ├── routes
│ │ │ │ ├── health.ts
│ │ │ │ └── health.test.ts
│ │ │ ├── helpers.ts
│ │ │ └── index.ts
│ │ ├── utils
│ │ │ ├── apiKey.ts
│ │ │ ├── checkSession.ts
│ │ │ ├── validateFlags.ts
│ │ │ ├── updateSession.ts
│ │ │ └── trpc.ts
│ │ ├── styles
│ │ │ └── theme.json
│ │ ├── env
│ │ │ ├── server.mjs
│ │ │ └── client.mjs
│ │ └── instrumentation.ts
│ ├── prisma
│ │ ├── migrations
│ │ │ ├── 20240903210250_add_anonymous_id_to_event
│ │ │ │ └── migration.sql
│ │ │ ├── 20230303082044_use_text_for_description_for_longer_texts
│ │ │ │ └── migration.sql
│ │ │ ├── migration_lock.toml
│ │ │ ├── 20230302165144_add_description_to_feature_flag
│ │ │ │ └── migration.sql
│ │ │ ├── 20221212063242_add_plan_to_project
│ │ │ │ └── migration.sql
│ │ │ ├── 20230630140739_add_json_type
│ │ │ │ └── migration.sql
│ │ │ ├── 20230305160755_rename_flag_value_and_add_index_for_faster_access
│ │ │ │ └── migration.sql
│ │ │ ├── 20240620195120_add_apirequest_type
│ │ │ │ └── migration.sql
│ │ │ ├── 20240621084922_add_indices_for_apirequest
│ │ │ │ └── migration.sql
│ │ │ ├── 20230303130553_
│ │ │ │ └── migration.sql
│ │ │ ├── 20231207205533_add_api_version_to_request
│ │ │ │ └── migration.sql
│ │ │ ├── 20230115171608_remove
│ │ │ │ └── migration.sql
│ │ │ ├── 20230901065851_remove_hashed_ip_field
│ │ │ │ └── migration.sql
│ │ │ ├── 20221208172715_
│ │ │ │ └── migration.sql
│ │ │ ├── 20230222182342_add_sort_index_for_env
│ │ │ │ └── migration.sql
│ │ │ ├── 20221208134716_use_decimal_for_weights
│ │ │ │ └── migration.sql
│ │ │ ├── 20230602080502_remove_json_field_from_flagtype_enum
│ │ │ │ └── migration.sql
│ │ │ ├── 20230104080059_make_featureflag_name_unique_per_project
│ │ │ │ └── migration.sql
│ │ │ ├── 20230101140121_use_role_enum_for_project_users
│ │ │ │ └── migration.sql
│ │ │ ├── 20230303181813_remove_env_flag_relation
│ │ │ │ └── migration.sql
│ │ │ ├── 20231207205701_add_apiversion_enum
│ │ │ │ └── migration.sql
│ │ │ ├── 20230611180244_move_flag_type_to_flag_instead_of_value
│ │ │ │ └── migration.sql
│ │ │ ├── 20230303182053_make_id_for_flag_values_unique
│ │ │ │ └── migration.sql
│ │ │ ├── 20230903075910_add_user_onboarding_information
│ │ │ │ └── migration.sql
│ │ │ ├── 20221205132203_
│ │ │ │ └── migration.sql
│ │ │ ├── 20230104081514_fix
│ │ │ │ └── migration.sql
│ │ │ ├── 20230511064125_renmae_price_id_field_for_code
│ │ │ │ └── migration.sql
│ │ │ ├── 20230901064929_add_requests
│ │ │ │ └── migration.sql
│ │ │ ├── 20221207080237_add_event_table
│ │ │ │ └── migration.sql
│ │ │ ├── 20230511063052_add_coupon_codes_table
│ │ │ │ └── migration.sql
│ │ │ ├── 20230210162012_add_history
│ │ │ │ └── migration.sql
│ │ │ ├── 20240826064116_add_integrations
│ │ │ │ └── migration.sql
│ │ │ ├── 20221205162949_make_name_and_project_id_unique_identifier_for_test
│ │ │ │ └── migration.sql
│ │ │ ├── 20230601161058_update_flag_history
│ │ │ │ └── migration.sql
│ │ │ ├── 20221208152910_add_project_user
│ │ │ │ └── migration.sql
│ │ │ ├── 20230628081242_add_api_key_table
│ │ │ │ └── migration.sql
│ │ │ ├── 20230803094512_change_api_key_validity_logic
│ │ │ │ └── migration.sql
│ │ │ ├── 20221208123701_add_project_invites
│ │ │ │ └── migration.sql
│ │ │ ├── 20230601155320_add_multivariate_flags
│ │ │ │ └── migration.sql
│ │ │ ├── 20230326163301_make_payment_interval_required
│ │ │ │ └── migration.sql
│ │ │ ├── 20221205131914_bruh
│ │ │ │ └── migration.sql
│ │ │ ├── 20230731133223_rename_apikey
│ │ │ │ └── migration.sql
│ │ │ ├── 20221205133208_
│ │ │ │ └── migration.sql
│ │ │ ├── 20221210195541_add_stripe_info_to_projects
│ │ │ │ └── migration.sql
│ │ │ ├── 20230303181212_update_history
│ │ │ │ └── migration.sql
│ │ │ ├── 20250131071445_add_flag_rulesets
│ │ │ │ └── migration.sql
│ │ │ └── 20230104071050_add_feature_flags
│ │ │ │ └── migration.sql
│ │ ├── sql
│ │ │ ├── getEventsByTestIdForDay.sql
│ │ │ ├── getEventsByTestIdForLast30Days.sql
│ │ │ └── getEventsByTestIdForAllTime.sql
│ │ └── generateCoupons.ts
│ ├── vercel.json
│ ├── docker-compose.mailhog.yaml
│ ├── vitest.config.ts
│ ├── components.json
│ ├── next-sitemap.config.js
│ ├── sentry.server.config.ts
│ ├── sentry.client.config.ts
│ ├── .env.example
│ ├── sentry.edge.config.ts
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── abby.config.ts
│ ├── emails
│ │ └── index.tsx
│ └── README.md
└── cdn
│ ├── README.md
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── package.json
│ └── src
│ └── lib
│ └── config.ts
├── .dockerignore
├── .changeset
├── config.json
└── README.md
├── .github
└── workflows
│ ├── pull_request.yaml
│ ├── tests.yaml
│ └── publish.yaml
├── .gitignore
├── docker-compose.yaml
├── package.json
└── docs
└── integrations.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22.8.0
--------------------------------------------------------------------------------
/packages/core/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/packages/next/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/packages/node/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/packages/cli/.gitignore:
--------------------------------------------------------------------------------
1 | dist/*
--------------------------------------------------------------------------------
/packages/react/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/packages/remix/.gitignore:
--------------------------------------------------------------------------------
1 | dist
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "apps/*"
3 | - "packages/*"
4 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["svelte.svelte-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/apps/docs/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": ["custom"]
4 | }
5 |
--------------------------------------------------------------------------------
/apps/web/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/og.png
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | apps/angular-example
3 | apps/docs
4 | node_modules/
5 | **/node_modules/
--------------------------------------------------------------------------------
/apps/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/favicon.ico
--------------------------------------------------------------------------------
/apps/web/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/favicon.png
--------------------------------------------------------------------------------
/packages/devtools/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["svelte.svelte-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/apps/cdn/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | npm install
3 | npm run dev
4 | ```
5 |
6 | ```
7 | npm run deploy
8 | ```
9 |
--------------------------------------------------------------------------------
/apps/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/docs/public/favicon.ico
--------------------------------------------------------------------------------
/apps/web/public/abby_next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/abby_next.png
--------------------------------------------------------------------------------
/apps/web/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/screenshot.png
--------------------------------------------------------------------------------
/apps/web/public/Mona-Sans.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/Mona-Sans.woff2
--------------------------------------------------------------------------------
/apps/web/public/videos/hero.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/videos/hero.mp4
--------------------------------------------------------------------------------
/apps/web/public/img/abby-next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/img/abby-next.png
--------------------------------------------------------------------------------
/apps/web/public/img/abby-react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/img/abby-react.png
--------------------------------------------------------------------------------
/apps/web/public/img/cut-the-bs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/img/cut-the-bs.png
--------------------------------------------------------------------------------
/apps/web/public/videos/devtools.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/videos/devtools.mp4
--------------------------------------------------------------------------------
/packages/devtools/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/apps/web/public/FragmentMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/FragmentMono-Regular.ttf
--------------------------------------------------------------------------------
/apps/web/public/videos/devtools_new.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/videos/devtools_new.mp4
--------------------------------------------------------------------------------
/apps/web/public/img/feature-flags-next.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/img/feature-flags-next.png
--------------------------------------------------------------------------------
/packages/cli/src/schemas.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const addCommandTypeSchema = z.enum(["flag", "config"]);
4 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/apps/web/public/img/featureflags-for-pms.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/img/featureflags-for-pms.png
--------------------------------------------------------------------------------
/apps/web/public/img/modern-feature-flags.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tryabby/abby/HEAD/apps/web/public/img/modern-feature-flags.png
--------------------------------------------------------------------------------
/packages/tsconfig/README.md:
--------------------------------------------------------------------------------
1 | # `tsconfig`
2 |
3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from.
4 |
--------------------------------------------------------------------------------
/apps/docs/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # docs
2 |
3 | ## 0.2.0
4 |
5 | ### Minor Changes
6 |
7 | - add edge/api function helpers and non-react a/b test function
8 |
--------------------------------------------------------------------------------
/apps/web/src/server/services/IntegrationService.ts:
--------------------------------------------------------------------------------
1 | export namespace IntegrationService {
2 | // createIntegration(projectId: string, In)
3 | }
4 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20240903210250_add_anonymous_id_to_event/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE `Event` ADD COLUMN `anonymousId` VARCHAR(191) NULL;
3 |
--------------------------------------------------------------------------------
/apps/web/src/types/flags.ts:
--------------------------------------------------------------------------------
1 | import type { RemoteConfigValueString } from "@tryabby/core";
2 |
3 | export type FlagValueString = RemoteConfigValueString | "Boolean";
4 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230303082044_use_text_for_description_for_longer_texts/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE `FeatureFlag` MODIFY `description` TEXT NULL;
3 |
--------------------------------------------------------------------------------
/apps/web/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 = "mysql"
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230302165144_add_description_to_feature_flag/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE `FeatureFlag` ADD COLUMN `description` VARCHAR(191) NULL;
3 |
--------------------------------------------------------------------------------
/apps/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/nextjs.json",
3 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "theme.config.jsx"],
4 | "exclude": ["node_modules"]
5 | }
--------------------------------------------------------------------------------
/apps/cdn/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .wrangler
4 | .dev.vars
5 |
6 | # Change them to your taste:
7 | wrangler.toml
8 | package-lock.json
9 | yarn.lock
10 | pnpm-lock.yaml
--------------------------------------------------------------------------------
/apps/web/src/lib/hooks/useProjectId.ts:
--------------------------------------------------------------------------------
1 | import { useUnsafeQueryParam } from "./useQueryParam";
2 |
3 | export function useProjectId() {
4 | return useUnsafeQueryParam("projectId");
5 | }
6 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20221212063242_add_plan_to_project/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE `Project` ADD COLUMN `plan` ENUM('FREE', 'STARTER', 'ENTERPRISE') NOT NULL DEFAULT 'FREE';
3 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230630140739_add_json_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE `FeatureFlag` MODIFY `type` ENUM('BOOLEAN', 'STRING', 'NUMBER', 'JSON') NOT NULL DEFAULT 'BOOLEAN';
3 |
--------------------------------------------------------------------------------
/packages/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/react-library.json",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "DOM"],
5 | "types": ["vitest/globals"]
6 | }
7 | }
--------------------------------------------------------------------------------
/packages/next/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/react-library.json",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "DOM"],
5 | "types": ["vitest/globals"]
6 | }
7 | }
--------------------------------------------------------------------------------
/packages/node/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/react-library.json",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "DOM"],
5 | "types": ["vitest/globals"]
6 | }
7 | }
--------------------------------------------------------------------------------
/packages/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/react-library.json",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "DOM"],
5 | "types": ["vitest/globals"]
6 | }
7 | }
--------------------------------------------------------------------------------
/packages/remix/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/react-library.json",
3 | "compilerOptions": {
4 | "lib": ["ESNext", "DOM"],
5 | "types": ["vitest/globals"]
6 | }
7 | }
--------------------------------------------------------------------------------
/apps/web/src/server/common/memory-cache.ts:
--------------------------------------------------------------------------------
1 | import createCacheRealm from "@databases/cache";
2 |
3 | const { createCache } = createCacheRealm({ maximumSize: 10_000 });
4 |
5 | export default createCache;
6 |
--------------------------------------------------------------------------------
/packages/svelte/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node"
6 | },
7 | "include": ["vite.config.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230305160755_rename_flag_value_and_add_index_for_faster_access/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateIndex
2 | CREATE INDEX `Environment_projectId_name_idx` ON `Environment`(`projectId`, `name`);
3 |
--------------------------------------------------------------------------------
/apps/web/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "crons": [
3 | {
4 | "path": "/api/invalidate-limits?secretKey=yfMWV3TC0xyLvEKoHjslTp8GeKFEFRDtfVckg3Y2LHA=",
5 | "schedule": "0 0 * * *"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/devtools/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "Node"
6 | },
7 | "include": ["vite.config.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/node/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig({
4 | dts: true,
5 | sourcemap: true,
6 | treeshake: true,
7 | format: ["cjs", "esm"],
8 | });
9 |
--------------------------------------------------------------------------------
/packages/svelte/src/index.ts:
--------------------------------------------------------------------------------
1 | // Reexport your entry components here
2 |
3 | export { createAbby } from "./lib/createAbby";
4 | export { type ABConfig, type AbbyConfig, defineConfig } from "@tryabby/core";
5 |
--------------------------------------------------------------------------------
/apps/web/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
7 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20240620195120_add_apirequest_type/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE `ApiRequest` MODIFY `type` ENUM('GET_CONFIG', 'GET_CONFIG_SCRIPT', 'TRACK_VIEW', 'TRACK_CONVERSION') NOT NULL;
3 |
--------------------------------------------------------------------------------
/packages/core/src/shared/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./types";
2 | export * from "./schemas";
3 | export * from "./constants";
4 | export * from "./helpers";
5 | export * from "./http";
6 | export * from "./interfaces";
7 |
--------------------------------------------------------------------------------
/packages/svelte/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /dist
5 | /.svelte-kit
6 | /package
7 | .env
8 | .env.*
9 | !.env.example
10 | vite.config.js.timestamp-*
11 | vite.config.ts.timestamp-*
12 |
--------------------------------------------------------------------------------
/packages/react/src/index.ts:
--------------------------------------------------------------------------------
1 | export {
2 | createAbby,
3 | type withDevtoolsFunction,
4 | type ABTestReturnValue,
5 | } from "./context";
6 | export { type ABConfig, type AbbyConfig, defineConfig } from "@tryabby/core";
7 |
--------------------------------------------------------------------------------
/packages/cli/src/consts.ts:
--------------------------------------------------------------------------------
1 | import os from "node:os";
2 | import path from "node:path";
3 |
4 | export const ABBY_BASE_URL = "https://www.tryabby.com";
5 |
6 | export const getTokenFilePath = () => path.join(os.homedir(), ".abby");
7 |
--------------------------------------------------------------------------------
/packages/core/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig({
4 | dts: true,
5 | clean: true,
6 | sourcemap: true,
7 | treeshake: true,
8 | format: ["cjs", "esm"],
9 | });
10 |
--------------------------------------------------------------------------------
/packages/angular/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/spec",
5 | "types": ["jasmine", "node"]
6 | },
7 | "include": ["**/*.spec.ts", "**/*.d.ts"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/angular/tsconfig.lib.prod.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.lib.json",
3 | "compilerOptions": {
4 | "declarationMap": false
5 | },
6 | "angularCompilerOptions": {
7 | "compilationMode": "partial"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/angular/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
3 | "dest": "./dist",
4 | "lib": {
5 | "entryFile": "src/public-api.ts"
6 | },
7 | "allowedNonPeerDependencies": ["@tryabby/core"]
8 | }
9 |
--------------------------------------------------------------------------------
/apps/docs/pages/integrations/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "nextjs": "Next.js",
3 | "react": "React",
4 | "remix": "Remix",
5 | "svelte": "Svelte",
6 | "angular": "Angular",
7 | "node": "Node",
8 | "express": "Express",
9 | "koa": "Koa"
10 | }
11 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20240621084922_add_indices_for_apirequest/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateIndex
2 | CREATE INDEX `ApiRequest_createdAt_idx` ON `ApiRequest`(`createdAt`);
3 |
4 | -- CreateIndex
5 | CREATE INDEX `ApiRequest_type_idx` ON `ApiRequest`(`type`);
6 |
--------------------------------------------------------------------------------
/apps/web/src/pages/api/[[...route]].ts:
--------------------------------------------------------------------------------
1 | import { handle } from "@hono/node-server/vercel";
2 | import { app } from "api";
3 |
4 | export default handle(app);
5 |
6 | export const config = {
7 | api: {
8 | bodyParser: false,
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/packages/cli/tests/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from "msw/node";
2 | import { handlers } from "./handlers";
3 |
4 | // This configures a request mocking server with the given request handlers.
5 | export const server = setupServer(...handlers);
6 |
--------------------------------------------------------------------------------
/packages/core/tests/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from "msw/node";
2 | import { handlers } from "./handlers";
3 |
4 | // This configures a request mocking server with the given request handlers.
5 | export const server = setupServer(...handlers);
6 |
--------------------------------------------------------------------------------
/packages/next/tests/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from "msw/node";
2 | import { handlers } from "./handlers";
3 |
4 | // This configures a request mocking server with the given request handlers.
5 | export const server = setupServer(...handlers);
6 |
--------------------------------------------------------------------------------
/packages/node/tests/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from "msw/node";
2 | import { handlers } from "./handlers";
3 |
4 | // This configures a request mocking server with the given request handlers.
5 | export const server = setupServer(...handlers);
6 |
--------------------------------------------------------------------------------
/packages/react/tests/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from "msw/node";
2 | import { handlers } from "./handlers";
3 |
4 | // This configures a request mocking server with the given request handlers.
5 | export const server = setupServer(...handlers);
6 |
--------------------------------------------------------------------------------
/packages/remix/tests/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from "msw/node";
2 | import { handlers } from "./handlers";
3 |
4 | // This configures a request mocking server with the given request handlers.
5 | export const server = setupServer(...handlers);
6 |
--------------------------------------------------------------------------------
/packages/tsconfig/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tsconfig",
3 | "version": "0.0.0",
4 | "private": true,
5 | "files": [
6 | "base.json",
7 | "nextjs.json",
8 | "react-library.json",
9 | "angular-library.json"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/apps/docs/pages/reference/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "nextjs": "Next.js",
3 | "react": "React",
4 | "remix": "Remix",
5 | "svelte": "Svelte",
6 | "angular": "Angular",
7 | "http": "HTTP API",
8 | "cli": "CLI",
9 | "operators": "Operators"
10 | }
11 |
--------------------------------------------------------------------------------
/packages/svelte/src/tests/mocks/server.ts:
--------------------------------------------------------------------------------
1 | import { setupServer } from "msw/node";
2 | import { handlers } from "./handlers";
3 |
4 | // This configures a request mocking server with the given request handlers.
5 | export const server = setupServer(...handlers);
6 |
--------------------------------------------------------------------------------
/apps/web/docker-compose.mailhog.yaml:
--------------------------------------------------------------------------------
1 | version: '3.4'
2 |
3 | services:
4 | mailhog:
5 | image: mailhog/mailhog
6 | logging:
7 | driver: 'none' # disable saving logs
8 | ports:
9 | - 1025:1025 # smtp server
10 | - 8025:8025 # web ui
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230303130553_/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `name` on the `FlagValue` table. All the data in the column will be lost.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE `FlagValue` DROP COLUMN `name`;
9 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20231207205533_add_api_version_to_request/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE `ApiRequest` ADD COLUMN `apiVersion` VARCHAR(191) NOT NULL DEFAULT 'v0',
3 | MODIFY `type` ENUM('GET_CONFIG', 'GET_CONFIG_SCRIPT', 'TRACK_VIEW') NOT NULL;
4 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230115171608_remove/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `plan` on the `Project` table. All the data in the column will be lost.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE `Project` DROP COLUMN `plan`;
9 |
--------------------------------------------------------------------------------
/apps/web/src/components/Logo.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | as?: React.ElementType;
3 | };
4 |
5 | export default function Logo({ as: Component = "span" }: Props) {
6 | return (
7 | Abby
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/packages/cli/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { defineConfig } from "vite";
4 |
5 | export default defineConfig({
6 | test: {
7 | globals: true,
8 | environment: "jsdom",
9 | setupFiles: "./tests/setup.ts",
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/packages/core/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { defineConfig } from "vite";
4 |
5 | export default defineConfig({
6 | test: {
7 | globals: true,
8 | environment: "jsdom",
9 | setupFiles: "./tests/setup.ts",
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/packages/node/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { defineConfig } from "vite";
4 |
5 | export default defineConfig({
6 | test: {
7 | globals: true,
8 | environment: "jsdom",
9 | setupFiles: "./tests/setup.ts",
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/packages/devtools/svelte.config.js:
--------------------------------------------------------------------------------
1 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
2 |
3 | export default {
4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
5 | // for more information about preprocessors
6 | preprocess: vitePreprocess({}),
7 | };
8 |
--------------------------------------------------------------------------------
/apps/docs/pages/_app.mdx:
--------------------------------------------------------------------------------
1 | import PlausibleProvider from "next-plausible";
2 |
3 | export default function MyApp({ Component, pageProps }) {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230901065851_remove_hashed_ip_field/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `hashedIp` on the `ApiRequest` table. All the data in the column will be lost.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE `ApiRequest` DROP COLUMN `hashedIp`;
9 |
--------------------------------------------------------------------------------
/apps/web/src/components/DashboardButton.tsx:
--------------------------------------------------------------------------------
1 | import type { ComponentPropsWithRef } from "react";
2 | import { Button } from "./ui/button";
3 |
4 | type Props = ComponentPropsWithRef<"button">;
5 |
6 | export function DashboardButton({ className, ...props }: Props) {
7 | return ;
8 | }
9 |
--------------------------------------------------------------------------------
/apps/web/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | ///
3 | import tsconfigPaths from "vite-tsconfig-paths";
4 |
5 | export default defineConfig({
6 | plugins: [tsconfigPaths()],
7 | test: {
8 | environment: "jsdom",
9 | globals: true,
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/packages/cli/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig({
4 | dts: true,
5 | // we want to bundle the shared package because it's not published to npm
6 | // it's hacky :)
7 | clean: true,
8 | sourcemap: true,
9 | treeshake: true,
10 | format: ["cjs", "esm"],
11 | });
12 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/apps/web/src/components/Divider.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from "tailwind-merge";
2 |
3 | export function Divider({ className }: { className?: string }) {
4 | return (
5 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/packages/angular/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "./out-tsc/lib",
5 | "declaration": true,
6 | "declarationMap": true,
7 | "inlineSources": true,
8 | "types": [],
9 | "experimentalDecorators": true
10 | },
11 | "exclude": ["**/*.spec.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20221208172715_/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `userId` on the `Project` table. All the data in the column will be lost.
5 |
6 | */
7 | -- DropIndex
8 | DROP INDEX `Project_userId_idx` ON `Project`;
9 |
10 | -- AlterTable
11 | ALTER TABLE `Project` DROP COLUMN `userId`;
12 |
--------------------------------------------------------------------------------
/packages/cli/src/sharedOptions.ts:
--------------------------------------------------------------------------------
1 | import { Option } from "commander";
2 |
3 | export const ConfigOption = new Option(
4 | "-c, --config ",
5 | "Path to your Abby config file"
6 | );
7 |
8 | export const HostOption = new Option(
9 | "-h, --host ",
10 | "The URL of your Abby instance. Defaults to the cloud version"
11 | );
12 |
--------------------------------------------------------------------------------
/packages/next/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import react from "@vitejs/plugin-react";
4 | import { defineConfig } from "vite";
5 |
6 | export default defineConfig({
7 | plugins: [react()],
8 | test: {
9 | globals: true,
10 | environment: "jsdom",
11 | setupFiles: "./tests/setup.ts",
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/packages/react/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import react from "@vitejs/plugin-react";
4 | import { defineConfig } from "vite";
5 |
6 | export default defineConfig({
7 | plugins: [react()],
8 | test: {
9 | globals: true,
10 | environment: "jsdom",
11 | setupFiles: "./tests/setup.ts",
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/packages/remix/vite.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import react from "@vitejs/plugin-react";
4 | import { defineConfig } from "vite";
5 |
6 | export default defineConfig({
7 | plugins: [react()],
8 | test: {
9 | globals: true,
10 | environment: "jsdom",
11 | setupFiles: "./tests/setup.ts",
12 | },
13 | });
14 |
--------------------------------------------------------------------------------
/packages/tsconfig/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "React Library",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "jsx": "react-jsx",
7 | "lib": ["ES2015"],
8 | "module": "ESNext",
9 | "moduleResolution": "bundler",
10 | "target": "es6"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnSave": true,
3 | "[svelte]": {
4 | "editor.defaultFormatter": "svelte.svelte-vscode"
5 | },
6 | "[typescript][typescriptreact][typescriptangular][json]": {
7 | "editor.defaultFormatter": "biomejs.biome"
8 | },
9 | "editor.codeActionsOnSave": {
10 | "quickfix.biome": "explicit"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/src/server/common/integrations.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const githubIntegrationSettingsSchema = z
4 | .object({
5 | installationId: z.number(),
6 | repositoryIds: z.array(z.number()),
7 | })
8 | .strict();
9 |
10 | export type GithubIntegrationSettings = z.infer<
11 | typeof githubIntegrationSettingsSchema
12 | >;
13 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230222182342_add_sort_index_for_env/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[projectId,sortIndex]` on the table `Environment` will be added. If there are existing duplicate values, this will fail.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE `Environment` ADD COLUMN `sortIndex` INTEGER NOT NULL DEFAULT 0;
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20221208134716_use_decimal_for_weights/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to alter the column `chance` on the `Option` table. The data in that column could be lost. The data in that column will be cast from `Int` to `Decimal(65,30)`.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE `Option` MODIFY `chance` DECIMAL(65, 30) NOT NULL;
9 |
--------------------------------------------------------------------------------
/packages/next/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig({
4 | dts: true,
5 | // we want to bundle the shared package because it's not published to npm
6 | // it's hacky :)
7 | noExternal: ["shared/src/types"],
8 | clean: true,
9 | format: ["cjs", "esm"],
10 | sourcemap: true,
11 | treeshake: true,
12 | });
13 |
--------------------------------------------------------------------------------
/packages/react/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig({
4 | dts: true,
5 | // we want to bundle the shared package because it's not published to npm
6 | // it's hacky :)
7 | noExternal: ["shared/src/types"],
8 | clean: true,
9 | format: ["cjs", "esm"],
10 | sourcemap: true,
11 | treeshake: true,
12 | });
13 |
--------------------------------------------------------------------------------
/packages/remix/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig({
4 | dts: true,
5 | // we want to bundle the shared package because it's not published to npm
6 | // it's hacky :)
7 | noExternal: ["shared/src/types"],
8 | clean: true,
9 | format: ["cjs", "esm"],
10 | sourcemap: true,
11 | treeshake: true,
12 | });
13 |
--------------------------------------------------------------------------------
/apps/web/src/server/trpc/router/auth.ts:
--------------------------------------------------------------------------------
1 | import { protectedProcedure, publicProcedure, router } from "../trpc";
2 |
3 | export const authRouter = router({
4 | getSession: publicProcedure.query(({ ctx }) => {
5 | return ctx.session;
6 | }),
7 | getSecretMessage: protectedProcedure.query(() => {
8 | return "you can now see this secret message!";
9 | }),
10 | });
11 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230602080502_remove_json_field_from_flagtype_enum/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - The values [JSON] on the enum `FlagValue_type` will be removed. If these variants are still used in the database, this will fail.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE `FlagValue` MODIFY `type` ENUM('BOOLEAN', 'STRING', 'NUMBER') NOT NULL DEFAULT 'BOOLEAN';
9 |
--------------------------------------------------------------------------------
/apps/web/src/server/db/redis.ts:
--------------------------------------------------------------------------------
1 | import Redis from "ioredis";
2 |
3 | import { env } from "../../env/server.mjs";
4 |
5 | declare global {
6 | var redis: Redis | undefined;
7 | }
8 |
9 | // biome-ignore lint/suspicious/noRedeclare:>
10 | export const redis = global.redis || new Redis(env.REDIS_URL);
11 |
12 | if (env.NODE_ENV !== "production") {
13 | global.redis = redis;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/devtools/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/packages/devtools/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | import type { Preview } from "@storybook/svelte";
2 |
3 | const preview: Preview = {
4 | parameters: {
5 | actions: { argTypesRegex: "^on[A-Z].*" },
6 | controls: {
7 | matchers: {
8 | color: /(background|color)$/i,
9 | date: /Date$/,
10 | },
11 | },
12 | },
13 | };
14 |
15 | export default preview;
16 |
--------------------------------------------------------------------------------
/apps/web/src/pages/invites/index.tsx:
--------------------------------------------------------------------------------
1 | import type { GetServerSideProps, NextPage } from "next";
2 |
3 | export const getServerSideProps: GetServerSideProps = async (_ctx) => {
4 | return {
5 | props: {},
6 | redirect: {
7 | destination: "/",
8 | },
9 | };
10 | };
11 |
12 | const Invite: NextPage = () => {
13 | return null;
14 | };
15 |
16 | export default Invite;
17 |
--------------------------------------------------------------------------------
/apps/cdn/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "lib": [
9 | "esnext"
10 | ],
11 | "types": [
12 | "@cloudflare/workers-types"
13 | ],
14 | "jsx": "react-jsx",
15 | "jsxImportSource": "hono/jsx"
16 | },
17 | }
--------------------------------------------------------------------------------
/apps/web/src/server/services/RequestService.ts:
--------------------------------------------------------------------------------
1 | import type { ApiRequest } from "@prisma/client";
2 | import { prisma } from "server/db/client";
3 |
4 | export abstract class RequestService {
5 | static async storeRequest(request: Omit) {
6 | await prisma.apiRequest.create({
7 | data: {
8 | ...request,
9 | },
10 | });
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230104080059_make_featureflag_name_unique_per_project/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[projectId,name]` on the table `FeatureFlag` will be added. If there are existing duplicate values, this will fail.
5 |
6 | */
7 | -- CreateIndex
8 | CREATE UNIQUE INDEX `FeatureFlag_projectId_name_key` ON `FeatureFlag`(`projectId`, `name`);
9 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230101140121_use_role_enum_for_project_users/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to alter the column `role` on the `ProjectUser` table. The data in that column could be lost. The data in that column will be cast from `Int` to `Enum(EnumId(0))`.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE `ProjectUser` MODIFY `role` ENUM('ADMIN', 'USER') NOT NULL DEFAULT 'USER';
9 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230303181813_remove_env_flag_relation/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `environmentId` on the `FeatureFlag` table. All the data in the column will be lost.
5 |
6 | */
7 | -- DropIndex
8 | DROP INDEX `FeatureFlag_environmentId_idx` ON `FeatureFlag`;
9 |
10 | -- AlterTable
11 | ALTER TABLE `FeatureFlag` DROP COLUMN `environmentId`;
12 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20231207205701_add_apiversion_enum/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to alter the column `apiVersion` on the `ApiRequest` table. The data in that column could be lost. The data in that column will be cast from `VarChar(191)` to `Enum(EnumId(3))`.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE `ApiRequest` MODIFY `apiVersion` ENUM('V0', 'V1') NOT NULL DEFAULT 'V0';
9 |
--------------------------------------------------------------------------------
/packages/svelte/src/tests/abby.ts:
--------------------------------------------------------------------------------
1 | import { createAbby } from "../lib/createAbby";
2 | export const abby = createAbby({
3 | environments: [],
4 | projectId: "123",
5 | currentEnvironment: process.env.NODE_ENV,
6 | tests: {
7 | "New Test3": {
8 | variants: ["A", "B"],
9 | },
10 | },
11 | flags: ["flag1"],
12 | remoteConfig: {
13 | remoteConfig1: "String",
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/apps/web/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.cjs",
8 | "css": "src/styles/shadcn.css",
9 | "baseColor": "slate",
10 | "cssVariables": true
11 | },
12 | "aliases": {
13 | "utils": "lib/utils",
14 | "components": "components"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/core/src/shared/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface StorageServiceOptions {
2 | expiresInDays?: number;
3 | }
4 |
5 | export interface IStorageService {
6 | get(projectId: string, key: string): string | null;
7 | set(
8 | projectId: string,
9 | key: string,
10 | value: string,
11 | options?: StorageServiceOptions
12 | ): void;
13 | remove(projectId: string, key: string): void;
14 | }
15 |
--------------------------------------------------------------------------------
/.github/workflows/pull_request.yaml:
--------------------------------------------------------------------------------
1 | name: Lint and Format
2 |
3 | on:
4 | push:
5 | pull_request:
6 |
7 | jobs:
8 | quality:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v4
13 | - name: Setup Biome
14 | uses: biomejs/setup-biome@v2
15 | with:
16 | version: latest
17 | - name: Run Biome
18 | run: biome ci .
19 |
--------------------------------------------------------------------------------
/packages/angular/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/angular-library.json",
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "paths": {
6 | "angular": ["dist"]
7 | }
8 | },
9 | "angularCompilerOptions": {
10 | "enableI18nLegacyMessageIdFormat": false,
11 | "strictInjectionParameters": true,
12 | "strictInputAccessModifiers": true,
13 | "strictTemplates": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/apps/cdn/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tryabby/cdn",
3 | "private": true,
4 | "scripts": {
5 | "dev": "wrangler dev src/index.ts",
6 | "deploy": "wrangler deploy --minify src/index.ts"
7 | },
8 | "dependencies": {
9 | "hono": "^4.5.8"
10 | },
11 | "devDependencies": {
12 | "@tryabby/core": "workspace:*",
13 | "@cloudflare/workers-types": "^4.20231218.0",
14 | "wrangler": "^3.22.0"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/angular/src/public-api.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Public API Surface of angular
3 | */
4 |
5 | export * from "./lib/abby.service";
6 | export * from "./lib/abby.module";
7 | export * from "./lib/test.directive";
8 | export * from "./lib/flag.directive";
9 | export * from "./lib/get-variant.pipe";
10 | export * from "./lib/devtools.component";
11 | export * from "./lib/get-remote-config.pipe";
12 | export { defineConfig } from "@tryabby/core";
13 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230611180244_move_flag_type_to_flag_instead_of_value/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `type` on the `FlagValue` table. All the data in the column will be lost.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE `FeatureFlag` ADD COLUMN `type` ENUM('BOOLEAN', 'STRING', 'NUMBER') NOT NULL DEFAULT 'BOOLEAN';
9 |
10 | -- AlterTable
11 | ALTER TABLE `FlagValue` DROP COLUMN `type`;
12 |
--------------------------------------------------------------------------------
/packages/devtools/src/components/CloseIcon.svelte:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230303182053_make_id_for_flag_values_unique/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[id]` on the table `FlagValue` will be added. If there are existing duplicate values, this will fail.
5 |
6 | */
7 | -- DropIndex
8 | DROP INDEX `FlagValue_flagId_environmentId_key` ON `FlagValue`;
9 |
10 | -- CreateIndex
11 | CREATE UNIQUE INDEX `FlagValue_id_key` ON `FlagValue`(`id`);
12 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230903075910_add_user_onboarding_information/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE `User` ADD COLUMN `experienceLevelFlags` INTEGER NULL,
3 | ADD COLUMN `experienceLevelTests` INTEGER NULL,
4 | ADD COLUMN `hasCompletedOnboarding` BOOLEAN NOT NULL DEFAULT false,
5 | ADD COLUMN `profession` VARCHAR(191) NULL,
6 | ADD COLUMN `technologies` JSON NULL;
7 |
8 |
9 | UPDATE `User` SET `hasCompletedOnboarding` = true;
--------------------------------------------------------------------------------
/apps/web/src/server/db/client.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | import { env } from "../../env/server.mjs";
3 |
4 | declare global {
5 | var prisma: PrismaClient | undefined;
6 | }
7 |
8 | // biome-ignore lint/suspicious/noRedeclare:>
9 | export const prisma =
10 | global.prisma ||
11 | new PrismaClient({
12 | log: ["error"],
13 | });
14 |
15 | if (env.NODE_ENV !== "production") {
16 | global.prisma = prisma;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "tsconfig/base.json",
3 | "compilerOptions": {
4 | "target": "es5",
5 | "module": "commonjs",
6 | "lib": ["es6", "es2015", "dom"],
7 | "declaration": true,
8 | "outDir": "dist",
9 | "rootDir": "",
10 | "strict": true,
11 | "types": ["node", "vitest/globals"],
12 | "esModuleInterop": true,
13 | "resolveJsonModule": true,
14 | "skipLibCheck": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/apps/web/next-sitemap.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next-sitemap').IConfig} */
2 | module.exports = {
3 | siteUrl: process.env.SITE_URL || "https://www.tryabby.com",
4 | generateRobotsTxt: true, // (optional)
5 | exclude: [
6 | "/test",
7 | "/checkout",
8 | "/projects",
9 | "/invites",
10 | "/marketing/*",
11 | "/redeem",
12 | "/profile",
13 | "/profile/*",
14 | "/welcome",
15 | ],
16 | // ...other options
17 | };
18 |
--------------------------------------------------------------------------------
/apps/web/src/api/routes/health.ts:
--------------------------------------------------------------------------------
1 | import { Hono } from "hono";
2 | import { prisma } from "server/db/client";
3 | import { redis } from "server/db/redis";
4 |
5 | export function makeHealthRoute() {
6 | const app = new Hono().get("/", async (c) => {
7 | await Promise.allSettled([
8 | await prisma.verificationToken.count(),
9 | await redis.get("test"),
10 | ]);
11 | return c.json({ status: "ok" });
12 | });
13 | return app;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/core/src/shared/constants.ts:
--------------------------------------------------------------------------------
1 | export const DOCS_URL = "https://docs.tryabby.com/";
2 | export const ABBY_BASE_URL = "https://www.tryabby.com/";
3 | export const ABBY_AB_STORAGE_PREFIX = "__abby__ab__";
4 | export const ABBY_FF_STORAGE_PREFIX = "__abby__ff__";
5 | export const ABBY_RC_STORAGE_PREFIX = "__abby__rc__";
6 | export const ABBY_INSTANCE_KEY = "__abby_instance__";
7 | export const DEFAULT_FEATURE_FLAG_VALUE = false;
8 | export const ABBY_WINDOW_KEY = "__abby_data__";
9 |
--------------------------------------------------------------------------------
/packages/cli/src/push.ts:
--------------------------------------------------------------------------------
1 | import { HttpService } from "./http";
2 | import { loadLocalConfig } from "./util";
3 |
4 | export async function push({
5 | apiKey,
6 | apiUrl,
7 | configPath,
8 | }: {
9 | apiKey: string;
10 | apiUrl?: string;
11 | configPath?: string;
12 | }) {
13 | const { config } = await loadLocalConfig({ configPath });
14 |
15 | await HttpService.updateConfigOnServer({
16 | apiKey,
17 | localAbbyConfig: config,
18 | apiUrl,
19 | });
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20221205132203_/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the `ProjectMemeber` table. If the table is not empty, all the data it contains will be lost.
5 | - Added the required column `userId` to the `Project` table without a default value. This is not possible if the table is not empty.
6 |
7 | */
8 | -- AlterTable
9 | ALTER TABLE `Project` ADD COLUMN `userId` VARCHAR(191) NOT NULL;
10 |
11 | -- DropTable
12 | DROP TABLE `ProjectMemeber`;
13 |
--------------------------------------------------------------------------------
/apps/web/src/utils/apiKey.ts:
--------------------------------------------------------------------------------
1 | import { createHmac, randomBytes } from "node:crypto";
2 | import { env } from "env/server.mjs";
3 |
4 | export function generateRandomString(length = 32): string {
5 | const apiKey = randomBytes(length).toString("hex");
6 | return apiKey;
7 | }
8 |
9 | export function hashString(data: string): string {
10 | const hmac = createHmac("sha256", env.HASHING_SECRET);
11 | hmac.update(data);
12 | const hashKey = hmac.digest("hex");
13 | return hashKey;
14 | }
15 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230104081514_fix/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[projectId,name,environmentId]` on the table `FeatureFlag` will be added. If there are existing duplicate values, this will fail.
5 |
6 | */
7 | -- DropIndex
8 | DROP INDEX `FeatureFlag_projectId_name_key` ON `FeatureFlag`;
9 |
10 | -- CreateIndex
11 | CREATE UNIQUE INDEX `FeatureFlag_projectId_name_environmentId_key` ON `FeatureFlag`(`projectId`, `name`, `environmentId`);
12 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230511064125_renmae_price_id_field_for_code/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `planId` on the `CouponCodes` table. All the data in the column will be lost.
5 | - Added the required column `stripePriceId` to the `CouponCodes` table without a default value. This is not possible if the table is not empty.
6 |
7 | */
8 | -- AlterTable
9 | ALTER TABLE `CouponCodes` DROP COLUMN `planId`,
10 | ADD COLUMN `stripePriceId` VARCHAR(191) NOT NULL;
11 |
--------------------------------------------------------------------------------
/apps/web/src/components/JSONEditor.tsx:
--------------------------------------------------------------------------------
1 | import { Editor } from "@monaco-editor/react";
2 |
3 | type Props = {
4 | value: string;
5 | onChange: (value: string) => void;
6 | };
7 |
8 | export function JSONEditor({ onChange, value }: Props) {
9 | return (
10 | onChange(e ?? "")}
15 | className="min-h-[300px]"
16 | options={{ padding: { top: 20 } }}
17 | />
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/web/src/lib/hooks/useQueryParam.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/router";
2 |
3 | export function useQueryParam(param: string) {
4 | const router = useRouter();
5 | const query = router.query;
6 | const queryParam = query[param];
7 | return (Array.isArray(queryParam) ? queryParam[0] : queryParam) as
8 | | T
9 | | undefined;
10 | }
11 |
12 | export function useUnsafeQueryParam(param: string) {
13 | return useQueryParam(param) as T;
14 | }
15 |
--------------------------------------------------------------------------------
/packages/cli/tests/abby.config.stub.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "@tryabby/core";
2 |
3 | export default defineConfig(
4 | {
5 | projectId: process.env.ABBY_PROJECT_ID!,
6 | currentEnvironment: "development",
7 | },
8 | {
9 | environments: [],
10 | tests: {
11 | test1: {
12 | variants: ["A", "B", "C", "D"],
13 | },
14 | test2: {
15 | variants: ["A", "B"],
16 | },
17 | },
18 | flags: ["flag1"],
19 | remoteConfig: { flag2: "Number" },
20 | }
21 | );
22 |
--------------------------------------------------------------------------------
/packages/core/tests/setup.ts:
--------------------------------------------------------------------------------
1 | import { afterEach } from "vitest";
2 |
3 | import fetch from "node-fetch";
4 | import { server } from "./mocks/server";
5 |
6 | /// @ts-ignore
7 | global.fetch = fetch;
8 |
9 | // Establish API mocking before all tests.
10 | beforeAll(() => server.listen());
11 | // Reset any request handlers that we may add during the tests,
12 | // so they don't affect other tests.
13 | afterEach(() => server.resetHandlers());
14 | // Clean up after the tests are finished.
15 | afterAll(() => server.close());
16 |
--------------------------------------------------------------------------------
/packages/node/src/utils/MemoryStorage.ts:
--------------------------------------------------------------------------------
1 | import type { IStorageService } from "@tryabby/core";
2 |
3 | export class InMemoryStorageService implements IStorageService {
4 | cache: Map;
5 | constructor() {
6 | this.cache = new Map();
7 | }
8 | get(key: string): string | null {
9 | return this.cache.get(key) ?? null;
10 | }
11 | set(key: string, value: string): void {
12 | this.cache.set(key, value);
13 | }
14 | remove(key: string): void {
15 | this.cache.delete(key);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/packages/node/tests/setup.ts:
--------------------------------------------------------------------------------
1 | import { afterEach } from "vitest";
2 |
3 | import fetch from "node-fetch";
4 | import { server } from "./mocks/server";
5 |
6 | /// @ts-ignore
7 | global.fetch = fetch;
8 |
9 | // Establish API mocking before all tests.
10 | beforeAll(() => server.listen());
11 | // Reset any request handlers that we may add during the tests,
12 | // so they don't affect other tests.
13 | afterEach(() => server.resetHandlers());
14 | // Clean up after the tests are finished.
15 | afterAll(() => server.close());
16 |
--------------------------------------------------------------------------------
/apps/web/src/lib/abby.tsx:
--------------------------------------------------------------------------------
1 | import abbyDevtools from "@tryabby/devtools";
2 | import { createAbby } from "@tryabby/next";
3 | import abbyConfig from "../../abby.config";
4 |
5 | export const {
6 | useAbby,
7 | AbbyProvider,
8 | useFeatureFlag,
9 | withAbby,
10 | getFeatureFlagValue,
11 | withDevtools,
12 | withAbbyEdge,
13 | getABTestValue,
14 | withAbbyApiHandler,
15 | getABResetFunction,
16 | updateUserProperties,
17 | } = createAbby(abbyConfig);
18 |
19 | export const AbbyDevtools = withDevtools(abbyDevtools, {});
20 |
--------------------------------------------------------------------------------
/packages/devtools/src/index.ts:
--------------------------------------------------------------------------------
1 | import type { ComponentProps } from "svelte";
2 | import DevtoolsComponent from "./Devtools.svelte";
3 |
4 | export class DevtoolsFactory {
5 | create(props: AbbyDevtoolProps) {
6 | const component = new DevtoolsComponent({
7 | target: document.body,
8 | props,
9 | });
10 |
11 | return () => {
12 | component?.$destroy();
13 | };
14 | }
15 | }
16 |
17 | export default new DevtoolsFactory();
18 |
19 | export type AbbyDevtoolProps = ComponentProps;
20 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/packages/cli/src/auth.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs/promises";
2 | import { z } from "zod";
3 | import { getTokenFilePath } from "./consts";
4 |
5 | export const tokenFileSchema = z.object({
6 | token: z.string(),
7 | });
8 |
9 | export function writeTokenFile(token: string) {
10 | return fs.writeFile(getTokenFilePath(), JSON.stringify({ token }));
11 | }
12 |
13 | export async function getToken() {
14 | const contents = await fs.readFile(getTokenFilePath(), "utf-8");
15 | return tokenFileSchema.parse(JSON.parse(contents)).token;
16 | }
17 |
--------------------------------------------------------------------------------
/apps/web/src/server/common/ratelimit.ts:
--------------------------------------------------------------------------------
1 | import { RateLimiterRedis } from "rate-limiter-flexible";
2 | import { redis } from "server/db/redis";
3 |
4 | export const rateLimiter = new RateLimiterRedis({
5 | storeClient: redis,
6 | keyPrefix: "rateLimiter",
7 | points: 10, // Number of points
8 | duration: 10, // Per 10 seconds
9 | });
10 |
11 | export const checkRateLimit = async (ip: string) => {
12 | try {
13 | await rateLimiter.consume(ip);
14 | return true;
15 | } catch (_rateLimiterRes) {
16 | return false;
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/packages/node/fix-exports.js:
--------------------------------------------------------------------------------
1 | const pkg = require("./package.json");
2 | const fs = require("node:fs");
3 |
4 | const basePkgJson = {
5 | main: "index.js",
6 | types: "index.d.ts",
7 | };
8 |
9 | function main() {
10 | Object.keys(pkg.exports).forEach((key) => {
11 | if (key === ".") {
12 | return;
13 | }
14 |
15 | const newPath = key.replace("./", "dist/");
16 |
17 | fs.writeFileSync(
18 | `./${newPath}/package.json`,
19 | JSON.stringify(basePkgJson, null, 2)
20 | );
21 | });
22 | }
23 |
24 | main();
25 |
--------------------------------------------------------------------------------
/.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 | build
15 |
16 | # misc
17 | .env
18 | .idea
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # turbo
35 | .turbo
36 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230901064929_add_requests/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `ApiRequest` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
5 | `type` ENUM('GET_CONFIG', 'TRACK_VIEW') NOT NULL,
6 | `hashedIp` VARCHAR(191) NOT NULL,
7 | `durationInMs` INTEGER NOT NULL,
8 | `projectId` VARCHAR(191) NOT NULL,
9 |
10 | INDEX `ApiRequest_projectId_idx`(`projectId`),
11 | PRIMARY KEY (`id`)
12 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
13 |
--------------------------------------------------------------------------------
/packages/cli/tests/setup.ts:
--------------------------------------------------------------------------------
1 | import { afterEach } from "vitest";
2 |
3 | import fetch from "node-fetch";
4 | import { server } from "./mocks/server";
5 |
6 | /// @ts-ignore
7 | global.fetch = fetch;
8 |
9 | // Establish API mocking before all tests.
10 | beforeAll(() => server.listen());
11 |
12 | afterAll(() => server.close());
13 |
14 | // Clean up after the tests are finished.
15 | afterEach(() => {
16 | // Reset any request handlers that we may add during the tests,
17 | // so they don't affect other tests.
18 | server.resetHandlers();
19 | });
20 |
--------------------------------------------------------------------------------
/apps/docs/pages/a-b-testing.mdx:
--------------------------------------------------------------------------------
1 | # A/B Tests
2 |
3 | Abby allows you to define different tests for your application.
4 | A test is uniquely identified by a name. After creating a test you can define weights for each variant of the test.
5 | By default all variants have the same chance.
6 |
7 | When implementing a test in you application you can show different things depending on the selected variant.
8 | The variants will be assigned to users randomly, but the same user will always get the same variant.
9 | The assignment of variants uses the defined weights.
10 |
--------------------------------------------------------------------------------
/apps/docs/.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 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/apps/web/prisma/sql/getEventsByTestIdForDay.sql:
--------------------------------------------------------------------------------
1 | -- @param {String} $1:testId
2 | -- @param {Int} $2:type
3 | -- @param {DateTime} $3:date
4 |
5 | SELECT
6 | COUNT(DISTINCT CASE WHEN anonymousId IS NOT NULL THEN
7 | anonymousId
8 | ELSE
9 | CONCAT('NULL_', UUID())
10 | END) AS uniqueEventCount,
11 | COUNT(*) AS eventCount,
12 | TYPE,
13 | createdAt,
14 | selectedVariant
15 | FROM
16 | `Event`
17 | WHERE
18 | testId = ?
19 | AND TYPE = ?
20 | AND DATE(createdAt) = DATE(?)
21 | GROUP BY
22 | HOUR(createdAt),
23 | selectedVariant
24 | ORDER BY createdAt ASC;
--------------------------------------------------------------------------------
/apps/web/prisma/sql/getEventsByTestIdForLast30Days.sql:
--------------------------------------------------------------------------------
1 | -- @param {String} $1:testId
2 | -- @param {Int} $2:type
3 |
4 | SELECT
5 | COUNT(DISTINCT CASE WHEN anonymousId IS NOT NULL THEN
6 | anonymousId
7 | ELSE
8 | CONCAT('NULL_', UUID())
9 | END) AS uniqueEventCount,
10 | COUNT(*) AS eventCount,
11 | TYPE,
12 | createdAt,
13 | selectedVariant
14 | FROM
15 | `Event`
16 | WHERE
17 | testId = ?
18 | AND TYPE = ?
19 | AND createdAt >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
20 | GROUP BY
21 | DATE(createdAt),
22 | selectedVariant
23 | ORDER BY createdAt ASC;
24 |
--------------------------------------------------------------------------------
/apps/web/src/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import { Head, Html, Main, NextScript } from "next/document";
2 |
3 | export default function Document() {
4 | return (
5 |
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/apps/web/src/server/common/auth.ts:
--------------------------------------------------------------------------------
1 | import { TRPCError } from "@trpc/server";
2 | import { prisma } from "server/db/client";
3 |
4 | export async function assertUserHasAcessToProject(
5 | projectId: string,
6 | userId: string
7 | ) {
8 | const project = await prisma.project.findFirst({
9 | where: {
10 | id: projectId,
11 | users: {
12 | some: {
13 | userId: userId,
14 | },
15 | },
16 | },
17 | });
18 | if (!project) {
19 | throw new TRPCError({ code: "UNAUTHORIZED" });
20 | }
21 | return project;
22 | }
23 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20221207080237_add_event_table/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `Event` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `testId` VARCHAR(191) NOT NULL,
5 | `type` INTEGER NOT NULL,
6 | `selectedVariant` VARCHAR(191) NOT NULL,
7 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
8 |
9 | INDEX `Event_testId_idx`(`testId`),
10 | INDEX `Event_type_idx`(`type`),
11 | INDEX `Event_selectedVariant_idx`(`selectedVariant`),
12 | PRIMARY KEY (`id`)
13 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
14 |
--------------------------------------------------------------------------------
/apps/web/src/components/DashboardHeader.tsx:
--------------------------------------------------------------------------------
1 | import { useTracking } from "lib/tracking";
2 | import { CodeSnippetModalButton } from "./CodeSnippetModalButton";
3 |
4 | type Props = {
5 | title: string;
6 | };
7 |
8 | export function DashboardHeader({ title }: Props) {
9 | const _trackEvent = useTracking();
10 | return (
11 |
12 |
{title}
13 |
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/src/server/trpc/router/project-user.ts:
--------------------------------------------------------------------------------
1 | import { prisma } from "server/db/client";
2 | import { z } from "zod";
3 | import { protectedProcedure, router } from "../trpc";
4 |
5 | export const projectUserRouter = router({
6 | addUser: protectedProcedure
7 | .input(z.object({ projectId: z.string(), userId: z.string() }))
8 | .mutation(async ({ input, ctx }) => {
9 | await prisma.projectUser.create({
10 | data: {
11 | projectId: input.projectId,
12 | userId: ctx.session.user.id,
13 | },
14 | });
15 | }),
16 | });
17 |
--------------------------------------------------------------------------------
/apps/web/src/server/trpc/helpers.ts:
--------------------------------------------------------------------------------
1 | import { createServerSideHelpers } from "@trpc/react-query/server";
2 | import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
3 | import superjson from "superjson";
4 | import { createContext } from "./context";
5 | import { appRouter } from "./router/_app";
6 |
7 | export async function getSSRTrpc(opts: CreateNextContextOptions) {
8 | return createServerSideHelpers({
9 | router: appRouter,
10 | ctx: await createContext(opts),
11 | transformer: superjson, // optional - adds superjson serialization
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/apps/docs/next.config.js:
--------------------------------------------------------------------------------
1 | const withNextra = require("nextra")({
2 | theme: "nextra-theme-docs",
3 | themeConfig: "./theme.config.jsx",
4 | defaultShowCopyCode: true,
5 | });
6 |
7 | const { withPlausibleProxy } = require("next-plausible");
8 |
9 | /** @type {import('next').NextConfig} */
10 | const nextConfig = {
11 | reactStrictMode: true,
12 | swcMinify: true,
13 | // use this to add to all pages
14 | i18n: {
15 | locales: ["en"],
16 | defaultLocale: "en",
17 | },
18 | };
19 |
20 | module.exports = withPlausibleProxy()(withNextra(nextConfig));
21 |
--------------------------------------------------------------------------------
/apps/web/src/utils/checkSession.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from "next";
2 | import { type Session, unstable_getServerSession } from "next-auth";
3 | import { authOptions } from "../pages/api/auth/[...nextauth]";
4 |
5 | const checkSession = async (
6 | req: NextApiRequest,
7 | res: NextApiResponse
8 | ): Promise => {
9 | const session = await unstable_getServerSession(req, res, authOptions);
10 | if (session) {
11 | return session;
12 | }
13 | throw Error("Couldn't find a session.");
14 | };
15 |
16 | export default checkSession;
17 |
--------------------------------------------------------------------------------
/packages/devtools/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from "@storybook/svelte-vite";
2 | const config: StorybookConfig = {
3 | core: {
4 | disableTelemetry: true,
5 | },
6 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx|svelte)"],
7 | addons: [
8 | "@storybook/addon-links",
9 | "@storybook/addon-essentials",
10 | "@storybook/addon-interactions",
11 | ],
12 | framework: {
13 | name: "@storybook/svelte-vite",
14 | options: {},
15 | },
16 | docs: {
17 | autodocs: "tag",
18 | },
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/apps/web/src/components/AsyncCodeExample.tsx:
--------------------------------------------------------------------------------
1 | import { trpc } from "utils/trpc";
2 | import { BaseCodeSnippet } from "./CodeSnippet";
3 | import { LoadingSpinner } from "./LoadingSpinner";
4 |
5 | export function AsyncCodeExample() {
6 | const { data, isLoading } = trpc.example.exampleSnippet.useQuery();
7 |
8 | if (isLoading) {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 | if (!data) return null;
16 |
17 | return ;
18 | }
19 |
--------------------------------------------------------------------------------
/packages/svelte/src/lib/AbbyDevtools.svelte:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230511063052_add_coupon_codes_table/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `CouponCodes` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
5 | `code` VARCHAR(191) NOT NULL,
6 | `planId` VARCHAR(191) NOT NULL,
7 | `redeemedAt` DATETIME(3) NULL,
8 | `redeemedById` VARCHAR(191) NULL,
9 |
10 | UNIQUE INDEX `CouponCodes_code_key`(`code`),
11 | INDEX `CouponCodes_redeemedById_idx`(`redeemedById`),
12 | PRIMARY KEY (`id`)
13 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
14 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230210162012_add_history/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `FeatureFlagHistory` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
5 | `oldValue` BOOLEAN NULL,
6 | `newValue` BOOLEAN NULL,
7 | `featureFlagId` VARCHAR(191) NOT NULL,
8 | `userId` VARCHAR(191) NOT NULL,
9 |
10 | INDEX `FeatureFlagHistory_featureFlagId_idx`(`featureFlagId`),
11 | INDEX `FeatureFlagHistory_userId_idx`(`userId`),
12 | PRIMARY KEY (`id`)
13 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
14 |
--------------------------------------------------------------------------------
/apps/docs/pages/remote-config.mdx:
--------------------------------------------------------------------------------
1 | # Remote Configuration variables
2 |
3 | Remote Configuration variables are custom String, Number and JSON values, which you can control per [Environment](environments)
4 |
5 | They behave similarly to Feature Flags, but allow you to control more types of values than just booleans.
6 | You could for example display a dynamic string on your page, which you can easily change on the Abby Dashboard for all users.
7 |
8 | The [Abby Devtools](devtools) also allow you to control Remote Configuration variables in realtime during development,
9 | providing a nice developer experience.
10 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20240826064116_add_integrations/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `Integration` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
5 | `updatedAt` DATETIME(3) NOT NULL,
6 | `type` ENUM('GITHUB') NOT NULL,
7 | `settings` JSON NOT NULL,
8 | `projectId` VARCHAR(191) NOT NULL,
9 |
10 | INDEX `Integration_projectId_idx`(`projectId`),
11 | UNIQUE INDEX `Integration_projectId_type_key`(`projectId`, `type`),
12 | PRIMARY KEY (`id`)
13 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
14 |
--------------------------------------------------------------------------------
/packages/devtools/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { svelte } from "@sveltejs/vite-plugin-svelte";
2 | import { defineConfig } from "vite";
3 | import dts from "vite-plugin-dts";
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | build: {
8 | lib: {
9 | entry: ["src/index.ts"],
10 | formats: ["umd", "es"],
11 | fileName: "index",
12 | name: "abbyDevtools",
13 | },
14 | },
15 | plugins: [
16 | dts(),
17 | svelte({
18 | include: ["src/**/*.svelte"],
19 | emitCss: false,
20 | prebundleSvelteLibraries: true,
21 | }),
22 | ],
23 | });
24 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20221205162949_make_name_and_project_id_unique_identifier_for_test/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[projectId,name]` on the table `Test` will be added. If there are existing duplicate values, this will fail.
5 | - Made the column `projectId` on table `Test` required. This step will fail if there are existing NULL values in that column.
6 |
7 | */
8 | -- AlterTable
9 | ALTER TABLE `Test` MODIFY `projectId` VARCHAR(191) NOT NULL;
10 |
11 | -- CreateIndex
12 | CREATE UNIQUE INDEX `Test_projectId_name_key` ON `Test`(`projectId`, `name`);
13 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230601161058_update_flag_history/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to alter the column `oldValue` on the `FeatureFlagHistory` table. The data in that column could be lost. The data in that column will be cast from `TinyInt` to `LongText`.
5 | - You are about to alter the column `newValue` on the `FeatureFlagHistory` table. The data in that column could be lost. The data in that column will be cast from `TinyInt` to `LongText`.
6 |
7 | */
8 | -- AlterTable
9 | ALTER TABLE `FeatureFlagHistory` MODIFY `oldValue` LONGTEXT NULL,
10 | MODIFY `newValue` LONGTEXT NULL;
11 |
--------------------------------------------------------------------------------
/apps/docs/pages/environments.mdx:
--------------------------------------------------------------------------------
1 | # Environments
2 |
3 | Environments in Abby allow you to reflect the different environments that your application may be deployed to.
4 | For example, you may have a `development` environment, a `staging` environment, and a `production` environment.
5 |
6 | When using [Feature Flags](/feature-flags) it is often useful to have different values for the same flag in different environments.
7 |
8 | [Remote Configuration variables](/remote-config) can also be controlled per environment.
9 |
10 | When you create a new Environment all Flags will get a default value of `false` for that environment.
11 |
--------------------------------------------------------------------------------
/apps/web/src/pages/checkout/index.tsx:
--------------------------------------------------------------------------------
1 | import type { NextPage } from "next";
2 | import { checkout } from "pages/api/checkout";
3 |
4 | const Projects: NextPage = () => {
5 | return (
6 | <>
7 |
22 | >
23 | );
24 | };
25 |
26 | export default Projects;
27 |
--------------------------------------------------------------------------------
/packages/next/src/cache.ts:
--------------------------------------------------------------------------------
1 | export class PromiseCache {
2 | private ttl: number;
3 | private cache: Map }> =
4 | new Map();
5 |
6 | constructor(ttl = 1000 * 60) {
7 | this.ttl = ttl;
8 | }
9 |
10 | public async get(key: string, fn: () => Promise): Promise {
11 | const cached = this.cache.get(key);
12 | if (cached && Date.now() - cached.storedAt < this.ttl) {
13 | return cached.value;
14 | }
15 |
16 | const promise = fn();
17 | this.cache.set(key, { value: promise, storedAt: Date.now() });
18 | return promise;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/remix/src/cache.ts:
--------------------------------------------------------------------------------
1 | export class PromiseCache {
2 | private ttl: number;
3 | private cache: Map }> =
4 | new Map();
5 |
6 | constructor(ttl = 1000 * 60) {
7 | this.ttl = ttl;
8 | }
9 |
10 | public async get(key: string, fn: () => Promise): Promise {
11 | const cached = this.cache.get(key);
12 | if (cached && Date.now() - cached.storedAt < this.ttl) {
13 | return cached.value;
14 | }
15 |
16 | const promise = fn();
17 | this.cache.set(key, { value: promise, storedAt: Date.now() });
18 | return promise;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/tsconfig/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "composite": false,
6 | "declaration": true,
7 | "declarationMap": true,
8 | "esModuleInterop": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "inlineSources": false,
11 | "isolatedModules": true,
12 | "moduleResolution": "node",
13 | "noUnusedLocals": false,
14 | "noUnusedParameters": false,
15 | "preserveWatchOutput": true,
16 | "skipLibCheck": true,
17 | "strict": true
18 | },
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/apps/web/src/server/common/stripe.ts:
--------------------------------------------------------------------------------
1 | import { env } from "env/server.mjs";
2 | import Stripe from "stripe";
3 |
4 | if (process.browser)
5 | throw new Error(
6 | "DO NOT USE stripe/server.ts IN THE BROWSER AS YOU WILL EXPOSE FULL CONTROL OVER YOUR STRIPE ACCOUNT!"
7 | );
8 |
9 | if (!env.STRIPE_SECRET_KEY)
10 | throw new Error("Please provide a STRIPE_SECRET_KEY environment variable!");
11 |
12 | const stripe = new Stripe(env.STRIPE_SECRET_KEY, {
13 | // @ts-ignore The Stripe docs state that null denotes the Stripe account's default version and to use ts-ignore
14 | apiVersion: null,
15 | });
16 |
17 | export { stripe };
18 |
--------------------------------------------------------------------------------
/packages/core/src/helpers.ts:
--------------------------------------------------------------------------------
1 | // taken from https://stackoverflow.com/questions/3393854/get-and-set-a-single-cookie-with-node-js-http-server
2 | export function parseCookies(cookieHeader: string) {
3 | const cookieRegexp = /([^;=\s]*)=([^;]*)/g;
4 | const cookies: Record = {};
5 | // biome-ignore lint/suspicious/noAssignInExpressions:>
6 | // biome-ignore lint/suspicious/noImplicitAnyLet:>
7 | for (let m; (m = cookieRegexp.exec(cookieHeader)); ) {
8 | const [, cookieName, cookieValue] = m;
9 | cookies[decodeURIComponent(cookieName)] = decodeURIComponent(cookieValue);
10 | }
11 |
12 | return cookies;
13 | }
14 |
--------------------------------------------------------------------------------
/apps/web/src/pages/api/trpc/[trpc].ts:
--------------------------------------------------------------------------------
1 | import { createNextApiHandler } from "@trpc/server/adapters/next";
2 |
3 | import { env } from "../../../env/server.mjs";
4 | import { createContext } from "../../../server/trpc/context";
5 | import { appRouter } from "../../../server/trpc/router/_app";
6 |
7 | // export API handler
8 | export default createNextApiHandler({
9 | router: appRouter,
10 | batching: { enabled: false },
11 | createContext,
12 | onError:
13 | env.NODE_ENV === "development"
14 | ? ({ path, error }) => {
15 | console.error(`❌ tRPC failed on ${path}: ${error}`);
16 | }
17 | : undefined,
18 | });
19 |
--------------------------------------------------------------------------------
/apps/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "version": "0.2.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@types/node": "^18.0.0",
12 | "@types/react": "18.0.26",
13 | "@types/react-dom": "^18.0.5",
14 | "next": "14.1.1",
15 | "next-plausible": "^3.11.3",
16 | "nextra": "2.13.2",
17 | "nextra-theme-docs": "2.13.2",
18 | "react": "18.2.0",
19 | "react-dom": "18.2.0",
20 | "typescript": "5.5.4"
21 | },
22 | "devDependencies": {
23 | "tsconfig": "workspace:^0.0.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/apps/web/src/api/routes/health.test.ts:
--------------------------------------------------------------------------------
1 | import { testClient } from "hono/testing";
2 | import { makeHealthRoute } from "./health";
3 |
4 | vi.mock("server/db/client", () => ({
5 | prisma: {
6 | verificationToken: {
7 | count: vi.fn(async () => 1),
8 | },
9 | },
10 | }));
11 |
12 | vi.mock("server/db/redis", () => ({
13 | redis: {
14 | get: vi.fn(async () => "test"),
15 | },
16 | }));
17 |
18 | it("should work", async () => {
19 | const app = makeHealthRoute();
20 |
21 | const res = await testClient(app).index.$get();
22 |
23 | expect(res.status).toEqual(200);
24 | expect(await res.json()).toEqual({ status: "ok" });
25 | });
26 |
--------------------------------------------------------------------------------
/apps/web/src/lib/shiki/languages/jinja-html.tmLanguage.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "jinja-html",
3 | "scopeName": "text.html.jinja",
4 | "comment": "Jinja HTML Templates",
5 | "firstLineMatch": "^{% extends [\"'][^\"']+[\"'] %}",
6 | "foldingStartMarker": "(<(?i:(head|table|tr|div|style|script|ul|ol|form|dl))\\b.*?>|{%\\s*(block|filter|for|if|macro|raw))",
7 | "foldingStopMarker": "((?i:(head|table|tr|div|style|script|ul|ol|form|dl))\\b.*?>|{%\\s*(endblock|endfilter|endfor|endif|endmacro|endraw)\\s*%})",
8 | "patterns": [
9 | {
10 | "include": "source.jinja"
11 | },
12 | {
13 | "include": "text.html.basic"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/apps/web/sentry.server.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the server.
2 | // The config you add here will be used whenever the server handles a request.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from "@sentry/nextjs";
6 |
7 | Sentry.init({
8 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
9 |
10 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
11 | tracesSampleRate: 1,
12 |
13 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
14 | debug: false,
15 | });
16 |
--------------------------------------------------------------------------------
/apps/web/src/server/common/get-server-auth-session.ts:
--------------------------------------------------------------------------------
1 | import type { GetServerSidePropsContext } from "next";
2 | import { unstable_getServerSession } from "next-auth";
3 |
4 | import { authOptions } from "../../pages/api/auth/[...nextauth]";
5 |
6 | /**
7 | * Wrapper for unstable_getServerSession https://next-auth.js.org/configuration/nextjs
8 | * See example usage in trpc createContext or the restricted API route
9 | */
10 | export const getServerAuthSession = async (ctx: {
11 | req: GetServerSidePropsContext["req"];
12 | res: GetServerSidePropsContext["res"];
13 | }) => {
14 | return await unstable_getServerSession(ctx.req, ctx.res, authOptions);
15 | };
16 |
--------------------------------------------------------------------------------
/packages/svelte/src/tests/setupTest.ts:
--------------------------------------------------------------------------------
1 | import fetch from "node-fetch";
2 | import { afterEach } from "vitest";
3 | /// @ts-ignore it doesn't have types
4 | import { server } from "./mocks/server";
5 |
6 | /// @ts-ignore
7 | global.fetch = fetch;
8 |
9 | // Establish API mocking before all tests.
10 | beforeAll(() => server.listen());
11 |
12 | // Clean up after the tests are finished.
13 | afterAll(() => server.close());
14 |
15 | // runs a cleanup after each test case (e.g. clearing jsdom)
16 | afterEach(() => {
17 | // Reset any request handlers that we may add during the tests,
18 | // so they don't affect other tests.
19 | server.resetHandlers();
20 | });
21 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20221208152910_add_project_user/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `ProjectUser` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
5 | `updatedAt` DATETIME(3) NOT NULL,
6 | `userId` VARCHAR(191) NOT NULL,
7 | `projectId` VARCHAR(191) NOT NULL,
8 | `role` INTEGER NOT NULL DEFAULT 0,
9 |
10 | INDEX `ProjectUser_userId_idx`(`userId`),
11 | INDEX `ProjectUser_projectId_idx`(`projectId`),
12 | UNIQUE INDEX `ProjectUser_userId_projectId_key`(`userId`, `projectId`),
13 | PRIMARY KEY (`id`)
14 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
15 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230628081242_add_api_key_table/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `APIKey` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `name` VARCHAR(191) NOT NULL,
5 | `hashedKey` VARCHAR(191) NOT NULL,
6 | `validDays` INTEGER NOT NULL,
7 | `isRevoked` BOOLEAN NOT NULL DEFAULT false,
8 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
9 | `updatedAt` DATETIME(3) NOT NULL,
10 | `userId` VARCHAR(191) NOT NULL,
11 |
12 | UNIQUE INDEX `APIKey_hashedKey_key`(`hashedKey`),
13 | INDEX `APIKey_userId_idx`(`userId`),
14 | PRIMARY KEY (`id`)
15 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
16 |
--------------------------------------------------------------------------------
/apps/web/sentry.client.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry on the client.
2 | // The config you add here will be used whenever a users loads a page in their browser.
3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
4 |
5 | import * as Sentry from "@sentry/nextjs";
6 |
7 | Sentry.init({
8 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
9 |
10 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
11 | tracesSampleRate: 1,
12 |
13 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
14 | debug: false,
15 | });
16 |
--------------------------------------------------------------------------------
/apps/web/src/server/common/getRequestOrigin.ts:
--------------------------------------------------------------------------------
1 | import type { IncomingMessage } from "node:http";
2 |
3 | /**
4 | * Reliably get a request's origin, even when deployed on serverless functions
5 | *
6 | * @example
7 | * ```ts
8 | * const route = (req, res) => {
9 | * const origin = getRequestOrigin(req)
10 | * }
11 | * ```
12 | */
13 | export const getRequestOrigin = (req: IncomingMessage): string =>
14 | // The x-forwarded-proto header is the only reliable way to determine HTTP vs HTTPS
15 | // with Vercel serverless functions and Netlify functions.
16 | `${req.headers["x-forwarded-proto"] === "https" ? "https" : "http"}://${
17 | req.headers.host
18 | }`;
19 |
--------------------------------------------------------------------------------
/packages/svelte/src/tests/pages/+test.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 | {#if $flag1}
12 | my super secret feature 1
13 | {/if}
14 |
15 | {#if $remoteConfig1 !== "FooBar"}
16 | my remoteConfig1 value
17 | {/if}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/packages/tsconfig/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Next.js",
4 | "extends": "./base.json",
5 | "compilerOptions": {
6 | "target": "es5",
7 | "lib": ["dom", "dom.iterable", "esnext"],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noEmit": true,
13 | "incremental": true,
14 | "esModuleInterop": true,
15 | "module": "esnext",
16 | "resolveJsonModule": true,
17 | "isolatedModules": true,
18 | "jsx": "preserve"
19 | },
20 | "include": ["src", "next-env.d.ts"],
21 | "exclude": ["node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/angular/src/lib/abby-logger.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable } from "@angular/core";
2 | import type { AbbyConfig } from "@tryabby/core";
3 | import { ABBY_CONFIG_TOKEN } from "./abby.module";
4 |
5 | @Injectable()
6 | export class AbbyLoggerService {
7 | readonly LOGGER_SCOPE = "ng.Abby";
8 |
9 | constructor(@Inject(ABBY_CONFIG_TOKEN) private config: AbbyConfig) {}
10 |
11 | log(...args: unknown[]): void {
12 | if (this.config.debug) {
13 | console.log(this.LOGGER_SCOPE, ...args);
14 | }
15 | }
16 |
17 | warn(...args: unknown[]): void {
18 | if (this.config.debug) {
19 | console.warn(this.LOGGER_SCOPE, ...args);
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230803094512_change_api_key_validity_logic/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `isRevoked` on the `ApiKey` table. All the data in the column will be lost.
5 | - You are about to drop the column `validDays` on the `ApiKey` table. All the data in the column will be lost.
6 | - Added the required column `validUntil` to the `ApiKey` table without a default value. This is not possible if the table is not empty.
7 |
8 | */
9 | -- AlterTable
10 | ALTER TABLE `ApiKey` DROP COLUMN `isRevoked`,
11 | DROP COLUMN `validDays`,
12 | ADD COLUMN `revokedAt` DATETIME(3) NULL,
13 | ADD COLUMN `validUntil` DATETIME(3) NOT NULL;
14 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20221208123701_add_project_invites/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `ProjectInvite` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
5 | `updatedAt` DATETIME(3) NOT NULL,
6 | `email` VARCHAR(191) NOT NULL,
7 | `projectId` VARCHAR(191) NOT NULL,
8 | `userId` VARCHAR(191) NULL,
9 | `usedAt` DATETIME(3) NULL,
10 |
11 | INDEX `ProjectInvite_projectId_idx`(`projectId`),
12 | INDEX `ProjectInvite_userId_idx`(`userId`),
13 | UNIQUE INDEX `ProjectInvite_projectId_email_key`(`projectId`, `email`),
14 | PRIMARY KEY (`id`)
15 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
16 |
--------------------------------------------------------------------------------
/apps/web/src/lib/graphs.ts:
--------------------------------------------------------------------------------
1 | import colors from "tailwindcss/colors";
2 |
3 | const forbiddenColors: Array = [
4 | "slate",
5 | "gray",
6 | "zinc",
7 | "neutral",
8 | "stone",
9 | "black",
10 | "blueGray",
11 | "coolGray",
12 | "warmGray",
13 | "current",
14 | "inherit",
15 | "transparent",
16 | ];
17 |
18 | const COLORS = Object.keys(colors)
19 | .flatMap((key) => {
20 | const currentColor = key as keyof typeof colors;
21 | if (forbiddenColors.includes(currentColor)) return [];
22 | return colors[currentColor][300];
23 | })
24 | .filter(Boolean);
25 |
26 | export function getColorByIndex(index: number) {
27 | return COLORS[index % COLORS.length];
28 | }
29 |
--------------------------------------------------------------------------------
/apps/web/src/server/services/RequestCache.ts:
--------------------------------------------------------------------------------
1 | import { redis } from "server/db/redis";
2 |
3 | export abstract class RequestCache {
4 | private static getCacheKey(projectId: string) {
5 | return `requests:${projectId}:`;
6 | }
7 |
8 | static async increment(projectId: string) {
9 | return redis.incr(RequestCache.getCacheKey(projectId));
10 | }
11 |
12 | static async get(projectId: string) {
13 | return Number(await redis.get(RequestCache.getCacheKey(projectId)));
14 | }
15 |
16 | static async reset(projectId: string | string[]) {
17 | return redis.del(
18 | new Array()
19 | .concat(projectId)
20 | .map((id) => RequestCache.getCacheKey(id))
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/apps/web/src/utils/validateFlags.ts:
--------------------------------------------------------------------------------
1 | import { FeatureFlagType } from "@prisma/client";
2 |
3 | export function validateFlag(flagType: FeatureFlagType, value: string) {
4 | switch (flagType) {
5 | case FeatureFlagType.BOOLEAN: {
6 | return value === "true" || value === "false";
7 | }
8 | case FeatureFlagType.NUMBER: {
9 | return !Number.isNaN(Number(value));
10 | }
11 | case FeatureFlagType.STRING: {
12 | return true;
13 | }
14 | case FeatureFlagType.JSON: {
15 | try {
16 | JSON.parse(value);
17 | return true;
18 | } catch (_e) {
19 | return false;
20 | }
21 | }
22 | }
23 | // exhaustivenes test for switch
24 | flagType satisfies never;
25 | }
26 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230601155320_add_multivariate_flags/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `isEnabled` on the `FlagValue` table. All the data in the column will be lost.
5 | - Added the required column `value` to the `FlagValue` table without a default value. This is not possible if the table is not empty.
6 |
7 | */
8 | -- AlterTable
9 | ALTER TABLE `FlagValue`
10 | ADD COLUMN `type` ENUM('BOOLEAN', 'STRING', 'NUMBER', 'JSON') NOT NULL DEFAULT 'BOOLEAN',
11 | ADD COLUMN `value` LONGTEXT NOT NULL;
12 |
13 | UPDATE `FlagValue` SET `value` = IF(`isEnabled`, 'true', 'false') WHERE `type` = 'BOOLEAN';
14 |
15 | -- DropColumn
16 | ALTER TABLE `FlagValue`
17 | DROP COLUMN `isEnabled`;
18 |
--------------------------------------------------------------------------------
/apps/web/src/pages/_error.jsx:
--------------------------------------------------------------------------------
1 | import * as Sentry from "@sentry/nextjs";
2 | // biome-ignore lint/suspicious/noShadowRestrictedNames:
3 | import Error from "next/error";
4 |
5 | const CustomErrorComponent = (props) => {
6 | return ;
7 | };
8 |
9 | CustomErrorComponent.getInitialProps = async (contextData) => {
10 | // In case this is running in a serverless function, await this in order to give Sentry
11 | // time to send the error before the lambda exits
12 | await Sentry.captureUnderscoreErrorException(contextData);
13 |
14 | // This will contain the status code of the response
15 | return Error.getInitialProps(contextData);
16 | };
17 |
18 | export default CustomErrorComponent;
19 |
--------------------------------------------------------------------------------
/apps/web/prisma/sql/getEventsByTestIdForAllTime.sql:
--------------------------------------------------------------------------------
1 | -- @param {String} $1:testId
2 | -- @param {Int} $2:type
3 | -- @param {String} $3:testIdAgain
4 | -- @param {Int} $4:typeAgain
5 |
6 | SELECT
7 | COUNT(DISTINCT CASE WHEN anonymousId IS NOT NULL THEN
8 | anonymousId
9 | ELSE
10 | CONCAT('NULL_', UUID())
11 | END) AS uniqueEventCount,
12 | COUNT(*) AS eventCount,
13 | TYPE,
14 | createdAt,
15 | selectedVariant
16 | FROM
17 | `Event`
18 | WHERE
19 | testId = ?
20 | AND TYPE = ?
21 | AND(DATE(createdAt) BETWEEN DATE((
22 | SELECT
23 | MIN(createdAt)
24 | FROM `Event`
25 | WHERE
26 | testId = ?
27 | AND TYPE = ?))
28 | AND DATE(NOW()))
29 | GROUP BY
30 | DATE(createdAt), selectedVariant
31 | ORDER BY createdAt ASC;
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230326163301_make_payment_interval_required/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `stripeCurrentPeriodEnd` on the `Project` table. All the data in the column will be lost.
5 |
6 | */
7 | -- AlterTable
8 | SET SQL_MODE='ALLOW_INVALID_DATES';
9 |
10 | ALTER TABLE `Project` ADD COLUMN `currentPeriodEnd` DATETIME(3) NOT NULL DEFAULT (CURRENT_TIMESTAMP(3) + INTERVAL 30 DAY);
11 |
12 | UPDATE `Project` SET `currentPeriodEnd` = `stripeCurrentPeriodEnd` WHERE `stripeCurrentPeriodEnd` IS NOT NULL;
13 | UPDATE `Project` SET `currentPeriodEnd` = (CURRENT_TIMESTAMP(3) + INTERVAL 30 DAY) WHERE `stripeCurrentPeriodEnd` IS NULL;
14 |
15 | ALTER TABLE `Project` DROP COLUMN `stripeCurrentPeriodEnd`;
16 |
--------------------------------------------------------------------------------
/apps/web/src/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import type { DefaultSession } from "next-auth";
2 |
3 | type UserSession = {
4 | id: string;
5 | image: string;
6 | projectIds: string[];
7 | lastOpenProjectId: string | undefined;
8 | hasCompletedOnboarding: boolean;
9 | };
10 |
11 | declare module "next-auth" {
12 | /**
13 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
14 | */
15 | interface Session {
16 | user?: UserSession & DefaultSession["user"];
17 | }
18 | }
19 |
20 | declare module "next-auth/jwt" {
21 | /** Returned by the `jwt` callback and `getToken`, when using JWT sessions */
22 | interface JWT {
23 | /** OpenID ID Token */
24 | user: {} & UserSession;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/apps/web/.env.example:
--------------------------------------------------------------------------------
1 | DATABASE_URL=mysql://root:password@localhost:3306/abby
2 |
3 | NODE_ENV=development
4 |
5 | NEXTAUTH_SECRET=secret
6 | NEXTAUTH_URL=http://localhost:3000
7 | EMAIL_SERVER=smtp://0.0.0.0:1025
8 | GITHUB_ID=null
9 | GITHUB_SECRET=null
10 |
11 | GOOGLE_CLIENT_ID=
12 | GOOGLE_CLIENT_SECRET=
13 |
14 | REDIS_URL=redis://localhost:6379
15 | ABBY_FROM_EMAIL=no-reply@tryabby.dev
16 | GITHUB_OAUTH_TOKEN=
17 |
18 | STRIPE_WEBHOOK_SECRET=null
19 | STRIPE_SECRET_KEY=null
20 | LOGSNAG_API_KEY=null
21 | HASHING_SECRET=secret
22 |
23 | NEXT_PUBLIC_PLAUSIBLE_DOMAIN=tryabby.com
24 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=null
25 | NEXT_PUBLIC_STRIPE_STARTER_PLAN_PRICE_ID=null
26 | NEXT_PUBLIC_STRIPE_PRO_PLAN_PRICE_ID=null
27 | NEXT_PUBLIC_ABBY_PROJECT_ID=null
28 |
--------------------------------------------------------------------------------
/packages/devtools/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 | "compilerOptions": {
4 | "target": "ESNext",
5 | "useDefineForClassFields": true,
6 | "module": "ESNext",
7 | "resolveJsonModule": true,
8 | /**
9 | * Typecheck JS in `.svelte` and `.js` files by default.
10 | * Disable checkJs if you'd like to use dynamic types in JS.
11 | * Note that setting allowJs false does not prevent the use
12 | * of JS in `.svelte` files.
13 | */
14 | "allowJs": true,
15 | "checkJs": true,
16 | "isolatedModules": true,
17 | "rootDir": "src"
18 | },
19 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
20 | "references": [{ "path": "./tsconfig.node.json" }]
21 | }
22 |
--------------------------------------------------------------------------------
/packages/svelte/src/lib/AbbyProvider.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/packages/angular/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Miscellaneous
32 | /.angular/cache
33 | .sass-cache/
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | testem.log
38 | /typings
39 | TESTS.xml
40 |
41 | # System files
42 | .DS_Store
43 | Thumbs.db
44 |
--------------------------------------------------------------------------------
/packages/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-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
12 | // If your environment is not supported or you settled on a specific environment, switch out the adapter.
13 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
14 | adapter: adapter(),
15 | },
16 | };
17 |
18 | export default config;
19 |
--------------------------------------------------------------------------------
/packages/svelte/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/svelte/tsconfig.json",
3 | "compilerOptions": {
4 | "target": "ESNext",
5 | "useDefineForClassFields": true,
6 | "module": "ESNext",
7 | "resolveJsonModule": true,
8 | /**
9 | * Typecheck JS in `.svelte` and `.js` files by default.
10 | * Disable checkJs if you'd like to use dynamic types in JS.
11 | * Note that setting allowJs false does not prevent the use
12 | * of JS in `.svelte` files.
13 | */
14 | "allowJs": true,
15 | "checkJs": true,
16 | "isolatedModules": true
17 | },
18 | "include": [
19 | "src/**/*.d.ts",
20 | "src/**/*.ts",
21 | "src/**/*.js",
22 | "src/**/*.svelte"
23 | ],
24 | "references": [
25 | {
26 | "path": "./tsconfig.node.json"
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/apps/web/src/components/Test/Serves.tsx:
--------------------------------------------------------------------------------
1 | import { DonutChart } from "components/charts/Donut";
2 | import type { ProjectClientEvents } from "pages/projects/[projectId]";
3 | import type { ClientOption } from "server/trpc/router/project";
4 |
5 | const Serves = ({
6 | pingEvents,
7 | options,
8 | }: {
9 | pingEvents: ProjectClientEvents;
10 | options: ClientOption[];
11 | }) => {
12 | const labels = options.map((option) => option.identifier);
13 |
14 | return (
15 |
16 | acc + e._count._all, 0)}
18 | variants={labels}
19 | events={pingEvents}
20 | totalText="Visits"
21 | />
22 |
23 | );
24 | };
25 |
26 | export { Serves };
27 |
--------------------------------------------------------------------------------
/apps/web/src/components/Test/Metrics.tsx:
--------------------------------------------------------------------------------
1 | import { DonutChart } from "components/charts/Donut";
2 | import type { ProjectClientEvents } from "pages/projects/[projectId]";
3 | import type { ClientOption } from "server/trpc/router/project";
4 |
5 | const Metrics = ({
6 | actEvents,
7 | options,
8 | }: {
9 | actEvents: ProjectClientEvents;
10 | options: ClientOption[];
11 | }) => {
12 | const labels = options.map((option) => option.identifier);
13 |
14 | return (
15 |
16 | acc + e._count._all, 0)}
18 | variants={labels}
19 | events={actEvents}
20 | totalText="Conversions"
21 | />
22 |
23 | );
24 | };
25 |
26 | export { Metrics };
27 |
--------------------------------------------------------------------------------
/packages/angular/src/lib/get-variant.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, type PipeTransform } from "@angular/core";
2 | import type { ABConfig } from "@tryabby/core";
3 | import type { Observable } from "rxjs";
4 | import type { Key } from "ts-toolbelt/out/Any/Key";
5 | // biome-ignore lint/style/useImportType: angular needs this
6 | import { AbbyService } from "./abby.service";
7 |
8 | @Pipe({
9 | name: "getAbbyVariant",
10 | })
11 | export class GetAbbyVariantPipe<
12 | const TestName extends Key,
13 | const Tests extends Record,
14 | > implements PipeTransform
15 | {
16 | constructor(private abbyService: AbbyService) {}
17 |
18 | transform(testName: TestName): Observable {
19 | return this.abbyService.getVariant(testName);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/packages/devtools/src/components/ChevronIcon.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
20 |
21 |
34 |
--------------------------------------------------------------------------------
/packages/node/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type ABConfig,
3 | Abby,
4 | type AbbyConfig,
5 | type RemoteConfigValueString,
6 | } from "@tryabby/core";
7 | export { defineConfig } from "@tryabby/core";
8 | import { InMemoryStorageService } from "./utils/MemoryStorage";
9 |
10 | export function createAbby<
11 | const FlagName extends string,
12 | const TestName extends string,
13 | const Tests extends Record,
14 | const RemoteConfig extends Record,
15 | const RemoteConfigName extends Extract,
16 | >(
17 | config: AbbyConfig
18 | ) {
19 | const testStorage = new InMemoryStorageService();
20 | return new Abby(config, testStorage);
21 | }
22 |
--------------------------------------------------------------------------------
/apps/web/src/components/Toggle.tsx:
--------------------------------------------------------------------------------
1 | import { useId } from "react";
2 | import { Switch } from "./ui/switch";
3 | export function Toggle({
4 | label,
5 | onChange,
6 | isChecked,
7 | }: {
8 | isChecked?: boolean;
9 | label: string;
10 | onChange: (value: boolean) => void;
11 | }) {
12 | const id = useId();
13 | return (
14 |
15 | onChange(checked)}
19 | />
20 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/apps/web/prisma/generateCoupons.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs/promises";
2 | import path from "node:path";
3 | import { PrismaClient } from "@prisma/client";
4 |
5 | const prisma = new PrismaClient();
6 |
7 | const COUPON_CODE_AMOUNT = 200;
8 |
9 | async function main() {
10 | const fileName = path.join(__dirname, "./coupons.csv");
11 |
12 | const items = Array.from({ length: COUPON_CODE_AMOUNT }).map(() => ({
13 | stripePriceId: "STARTUP_LIFETIME",
14 | }));
15 |
16 | const codes = await prisma.$transaction(
17 | items.map((item) => prisma.couponCodes.create({ data: item }))
18 | );
19 |
20 | const csv = codes.map((code) => code.code).join("\n");
21 |
22 | await fs.writeFile(fileName, csv);
23 |
24 | console.log(`Wrote ${COUPON_CODE_AMOUNT} codes to ${fileName}`);
25 | }
26 |
27 | main();
28 |
--------------------------------------------------------------------------------
/apps/web/sentry.edge.config.ts:
--------------------------------------------------------------------------------
1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
2 | // The config you add here will be used whenever one of the edge features is loaded.
3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
5 |
6 | import * as Sentry from "@sentry/nextjs";
7 |
8 | Sentry.init({
9 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
10 |
11 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
12 | tracesSampleRate: 1,
13 |
14 | // Setting this option to true will print useful information to the console while you're setting up Sentry.
15 | debug: false,
16 | });
17 |
--------------------------------------------------------------------------------
/apps/web/src/pages/imprint.mdx:
--------------------------------------------------------------------------------
1 | import { MarketingLayout } from "components/MarketingLayout";
2 |
3 | # Imprint
4 |
5 | **dynabase Technologies GmbH**
6 | Von-Werth-Str. 37
7 | 50670 Köln
8 | Telefon: [+49-221-588-307-0](tel:+49-221-588-307-0)
9 | Telefax: [+49-221-588-307-0](fax:+49-221-588-307-00)
10 | E-Mail: [mail@dynabase.de](mailto:mail@dynabase.de)
11 |
12 | Geschäftsführer: Daniel Angileri, Norman Wenk
13 | Registergericht: Amtsgericht Köln
14 | Registernummer: HRB 91004 - USt-IdNr.: DE312712619
15 | Inhaltlich Verantwortliche: Daniel Angileri, Norman Wenk
16 |
17 | export default ({ children }) => (
18 |
19 | {children}
20 |
21 | );
22 |
23 | export const getStaticProps = () => {
24 | return {
25 | props: {},
26 | };
27 | };
28 |
--------------------------------------------------------------------------------
/apps/docs/pages/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "index": "Getting Started",
3 | "integrations": "Integrations",
4 | "reference": "Reference",
5 | "-- Guides --": {
6 | "type": "separator",
7 | "title": "Guides"
8 | },
9 | "nextjs": "Next.js",
10 | "devtools": "Devtools",
11 | "-- Concepts --": {
12 | "type": "separator",
13 | "title": "Concepts"
14 | },
15 | "feature-flags": "Feature Flags",
16 | "remote-config": "Remote Configuration",
17 | "environments": "Environments",
18 | "config": "Configuration as Code",
19 | "user-segments": "User Segments",
20 | "a-b-testing": "A/B Tests",
21 | "-- More --": {
22 | "type": "separator",
23 | "title": "More"
24 | },
25 | "abby_link": {
26 | "title": "Abby ↗",
27 | "href": "https://www.tryabby.com",
28 | "newWindow": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as LabelPrimitive from "@radix-ui/react-label";
2 | import { type VariantProps, cva } from "class-variance-authority";
3 | import * as React from "react";
4 |
5 | import { cn } from "lib/utils";
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | );
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ));
22 | Label.displayName = LabelPrimitive.Root.displayName;
23 |
24 | export { Label };
25 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20221205131914_bruh/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `Project` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
5 | `updatedAt` DATETIME(3) NOT NULL,
6 | `name` VARCHAR(191) NOT NULL,
7 |
8 | PRIMARY KEY (`id`)
9 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
10 |
11 | -- CreateTable
12 | CREATE TABLE `ProjectMemeber` (
13 | `id` VARCHAR(191) NOT NULL,
14 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
15 | `updatedAt` DATETIME(3) NOT NULL,
16 | `userId` VARCHAR(191) NOT NULL,
17 | `projectId` VARCHAR(191) NOT NULL,
18 |
19 | UNIQUE INDEX `ProjectMemeber_userId_projectId_key`(`userId`, `projectId`),
20 | PRIMARY KEY (`id`)
21 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
22 |
--------------------------------------------------------------------------------
/apps/web/src/lib/tracking.ts:
--------------------------------------------------------------------------------
1 | import { useOpenPanel } from "@openpanel/nextjs";
2 | import { usePlausible } from "next-plausible";
3 | import { useCallback } from "react";
4 | import type { EventOptionsTuple } from "server/common/tracking";
5 | import type { PlausibleEvents } from "types/plausible-events";
6 |
7 | export const useTracking = () => {
8 | const trackPlausible = usePlausible();
9 | const { track: trackOpenPanel } = useOpenPanel();
10 |
11 | return useCallback(
12 | (
13 | eventName: N,
14 | ...rest: PlausibleEvents[N] extends never
15 | ? []
16 | : EventOptionsTuple
17 | ) => {
18 | trackPlausible(eventName, ...rest);
19 | trackOpenPanel(eventName, ...rest);
20 | },
21 | [trackPlausible, trackOpenPanel]
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # database
12 | /prisma/db.sqlite
13 | /prisma/db.sqlite-journal
14 |
15 | # next.js
16 | /.next/
17 | /out/
18 | next-env.d.ts
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 |
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 |
33 | # local env files
34 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
35 | .env
36 | .env*.local
37 |
38 | # vercel
39 | .vercel
40 |
41 | # typescript
42 | *.tsbuildinfo
43 |
44 | public/sitemap*
45 | public/robots.txt
46 | prisma/coupons.csv
47 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230731133223_rename_apikey/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the `APIKey` table. If the table is not empty, all the data it contains will be lost.
5 |
6 | */
7 | -- DropTable
8 | DROP TABLE `APIKey`;
9 |
10 | -- CreateTable
11 | CREATE TABLE `ApiKey` (
12 | `id` VARCHAR(191) NOT NULL,
13 | `name` VARCHAR(191) NOT NULL,
14 | `hashedKey` VARCHAR(191) NOT NULL,
15 | `validDays` INTEGER NOT NULL,
16 | `isRevoked` BOOLEAN NOT NULL DEFAULT false,
17 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
18 | `updatedAt` DATETIME(3) NOT NULL,
19 | `userId` VARCHAR(191) NOT NULL,
20 |
21 | UNIQUE INDEX `ApiKey_hashedKey_key`(`hashedKey`),
22 | INDEX `ApiKey_userId_idx`(`userId`),
23 | PRIMARY KEY (`id`)
24 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
25 |
--------------------------------------------------------------------------------
/apps/web/src/pages/api/checkout/index.ts:
--------------------------------------------------------------------------------
1 | import { type Stripe, loadStripe } from "@stripe/stripe-js";
2 |
3 | let stripePromise: Promise | null = null;
4 |
5 | const getStripe = () => {
6 | if (!stripePromise) {
7 | if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) return;
8 | stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
9 | }
10 | return stripePromise as Promise;
11 | };
12 |
13 | export async function checkout({
14 | lineItems,
15 | }: {
16 | lineItems: { price: string; quantity: number }[];
17 | }) {
18 | const stripe = await getStripe();
19 | if (!stripe) return;
20 | await stripe.redirectToCheckout({
21 | mode: "payment",
22 | lineItems,
23 | successUrl: `${window.location.origin}?session_id={CHECKOUT_SESSION_ID}`,
24 | cancelUrl: window.location.origin,
25 | });
26 | }
27 |
--------------------------------------------------------------------------------
/packages/core/tests/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { rest } from "msw";
2 | import { ABBY_BASE_URL, type AbbyDataResponse } from "../../src/shared";
3 |
4 | const returnData: AbbyDataResponse = {
5 | tests: [
6 | {
7 | name: "test",
8 | weights: [1, 1, 1, 1],
9 | },
10 | {
11 | name: "test2",
12 | weights: [1, 0],
13 | },
14 | ],
15 | flags: [
16 | {
17 | name: "flag1",
18 | value: true,
19 | },
20 | ],
21 | remoteConfig: [{ name: "remoteConfig1", value: "asdf" }],
22 | };
23 |
24 | export const handlers = [
25 | rest.get(
26 | `${ABBY_BASE_URL}api/dashboard/:projectId/data`,
27 | (_req, res, ctx) => {
28 | return res(ctx.json(returnData));
29 | }
30 | ),
31 | rest.get(`${ABBY_BASE_URL}api/v2/data/:projectId`, (_req, res, ctx) => {
32 | return res(ctx.json(returnData));
33 | }),
34 | ];
35 |
--------------------------------------------------------------------------------
/apps/web/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/apps/web/src/styles/theme.json:
--------------------------------------------------------------------------------
1 | {
2 | "--background": "231.43 42.17% 16.27%",
3 | "--foreground": "222.2 47.4% 11.2%",
4 | "--muted": "210 40% 96.1%",
5 | "--muted-foreground": "215.4 16.3% 46.9%",
6 | "--primary": "222.2 47.4% 11.2%",
7 | "--primary-foreground": "210 40% 98%",
8 | "--secondary": "210 40% 96.1%",
9 | "--secondary-foreground": "222.2 47.4% 11.2%",
10 | "--popover": "0 0% 100%",
11 | "--popover-foreground": "222.2 47.4% 11.2%",
12 | "--input": "214.3 31.8% 91.4%",
13 | "--accent": "210 40% 96.1%",
14 | "--accent-foreground": "222.2 47.4% 11.2%",
15 | "--card": "0 0% 100%",
16 | "--card-foreground": "222.2 47.4% 11.2%",
17 | "--border": "214.3 31.8% 91.4%",
18 | "--destructive": "0 100% 50%",
19 | "--destructive-foreground": "210 40% 98%",
20 | "--ring": "215 20.2% 65.1%",
21 | "--radius": "0.5rem",
22 | "[object Object]": "--background"
23 | }
24 |
--------------------------------------------------------------------------------
/packages/core/src/shared/types.ts:
--------------------------------------------------------------------------------
1 | import type { ABConfig, FlagRuleSet, RemoteConfigValue } from "..";
2 |
3 | export enum AbbyEventType {
4 | PING = 0,
5 | ACT = 1,
6 | }
7 |
8 | export type AbbyDataResponse = {
9 | tests: Array<{
10 | name: string;
11 | weights: number[];
12 | }>;
13 | flags: Array<{
14 | name: string;
15 | value: boolean;
16 | ruleSet?: FlagRuleSet;
17 | }>;
18 | remoteConfig: Array<{
19 | name: string;
20 | value: RemoteConfigValue;
21 | ruleSet?: FlagRuleSet;
22 | }>;
23 | };
24 |
25 | export type LegacyAbbyDataResponse = {
26 | tests: Array<{
27 | name: string;
28 | weights: number[];
29 | }>;
30 | flags: Array<{ name: string; isEnabled: boolean }>;
31 | };
32 |
33 | export type ExtractVariants<
34 | TestName extends string,
35 | Tests extends Record,
36 | > = Tests[TestName]["variants"][number];
37 |
--------------------------------------------------------------------------------
/apps/web/src/components/Button.tsx:
--------------------------------------------------------------------------------
1 | import type React from "react";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | interface ButtonProps {
5 | as?: T;
6 | children?: React.ReactNode;
7 | }
8 |
9 | export function Button({
10 | children,
11 | className,
12 | as,
13 | ...props
14 | }: ButtonProps &
15 | Omit, keyof ButtonProps>) {
16 | const Component = as || "button";
17 | return (
18 |
25 | {children}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/packages/react/tests/setup.ts:
--------------------------------------------------------------------------------
1 | /// @ts-ignore it doesn't have types
2 | import matchers from "@testing-library/jest-dom/matchers";
3 | import { cleanup } from "@testing-library/react";
4 | import fetch from "node-fetch";
5 | import { afterEach, expect } from "vitest";
6 | import { server } from "./mocks/server";
7 |
8 | /// @ts-ignore
9 | global.fetch = fetch;
10 |
11 | // Establish API mocking before all tests.
12 | beforeAll(() => server.listen());
13 |
14 | // Clean up after the tests are finished.
15 | afterAll(() => server.close());
16 |
17 | // extends Vitest's expect method with methods from react-testing-library
18 | expect.extend(matchers);
19 |
20 | // runs a cleanup after each test case (e.g. clearing jsdom)
21 | afterEach(() => {
22 | // Reset any request handlers that we may add during the tests,
23 | // so they don't affect other tests.
24 | server.resetHandlers();
25 | cleanup();
26 | });
27 |
--------------------------------------------------------------------------------
/packages/next/tests/setup.ts:
--------------------------------------------------------------------------------
1 | /// @ts-ignore it doesn't have types
2 | import matchers from "@testing-library/jest-dom/matchers";
3 | import { cleanup } from "@testing-library/react";
4 | import fetch from "node-fetch";
5 | import { afterEach, expect } from "vitest";
6 | import { server } from "./mocks/server";
7 |
8 | /// @ts-ignore
9 | global.fetch = fetch;
10 |
11 | // Establish API mocking before all tests.
12 | beforeAll(() => server.listen());
13 | // Reset any request handlers that we may add during the tests,
14 | // so they don't affect other tests.
15 | afterEach(() => server.resetHandlers());
16 | // Clean up after the tests are finished.
17 | afterAll(() => server.close());
18 |
19 | // extends Vitest's expect method with methods from react-testing-library
20 | expect.extend(matchers);
21 |
22 | // runs a cleanup after each test case (e.g. clearing jsdom)
23 | afterEach(() => {
24 | cleanup();
25 | });
26 |
--------------------------------------------------------------------------------
/packages/remix/tests/setup.ts:
--------------------------------------------------------------------------------
1 | /// @ts-ignore it doesn't have types
2 | import matchers from "@testing-library/jest-dom/matchers";
3 | import { cleanup } from "@testing-library/react";
4 | import fetch from "node-fetch";
5 | import { afterEach, expect } from "vitest";
6 | import { server } from "./mocks/server";
7 |
8 | /// @ts-ignore
9 | global.fetch = fetch;
10 |
11 | // Establish API mocking before all tests.
12 | beforeAll(() => server.listen());
13 | // Reset any request handlers that we may add during the tests,
14 | // so they don't affect other tests.
15 | afterEach(() => server.resetHandlers());
16 | // Clean up after the tests are finished.
17 | afterAll(() => server.close());
18 |
19 | // extends Vitest's expect method with methods from react-testing-library
20 | expect.extend(matchers);
21 |
22 | // runs a cleanup after each test case (e.g. clearing jsdom)
23 | afterEach(() => {
24 | cleanup();
25 | });
26 |
--------------------------------------------------------------------------------
/apps/web/src/components/Input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/packages/svelte/src/tests/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { ABBY_BASE_URL, type AbbyDataResponse } from "@tryabby/core";
2 | import { rest } from "msw";
3 |
4 | const returnData: AbbyDataResponse = {
5 | tests: [
6 | {
7 | name: "test",
8 | weights: [1, 1, 1, 1],
9 | },
10 | {
11 | name: "test2",
12 | weights: [1, 0],
13 | },
14 | ],
15 | flags: [
16 | {
17 | name: "flag1",
18 | value: true,
19 | },
20 | ],
21 | remoteConfig: [
22 | {
23 | name: "remoteConfig1",
24 | value: "FooBar",
25 | },
26 | ],
27 | };
28 | export const handlers = [
29 | rest.get(
30 | `${ABBY_BASE_URL}api/dashboard/:projectId/data`,
31 | (_req, res, ctx) => {
32 | return res(ctx.json(returnData));
33 | }
34 | ),
35 | rest.get(`${ABBY_BASE_URL}api/v2/data/:projectId`, (_req, res, ctx) => {
36 | return res(ctx.json(returnData));
37 | }),
38 | ];
39 |
--------------------------------------------------------------------------------
/packages/cli/src/init.ts:
--------------------------------------------------------------------------------
1 | import fs from "node:fs/promises";
2 | import type { AbbyConfigFile, DynamicConfigKeys } from "@tryabby/core";
3 | import * as prettier from "prettier";
4 |
5 | export async function initAbbyConfig({ path }: { path: string }) {
6 | const dynamicConfig: Pick = {
7 | projectId: "",
8 | currentEnvironment: "",
9 | };
10 |
11 | const staticConfig: Omit = {
12 | environments: [],
13 | };
14 |
15 | const fileContent = `
16 | import { defineConfig } from '@tryabby/core';
17 |
18 |
19 | export default defineConfig(${JSON.stringify(dynamicConfig, null, 2)}, ${JSON.stringify(
20 | staticConfig,
21 | null,
22 | 2
23 | )});
24 | `;
25 |
26 | await fs.writeFile(
27 | path,
28 | await prettier.format(fileContent, { parser: "typescript" })
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20221205133208_/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `Test` (
3 | `projectId` VARCHAR(191) NULL,
4 | `id` VARCHAR(191) NOT NULL,
5 | `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
6 | `updatedAt` DATETIME(3) NOT NULL,
7 | `name` VARCHAR(191) NOT NULL,
8 |
9 | INDEX `Test_projectId_idx`(`projectId`),
10 | PRIMARY KEY (`id`)
11 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
12 |
13 | -- CreateTable
14 | CREATE TABLE `Option` (
15 | `id` VARCHAR(191) NOT NULL,
16 | `identifier` VARCHAR(191) NOT NULL,
17 | `chance` INTEGER NOT NULL,
18 | `testId` VARCHAR(191) NOT NULL,
19 |
20 | UNIQUE INDEX `Option_testId_identifier_key`(`testId`, `identifier`),
21 | PRIMARY KEY (`id`)
22 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
23 |
24 | -- CreateIndex
25 | CREATE INDEX `Project_userId_idx` ON `Project`(`userId`);
26 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2017",
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 | "allowSyntheticDefaultImports": true,
12 | "module": "esnext",
13 | "moduleResolution": "Bundler",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "jsx": "preserve",
17 | "incremental": true,
18 | "noUncheckedIndexedAccess": true,
19 | "baseUrl": "src",
20 | "paths": {
21 | "server/*": ["server/*"],
22 | "pages/*": ["pages/*"],
23 | "components/*": ["components/*"]
24 | },
25 | "types": ["vitest/globals"]
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "**/*.mjs"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20221210195541_add_stripe_info_to_projects/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[stripeCustomerId]` on the table `Project` will be added. If there are existing duplicate values, this will fail.
5 | - A unique constraint covering the columns `[stripeSubscriptionId]` on the table `Project` will be added. If there are existing duplicate values, this will fail.
6 |
7 | */
8 | -- AlterTable
9 | ALTER TABLE `Project` ADD COLUMN `stripeCurrentPeriodEnd` DATETIME(3) NULL,
10 | ADD COLUMN `stripeCustomerId` VARCHAR(191) NULL,
11 | ADD COLUMN `stripePriceId` VARCHAR(191) NULL,
12 | ADD COLUMN `stripeSubscriptionId` VARCHAR(191) NULL;
13 |
14 | -- CreateIndex
15 | CREATE UNIQUE INDEX `Project_stripeCustomerId_key` ON `Project`(`stripeCustomerId`);
16 |
17 | -- CreateIndex
18 | CREATE UNIQUE INDEX `Project_stripeSubscriptionId_key` ON `Project`(`stripeSubscriptionId`);
19 |
--------------------------------------------------------------------------------
/packages/node/tests/node.test.ts:
--------------------------------------------------------------------------------
1 | import { createAbby } from "../src/index";
2 |
3 | const testVariants = ["OldFooter", "NewFooter"] as const;
4 | const test2Variants = [
5 | "SimonsText",
6 | "MatthiasText",
7 | "TomsText",
8 | "TimsText",
9 | ] as const;
10 |
11 | it("should work properly", async () => {
12 | const abby = createAbby({
13 | environments: [],
14 | currentEnvironment: "development",
15 | projectId: "123",
16 | tests: {
17 | test: { variants: testVariants },
18 | test2: {
19 | variants: test2Variants,
20 | },
21 | },
22 | flags: ["flag1", "flag2"],
23 | });
24 |
25 | await abby.loadProjectData();
26 |
27 | // expect(abby.getFeatureFlag("flag1")).toBe(true);
28 | // expect(abby.getFeatureFlag("flag2")).toBe(false);
29 | expect(abby.getTestVariant("test")).to.be.oneOf(testVariants);
30 | expect(abby.getTestVariant("test2")).to.be.oneOf(test2Variants);
31 | });
32 |
--------------------------------------------------------------------------------
/apps/web/src/components/EventCounter.tsx:
--------------------------------------------------------------------------------
1 | import { AnimatePresence, motion } from "framer-motion";
2 | import { trpc } from "utils/trpc";
3 |
4 | export function EventCounter() {
5 | const { data } = trpc.events.getEventCount.useQuery();
6 |
7 | return (
8 |
9 |
10 |
17 | {new Intl.NumberFormat("en", { notation: "compact" }).format(
18 | data ?? 0
19 | )}
20 |
21 |
22 |
23 | Events processed and counting...
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/apps/web/src/components/SignupButton.tsx:
--------------------------------------------------------------------------------
1 | import { useAbby } from "lib/abby";
2 | import Link from "next/link";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | export function SignupButton({ className }: { className?: string }) {
6 | const { variant, onAct } = useAbby("SignupButton");
7 | return (
8 |
9 | onAct()}
12 | className={twMerge(
13 | "mt-12 rounded-xl bg-ab_accent-background px-6 py-2 text-xl font-semibold text-ab_accent-foreground no-underline transition-transform duration-150 ease-in-out hover:scale-110",
14 | className
15 | )}
16 | >
17 | {variant === "A" && "Test Now"}
18 | {variant === "B" && "Sign Up for Free"}
19 |
20 |
21 | Free forever. No Credit Card required
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/apps/web/src/env/server.mjs:
--------------------------------------------------------------------------------
1 | import { env as clientEnv, formatErrors } from "./client.mjs";
2 | // @ts-check
3 | /**
4 | * This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars.
5 | * It has to be a `.mjs`-file to be imported there.
6 | */
7 | import { serverSchema } from "./schema.mjs";
8 |
9 | const _serverEnv = serverSchema.safeParse(process.env);
10 |
11 | if (!_serverEnv.success) {
12 | console.error(
13 | "❌ Invalid environment variables:\n",
14 | ...formatErrors(_serverEnv.error.format())
15 | );
16 | throw new Error("Invalid environment variables");
17 | }
18 |
19 | for (const key of Object.keys(_serverEnv.data)) {
20 | if (key.startsWith("NEXT_PUBLIC_")) {
21 | console.warn("❌ You are exposing a server-side env-variable:", key);
22 |
23 | throw new Error("You are exposing a server-side env-variable");
24 | }
25 | }
26 |
27 | export const env = { ..._serverEnv.data, ...clientEnv };
28 |
--------------------------------------------------------------------------------
/apps/web/src/server/common/config-cache.ts:
--------------------------------------------------------------------------------
1 | import type { AbbyDataResponse } from "@tryabby/core";
2 | import createCache from "./memory-cache";
3 |
4 | const configCache = createCache({
5 | name: "configCache",
6 | // expire after 24 hours
7 | expireAfterMilliseconds: 1000 * 60 * 60 * 24,
8 | });
9 |
10 | type ConfigCacheKey = {
11 | environment: string;
12 | projectId: string;
13 | };
14 |
15 | export abstract class ConfigCache {
16 | static getConfig({ environment, projectId }: ConfigCacheKey) {
17 | return configCache.get(projectId + environment);
18 | }
19 |
20 | static setConfig({
21 | environment,
22 | projectId,
23 | value,
24 | }: ConfigCacheKey & {
25 | value: AbbyDataResponse;
26 | }) {
27 | configCache.set(projectId + environment, value);
28 | }
29 |
30 | static deleteConfig({ environment, projectId }: ConfigCacheKey) {
31 | configCache.delete(projectId + environment);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/apps/web/src/components/Feature.tsx:
--------------------------------------------------------------------------------
1 | import type { LucideIcon } from "lucide-react";
2 | import type { IconType } from "react-icons";
3 |
4 | type Props = {
5 | children: React.ReactNode;
6 | title: string;
7 | subtitle: string;
8 | icon: IconType | LucideIcon;
9 | };
10 |
11 | export function Feature({ children, icon: Icon, subtitle, title }: Props) {
12 | return (
13 |
14 |
15 |
16 |
17 |
{title}
18 |
19 | {subtitle}
20 |
21 |
{children}
22 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/packages/devtools/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | type Modifier = "command" | "ctrl" | "alt" | "shift" | "option" | "meta";
2 | type Char =
3 | | "a"
4 | | "b"
5 | | "c"
6 | | "d"
7 | | "e"
8 | | "f"
9 | | "g"
10 | | "h"
11 | | "i"
12 | | "j"
13 | | "k"
14 | | "l"
15 | | "m"
16 | | "n"
17 | | "o"
18 | | "p"
19 | | "q"
20 | | "r"
21 | | "s"
22 | | "t"
23 | | "u"
24 | | "v"
25 | | "w"
26 | | "x"
27 | | "y"
28 | | "z";
29 | type SpecialKey =
30 | | "."
31 | | ","
32 | | "/"
33 | | ";"
34 | | "'"
35 | | "["
36 | | "]"
37 | | "-"
38 | | "="
39 | | "enter"
40 | | "space"
41 | | "tab"
42 | | "backspace"
43 | | "delete"
44 | | "escape"
45 | | "up"
46 | | "down"
47 | | "left"
48 | | "right"
49 | | "pageup"
50 | | "pagedown"
51 | | "home"
52 | | "end";
53 |
54 | type Key = Char | SpecialKey;
55 |
56 | export type Shortcut =
57 | | `${Modifier}+${Key}`
58 | | Key
59 | | `${Key} ${Key}`
60 | | (string & []);
61 |
--------------------------------------------------------------------------------
/apps/web/src/lib/logsnag.ts:
--------------------------------------------------------------------------------
1 | import { env } from "env/server.mjs";
2 | import { LogSnag } from "logsnag";
3 | import type { PlanName } from "server/common/plans";
4 |
5 | const logsnag = new LogSnag({
6 | token: env.LOGSNAG_API_KEY,
7 | project: "abby",
8 | });
9 |
10 | export function trackSignup() {
11 | if (process.env.NODE_ENV !== "production") return;
12 | return logsnag.publish({
13 | channel: "user-register",
14 | event: "user-register",
15 | icon: "👋",
16 | notify: true,
17 | });
18 | }
19 |
20 | export function trackPlanOverage(
21 | projectId: string,
22 | plan: PlanName | undefined,
23 | is80Percent?: boolean
24 | ) {
25 | if (process.env.NODE_ENV !== "production") return;
26 | return logsnag.publish({
27 | channel: "plan-overrage",
28 | event: is80Percent ? "Limit reached" : "80% reached",
29 | icon: "⚠️",
30 | tags: {
31 | plan: plan ?? "HOBBY",
32 | projectId,
33 | },
34 | notify: true,
35 | });
36 | }
37 |
--------------------------------------------------------------------------------
/apps/web/prisma/migrations/20230303181212_update_history/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `environmentId` on the `FeatureFlagHistory` table. All the data in the column will be lost.
5 | - You are about to drop the column `flagId` on the `FeatureFlagHistory` table. All the data in the column will be lost.
6 | - Added the required column `flagValueId` to the `FeatureFlagHistory` table without a default value. This is not possible if the table is not empty.
7 |
8 | */
9 | -- DropIndex
10 | DROP INDEX `FeatureFlagHistory_flagId_environmentId_idx` ON `FeatureFlagHistory`;
11 |
12 | -- AlterTable
13 | ALTER TABLE `FeatureFlagHistory` DROP COLUMN `environmentId`,
14 | DROP COLUMN `flagId`,
15 | ADD COLUMN `flagValueId` VARCHAR(191) NOT NULL;
16 |
17 | -- AlterTable
18 | ALTER TABLE `FlagValue` ALTER COLUMN `id` DROP DEFAULT;
19 |
20 | -- CreateIndex
21 | CREATE INDEX `FeatureFlagHistory_flagValueId_idx` ON `FeatureFlagHistory`(`flagValueId`);
22 |
--------------------------------------------------------------------------------
/apps/web/src/server/services/ProjectService.ts:
--------------------------------------------------------------------------------
1 | import { env } from "node:process";
2 | import { ROLE } from "@prisma/client";
3 | import { BETA_PRICE_ID } from "lib/stripe";
4 | import { prisma } from "server/db/client";
5 |
6 | export abstract class ProjectService {
7 | static async hasProjectAccess(projectId: string, userId: string) {
8 | return (
9 | (await prisma.projectUser.count({
10 | where: {
11 | projectId: projectId,
12 | userId: userId,
13 | },
14 | })) > 0
15 | );
16 | }
17 | static async createProject(input: { projectName: string; userId: string }) {
18 | return prisma.project.create({
19 | data: {
20 | name: input.projectName,
21 | stripePriceId: env.NODE_ENV === "development" ? BETA_PRICE_ID : null,
22 | users: {
23 | create: {
24 | userId: input.userId,
25 | role: ROLE.ADMIN,
26 | },
27 | },
28 | },
29 | });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/next/tests/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { ABBY_BASE_URL, type AbbyDataResponse } from "@tryabby/core";
2 | import { rest } from "msw";
3 |
4 | const returnData: AbbyDataResponse = {
5 | tests: [
6 | {
7 | name: "test",
8 | weights: [1, 1, 1, 1],
9 | },
10 | {
11 | name: "test2",
12 | weights: [1, 0],
13 | },
14 | ],
15 | flags: [
16 | {
17 | name: "flag1",
18 | value: true,
19 | },
20 | {
21 | name: "flag2",
22 | value: false,
23 | },
24 | ],
25 | remoteConfig: [
26 | {
27 | name: "remoteConfig1",
28 | value: "FooBar",
29 | },
30 | ],
31 | };
32 | export const handlers = [
33 | rest.get(
34 | `${ABBY_BASE_URL}api/dashboard/:projectId/data`,
35 | (_req, res, ctx) => {
36 | return res(ctx.json(returnData));
37 | }
38 | ),
39 | rest.get(`${ABBY_BASE_URL}api/v2/data/:projectId`, (_req, res, ctx) => {
40 | return res(ctx.json(returnData));
41 | }),
42 | ];
43 |
--------------------------------------------------------------------------------
/packages/node/tests/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { ABBY_BASE_URL, type AbbyDataResponse } from "@tryabby/core";
2 | import { rest } from "msw";
3 |
4 | const returnData: AbbyDataResponse = {
5 | tests: [
6 | {
7 | name: "test",
8 | weights: [1, 1, 1, 1],
9 | },
10 | {
11 | name: "test2",
12 | weights: [1, 0],
13 | },
14 | ],
15 | flags: [
16 | {
17 | name: "flag1",
18 | value: true,
19 | },
20 | {
21 | name: "flag2",
22 | value: false,
23 | },
24 | ],
25 | remoteConfig: [
26 | {
27 | name: "remoteConfig1",
28 | value: "FooBar",
29 | },
30 | ],
31 | };
32 | export const handlers = [
33 | rest.get(
34 | `${ABBY_BASE_URL}api/dashboard/:projectId/data`,
35 | (_req, res, ctx) => {
36 | return res(ctx.json(returnData));
37 | }
38 | ),
39 | rest.get(`${ABBY_BASE_URL}api/v2/data/:projectId`, (_req, res, ctx) => {
40 | return res(ctx.json(returnData));
41 | }),
42 | ];
43 |
--------------------------------------------------------------------------------
/packages/react/tests/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { ABBY_BASE_URL, type AbbyDataResponse } from "@tryabby/core";
2 | import { rest } from "msw";
3 |
4 | const returnData: AbbyDataResponse = {
5 | tests: [
6 | {
7 | name: "test",
8 | weights: [1, 1, 1, 1],
9 | },
10 | {
11 | name: "test2",
12 | weights: [1, 0],
13 | },
14 | ],
15 | flags: [
16 | {
17 | name: "flag1",
18 | value: true,
19 | },
20 | {
21 | name: "flag2",
22 | value: false,
23 | },
24 | ],
25 | remoteConfig: [
26 | {
27 | name: "remoteConfig1",
28 | value: "FooBar",
29 | },
30 | ],
31 | };
32 | export const handlers = [
33 | rest.get(
34 | `${ABBY_BASE_URL}api/dashboard/:projectId/data`,
35 | (_req, res, ctx) => {
36 | return res(ctx.json(returnData));
37 | }
38 | ),
39 | rest.get(`${ABBY_BASE_URL}api/v2/data/:projectId`, (_req, res, ctx) => {
40 | return res(ctx.json(returnData));
41 | }),
42 | ];
43 |
--------------------------------------------------------------------------------
/packages/remix/tests/mocks/handlers.ts:
--------------------------------------------------------------------------------
1 | import { ABBY_BASE_URL, type AbbyDataResponse } from "@tryabby/core";
2 | import { rest } from "msw";
3 |
4 | const returnData: AbbyDataResponse = {
5 | tests: [
6 | {
7 | name: "test",
8 | weights: [1, 1, 1, 1],
9 | },
10 | {
11 | name: "test2",
12 | weights: [1, 0],
13 | },
14 | ],
15 | flags: [
16 | {
17 | name: "flag1",
18 | value: true,
19 | },
20 | {
21 | name: "flag2",
22 | value: false,
23 | },
24 | ],
25 | remoteConfig: [
26 | {
27 | name: "remoteConfig1",
28 | value: "FooBar",
29 | },
30 | ],
31 | };
32 | export const handlers = [
33 | rest.get(
34 | `${ABBY_BASE_URL}api/dashboard/:projectId/data`,
35 | (_req, res, ctx) => {
36 | return res(ctx.json(returnData));
37 | }
38 | ),
39 | rest.get(`${ABBY_BASE_URL}api/v2/data/:projectId`, (_req, res, ctx) => {
40 | return res(ctx.json(returnData));
41 | }),
42 | ];
43 |
--------------------------------------------------------------------------------
/packages/cli/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @tryabby/cli
2 |
3 | ## 3.0.1
4 |
5 | ### Patch Changes
6 |
7 | - fix build
8 |
9 | ## 3.0.0
10 |
11 | ### Major Changes
12 |
13 | - add rules and user segments
14 |
15 | ## 2.0.0
16 |
17 | ### Major Changes
18 |
19 | - use typescript 5
20 |
21 | ## 1.2.1
22 |
23 | ### Patch Changes
24 |
25 | - add version to version command
26 |
27 | ## 1.2.0
28 |
29 | ### Minor Changes
30 |
31 | - update ai command
32 |
33 | ## 1.1.1
34 |
35 | ### Patch Changes
36 |
37 | - d05cb9a: add feature flag removal command
38 |
39 | ## 1.1.0
40 |
41 | ### Minor Changes
42 |
43 | - introduce new add commands
44 |
45 | ## 1.0.1
46 |
47 | ### Patch Changes
48 |
49 | - add fallback values to remote config
50 |
51 | ## 1.0.0
52 |
53 | ### Major Changes
54 |
55 | - add remote config
56 |
57 | ## 0.3.0
58 |
59 | ### Minor Changes
60 |
61 | - add auto login flow
62 |
63 | ## 0.1.0
64 |
65 | ### Minor Changes
66 |
67 | - add defineConfig helper to ease the usage with the cli
68 |
--------------------------------------------------------------------------------
/apps/web/src/components/DashboardSection.tsx:
--------------------------------------------------------------------------------
1 | import { twMerge } from "tailwind-merge";
2 |
3 | export function DashboardSection({
4 | children,
5 | className,
6 | }: {
7 | children: React.ReactNode;
8 | className?: string;
9 | }) {
10 | return (
11 |
16 | );
17 | }
18 |
19 | export function DashboardSectionTitle({
20 | children,
21 | className,
22 | }: {
23 | children: React.ReactNode;
24 | className?: string;
25 | }) {
26 | return (
27 | {children}
28 | );
29 | }
30 |
31 | export function DashboardSectionSubtitle({
32 | children,
33 | className,
34 | }: {
35 | children: React.ReactNode;
36 | className?: string;
37 | }) {
38 | return (
39 |
40 | {children}
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/packages/angular/src/lib/get-remote-config.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, type PipeTransform } from "@angular/core";
2 | import type {
3 | RemoteConfigValueString,
4 | RemoteConfigValueStringToType,
5 | } from "@tryabby/core";
6 | // biome-ignore lint/style/useImportType: angular needs this
7 | import { Observable } from "rxjs";
8 | // biome-ignore lint/style/useImportType: angular needs this
9 | import { AbbyService } from "./abby.service";
10 |
11 | @Pipe({
12 | name: "getAbbyRemoteConfig",
13 | })
14 | export class GetRemoteConfigPipe<
15 | RemoteConfig extends Record,
16 | > implements PipeTransform
17 | {
18 | constructor(private abbyService: AbbyService) {}
19 |
20 | transform>(
21 | value: T
22 | ): Observable> {
23 | return this.abbyService.getRemoteConfig(value) as Observable<
24 | RemoteConfigValueStringToType
25 | >;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/svelte/src/tests/withAbby.test.ts:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom";
2 | import { render, waitFor } from "@testing-library/svelte";
3 | import { abby } from "./abby";
4 | import testPage from "./pages/+test.svelte";
5 |
6 | import { HttpService } from "@tryabby/core";
7 |
8 | describe("withabby working", () => {
9 | it("works properly", async () => {
10 | const data = await HttpService.getProjectData({
11 | projectId: "123",
12 | });
13 | if (!data) throw new Error("");
14 | abby.__abby__.init(data);
15 |
16 | const { getByText, queryByText } = render(testPage, {
17 | props: {
18 | data: {
19 | __abby__data: data,
20 | __abby_cookie: "",
21 | },
22 | },
23 | });
24 | const flag1 = await waitFor(() => getByText("my super secret feature 1"));
25 | expect(flag1).toBeInTheDocument();
26 | const remoteConfig1 = queryByText("my remoteConfig1 value");
27 | expect(remoteConfig1).not.toBeInTheDocument();
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/apps/docs/pages/index.mdx:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | Abby is an open-source, fully-typed Feature Flagging & Remote Config service made for developers.
4 |
5 | It aims to be the simpliest way to do Feature Flags and Remote Configuration variables on your website.
6 |
7 | We tried to make Abby as simple as possible and create the **best** user experience. Therefore we decided to focus on `React` and `Next.js` for now.
8 |
9 | ## Getting Started
10 |
11 | Before you're able to use Abby, you need to create a free account first.
12 | After you created your account you're able to use Abby. Make sure to read the [React Docs](/integrations/react) to get started.
13 |
14 | ## Philosophy
15 |
16 | Here are some of the things that we believe in:
17 |
18 | - **Simplicity** - We believe that A/B testing should be as simple as possible. That's why we created Abby.
19 | - **Security** - All data is hosted in Europe and the code is open source.
20 | - **Transparency** - We don't store any user related data, everything is saved anonymously.
21 |
--------------------------------------------------------------------------------
/apps/web/src/server/trpc/trpc.ts:
--------------------------------------------------------------------------------
1 | import { TRPCError, initTRPC } from "@trpc/server";
2 | import superjson from "superjson";
3 |
4 | import type { Context } from "./context";
5 |
6 | const t = initTRPC.context().create({
7 | transformer: superjson,
8 | errorFormatter({ shape }) {
9 | return shape;
10 | },
11 | });
12 |
13 | export const router = t.router;
14 |
15 | /**
16 | * Unprotected procedure
17 | **/
18 | export const publicProcedure = t.procedure;
19 |
20 | /**
21 | * Reusable middleware to ensure
22 | * users are logged in
23 | */
24 | const isAuthed = t.middleware(({ ctx, next }) => {
25 | if (!ctx.session || !ctx.session.user) {
26 | throw new TRPCError({ code: "UNAUTHORIZED" });
27 | }
28 | return next({
29 | ctx: {
30 | // infers the `session` as non-nullable
31 | session: { ...ctx.session, user: ctx.session.user },
32 | },
33 | });
34 | });
35 |
36 | /**
37 | * Protected procedure
38 | **/
39 | export const protectedProcedure = t.procedure.use(isAuthed);
40 |
--------------------------------------------------------------------------------
/apps/web/src/types/plausible-events.ts:
--------------------------------------------------------------------------------
1 | import type { FeatureFlagType } from "@prisma/client";
2 | import type { PlanName } from "server/common/plans";
3 |
4 | export type Plan = PlanName | "HOBBY";
5 |
6 | export type PlausibleEvents = {
7 | "Sign Up Clicked": never;
8 | "Plan Selected": {
9 | Plan: Plan;
10 | };
11 | "Plan Upgrade Clicked": {
12 | Plan: Plan;
13 | };
14 | "Project Created": never;
15 | "AB-Test Created": {
16 | "Amount Of Variants": number;
17 | };
18 | "Environment Created": never;
19 | "Feature Flag Created": {
20 | "Feature Flag Type": FeatureFlagType;
21 | };
22 | "Devtools Opened": never;
23 | "Devtools Interaction": {
24 | type: "Flag Updated" | "Variant Selected";
25 | };
26 | "API Project Data Retrieved": {
27 | projectId: string;
28 | };
29 | "Dashboard Help Clicked": never;
30 | "Dashboard Code Clicked": never;
31 | };
32 |
33 | export type ServerEvents = {
34 | flag_removal_pr_created: {
35 | files_changed: number;
36 | };
37 | };
38 |
--------------------------------------------------------------------------------
/apps/web/src/components/RadioGroup.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | RadioGroup,
3 | RadioGroupItem as RadixRadioGroupItem,
4 | } from "components/ui/radio-group";
5 |
6 | export type RadioGroupItem = {
7 | label: string;
8 | value: string;
9 | };
10 |
11 | type ArrayLike = Array | ReadonlyArray;
12 |
13 | type Props = {
14 | items: ArrayLike;
15 | value: RadioGroupItem["value"];
16 | onChange: (value: RadioGroupItem["value"]) => void;
17 | isLoading?: boolean;
18 | label?: string;
19 | };
20 |
21 | export function RadioGroupComponent({ items, onChange, value }: Props) {
22 | return (
23 |
28 | {items.map((item) => (
29 |
30 |
31 |
{item.label}
32 |
33 | ))}
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/apps/web/src/instrumentation.ts:
--------------------------------------------------------------------------------
1 | export async function register() {
2 | if (process.env.NEXT_RUNTIME === "nodejs") {
3 | console.log("Registering workers...");
4 |
5 | const workers = await Promise.all([
6 | import("server/queue/AfterDataRequest"),
7 | import("server/queue/event"),
8 | ]);
9 |
10 | if (process.env.NEXT_RUNTIME === "nodejs") {
11 | await import("../sentry.server.config");
12 | }
13 |
14 | // biome-ignore lint/suspicious/noExplicitAny: typings are off
15 | if (process.env.NEXT_RUNTIME === ("edge" as any)) {
16 | await import("../sentry.edge.config");
17 | }
18 |
19 | const gracefulShutdown = async (signal: string) => {
20 | console.log(`Received ${signal}, closing server...`);
21 | await Promise.all(workers.map((w) => w.default.close()));
22 | // Other asynchronous closings
23 | process.exit(0);
24 | };
25 |
26 | process.on("SIGINT", () => gracefulShutdown("SIGINT"));
27 |
28 | process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3.3"
2 | services:
3 | db:
4 | image: mariadb
5 | restart: always
6 | environment:
7 | MYSQL_DATABASE: "db"
8 | # So you don't have to use root, but you can if you like
9 | MYSQL_USER: "user"
10 | # You can use whatever password you like
11 | MYSQL_PASSWORD: "password"
12 | # Password for root access
13 | MYSQL_ROOT_PASSWORD: "password"
14 | ports:
15 | # : < MySQL Port running inside container>
16 | - "3306:3306"
17 | expose:
18 | # Opens port 3306 on the container
19 | - "3306"
20 | # Where our data will be persisted
21 | volumes:
22 | - my-db:/var/lib/mysql
23 | redis:
24 | image: redis
25 | restart: always
26 | ports:
27 | - "6379:6379"
28 | expose:
29 | - "6379"
30 | volumes:
31 | - my-redis:/data
32 | mailhog:
33 | image: mailhog/mailhog
34 | ports:
35 | - "1025:1025"
36 | - "8025:8025"
37 | # Names our volume
38 | volumes:
39 | my-db:
40 | my-redis:
41 |
--------------------------------------------------------------------------------
/apps/web/src/api/helpers.ts:
--------------------------------------------------------------------------------
1 | import { env } from "env/server.mjs";
2 | import type { MiddlewareHandler } from "hono";
3 | import { getCookie } from "hono/cookie";
4 | import type { Session } from "next-auth";
5 | import { decode } from "next-auth/jwt";
6 |
7 | export const authMiddleware: MiddlewareHandler<{
8 | Variables: {
9 | user: NonNullable;
10 | };
11 | }> = async (c, next) => {
12 | const tokenCookie = getCookie(
13 | c,
14 | env.NODE_ENV === "production"
15 | ? "__Secure-next-auth.session-token"
16 | : "next-auth.session-token"
17 | );
18 | if (!tokenCookie) {
19 | return c.json({ error: "Unauthorized" }, { status: 401 });
20 | }
21 | if (!env.NEXTAUTH_SECRET) {
22 | throw new Error("NEXTAUTH_SECRET is not set");
23 | }
24 | const session = await decode({
25 | secret: env.NEXTAUTH_SECRET,
26 | token: tokenCookie,
27 | });
28 | if (!session || !session.user) {
29 | return c.json({ error: "Unauthorized" }, { status: 401 });
30 | }
31 | c.set("user", session.user);
32 | return next();
33 | };
34 |
--------------------------------------------------------------------------------
/apps/web/abby.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "@tryabby/core";
2 | import * as validation from "@tryabby/core/validation";
3 |
4 | export default defineConfig(
5 | {
6 | // biome-ignore lint/style/noNonNullAssertion:>
7 | projectId: process.env.NEXT_PUBLIC_ABBY_PROJECT_ID!,
8 | currentEnvironment: process.env.VERCEL_ENV ?? process.env.NODE_ENV,
9 | apiUrl: process.env.NEXT_PUBLIC_ABBY_API_URL,
10 | __experimentalCdnUrl: process.env.NEXT_PUBLIC_ABBY_CDN_URL,
11 | debug: process.env.NEXT_PUBLIC_ABBY_DEBUG === "true",
12 | },
13 | {
14 | environments: ["development", "production"],
15 | tests: {
16 | SignupButton: {
17 | variants: ["A", "B"],
18 | },
19 | TipsAndTricks: {
20 | variants: ["Blog"],
21 | },
22 | },
23 | flags: ["AdvancedTestStats", "showFooter", "test"],
24 | remoteConfig: {
25 | abc: "JSON",
26 | },
27 | cookies: { disableByDefault: true, expiresInDays: 30 },
28 | user: {
29 | id: validation.string(),
30 | email: validation.string(),
31 | },
32 | }
33 | );
34 |
--------------------------------------------------------------------------------
/apps/web/src/components/FlagIcon.tsx:
--------------------------------------------------------------------------------
1 | import type { FeatureFlagType } from "@prisma/client";
2 | import { Baseline, CurlyBraces, Hash, ToggleLeft } from "lucide-react";
3 | import { match } from "ts-pattern";
4 | import { Tooltip, TooltipContent, TooltipTrigger } from "./Tooltip";
5 |
6 | type Props = {
7 | type: FeatureFlagType;
8 | className?: string;
9 | };
10 |
11 | export function FlagIcon({ type, ...iconProps }: Props) {
12 | return (
13 | <>
14 |
15 |
16 |
17 | This Flag has the type {type}
18 |
19 |
20 |
21 | {match(type)
22 | .with("BOOLEAN", () => )
23 | .with("NUMBER", () => )
24 | .with("STRING", () => )
25 | .with("JSON", () => )
26 | .exhaustive()}
27 |
28 |
29 | >
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/apps/docs/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/src/env/client.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { clientEnv, clientSchema } from "./schema.mjs";
3 |
4 | const _clientEnv = clientSchema.safeParse(clientEnv);
5 |
6 | export const formatErrors = (
7 | /** @type {import('zod').ZodFormattedError