├── .browserslistrc
├── .dockerignore
├── .env.example
├── .gitattributes
├── .github
└── workflows
│ ├── docker.yml
│ ├── linting.yml
│ ├── security.yml
│ └── tests.yml
├── .gitignore
├── .gitpod.Dockerfile
├── .gitpod.yml
├── .rubocop.yml
├── .ruby-version
├── CONTRIBUTING.md
├── Dockerfile.dev
├── Dockerfile.prod
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── Procfile.dev
├── Procfile.docker.dev
├── README.md
├── Rakefile
├── SELFHOSTING.md
├── app
├── assets
│ ├── builds
│ │ └── .keep
│ ├── config
│ │ └── manifest.js
│ ├── images
│ │ ├── .keep
│ │ ├── fugu_favicon.png
│ │ └── fugu_logo.png
│ ├── javascripts
│ │ └── libraries
│ │ │ └── .keep
│ └── stylesheets
│ │ ├── application.css
│ │ └── application.tailwind.css
├── channels
│ └── application_cable
│ │ ├── channel.rb
│ │ └── connection.rb
├── controllers
│ ├── api
│ │ ├── api_controller.rb
│ │ └── v1
│ │ │ └── events_controller.rb
│ ├── application_controller.rb
│ ├── concerns
│ │ ├── .keep
│ │ ├── api_keyable.rb
│ │ ├── dateable.rb
│ │ ├── event_nameable.rb
│ │ └── inactive_alertable.rb
│ ├── development
│ │ └── embed_mock_controller.rb
│ ├── events_controller.rb
│ ├── funnels_controller.rb
│ ├── projects_controller.rb
│ ├── stripe_controller.rb
│ └── users
│ │ ├── confirmations_controller.rb
│ │ ├── omniauth_callbacks_controller.rb
│ │ ├── passwords_controller.rb
│ │ ├── registrations_controller.rb
│ │ ├── sessions_controller.rb
│ │ ├── settings_controller.rb
│ │ └── unlocks_controller.rb
├── helpers
│ ├── application_helper.rb
│ └── event_helper.rb
├── javascript
│ ├── application.js
│ ├── controllers
│ │ ├── application.js
│ │ ├── chart_controller.js
│ │ ├── event_controller.js
│ │ ├── funnel_chart_controller.js
│ │ ├── index.js
│ │ └── selection_controller.js
│ └── helpers
│ │ └── chart_helpers.js
├── jobs
│ └── application_job.rb
├── mailers
│ └── application_mailer.rb
├── models
│ ├── api_key.rb
│ ├── application_record.rb
│ ├── concerns
│ │ ├── .keep
│ │ └── inactivable.rb
│ ├── event.rb
│ ├── funnel.rb
│ ├── funnel_step.rb
│ ├── project.rb
│ └── user.rb
├── services
│ └── fugu_service.rb
└── views
│ ├── development
│ └── embed_mock
│ │ └── index.html.erb
│ ├── events
│ ├── _chart.html.erb
│ ├── _selections.html.erb
│ ├── index.html.erb
│ └── show.html.erb
│ ├── funnels
│ ├── _chart.html.erb
│ ├── _selections.html.erb
│ ├── index.html.erb
│ ├── new.html.erb
│ └── show.html.erb
│ ├── layouts
│ ├── application.html.erb
│ ├── data_view.html.erb
│ ├── mailer.html.erb
│ └── mailer.text.erb
│ ├── partials
│ ├── _footer.html.erb
│ ├── _header.html.erb
│ ├── _mode_toggle.html.erb
│ ├── _narrow_box.erb
│ └── _project_header.html.erb
│ ├── projects
│ ├── edit.html.erb
│ ├── index.html.erb
│ ├── new.html.erb
│ └── settings.html.erb
│ └── users
│ ├── confirmations
│ └── new.html.erb
│ ├── mailer
│ ├── confirmation_instructions.html.erb
│ ├── email_changed.html.erb
│ ├── password_change.html.erb
│ ├── reset_password_instructions.html.erb
│ └── unlock_instructions.html.erb
│ ├── passwords
│ ├── edit.html.erb
│ └── new.html.erb
│ ├── registrations
│ ├── edit.html.erb
│ └── new.html.erb
│ ├── sessions
│ └── new.html.erb
│ ├── settings
│ ├── _subscription.html.erb
│ ├── _support.html.erb
│ └── show.html.erb
│ ├── shared
│ ├── _error_messages.html.erb
│ └── _links.html.erb
│ └── unlocks
│ └── new.html.erb
├── bin
├── bundle
├── dev
├── importmap
├── rails
├── rake
├── setup
├── spring
└── yarn
├── config.ru
├── config
├── application.rb
├── boot.rb
├── cable.yml
├── credentials.yml.enc
├── database.yml
├── environment.rb
├── environments
│ ├── development.rb
│ ├── production.rb
│ └── test.rb
├── importmap.rb
├── initializers
│ ├── application_controller_renderer.rb
│ ├── assets.rb
│ ├── backtrace_silencers.rb
│ ├── content_security_policy.rb
│ ├── cookies_serializer.rb
│ ├── devise.rb
│ ├── filter_parameter_logging.rb
│ ├── inflections.rb
│ ├── logstop.rb
│ ├── mime_types.rb
│ ├── permissions_policy.rb
│ ├── sentry.rb
│ ├── stripe.rb
│ └── wrap_parameters.rb
├── locales
│ ├── devise.en.yml
│ └── en.yml
├── puma.rb
├── routes.rb
├── spring.rb
├── storage.yml
└── tailwind.config.js
├── db
├── migrate
│ ├── 20210512145433_create_events.rb
│ ├── 20210512164800_create_projects.rb
│ ├── 20210617151514_add_project_reference_to_events.rb
│ ├── 20210617152330_create_api_keys.rb
│ ├── 20210617152542_add_not_null_constraint_to_events.rb
│ ├── 20210617154111_add_more_not_null_constraints.rb
│ ├── 20210617235020_add_dev_to_events.rb
│ ├── 20210618081647_add_unique_index_to_api_key.rb
│ ├── 20210729144755_move_staging_to_api_key.rb
│ ├── 20210729145810_move_reference_to_api_key.rb
│ ├── 20210730092821_devise_create_users.rb
│ ├── 20210730101807_add_user_reference_to_project.rb
│ ├── 20210922065433_change_event_properties_json_to_json_b.rb
│ ├── 20211016133119_add_stripe_and_active_to_user.rb
│ ├── 20211227170605_create_funnels.rb
│ ├── 20211227171015_create_funnel_steps.rb
│ ├── 20220216191234_add_unique_index.rb
│ ├── 20220314175006_add_hash_id_to_user.rb
│ ├── 20220315181147_add_public_bool_to_projects.rb
│ └── 20220320112745_user_hash_id_null_false.rb
├── schema.rb
└── seeds.rb
├── docker-compose.dev.yml
├── docker-compose.prod-remote.yml
├── docker-compose.prod.yml
├── docker-entrypoint.sh
├── lib
├── assets
│ └── .keep
└── tasks
│ ├── .keep
│ ├── auto_annotate_models.rake
│ └── fugu_migration_tasks.rake
├── log
└── .keep
├── public
├── 404.html
├── 422.html
├── 500.html
├── apple-touch-icon-precomposed.png
├── apple-touch-icon.png
├── favicon.ico
└── robots.txt
├── storage
└── .keep
├── test
├── application_system_test_case.rb
├── channels
│ └── application_cable
│ │ └── connection_test.rb
├── controllers
│ ├── .keep
│ ├── api
│ │ └── v1
│ │ │ └── events_controller_test.rb
│ ├── events_controller_test.rb
│ ├── funnels_controller_test.rb
│ ├── projects_controller_test.rb
│ ├── stripe_controller_test.rb
│ └── users
│ │ ├── registrations_controller_test.rb
│ │ └── settings_controller_test.rb
├── factories
│ ├── api_keys.rb
│ ├── events.rb
│ ├── funnel_steps.rb
│ ├── funnels.rb
│ ├── projects.rb
│ └── users.rb
├── helpers
│ └── .keep
├── integration
│ └── .keep
├── mailers
│ └── .keep
├── models
│ ├── .keep
│ ├── api_key_test.rb
│ ├── event_test.rb
│ ├── funnel_step_test.rb
│ ├── funnel_test.rb
│ ├── project_test.rb
│ └── user_test.rb
├── support
│ └── stripe_test_helper.rb
├── system
│ └── .keep
└── test_helper.rb
├── tmp
└── .keep
└── vendor
└── .keep
/.browserslistrc:
--------------------------------------------------------------------------------
1 | defaults
2 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | .github
3 | .gitignore
4 | .dockerignore
5 | .env
6 | LICENSE.md
7 | README.md
8 | SELFHOSTING.md
9 | CONTRIBUTING.md
10 | .gitpod.Dockerfile
11 | .gitpod.yml
12 | docker-compose.*
13 | node_modules
14 | public/assets
15 | storage
16 | public/packs
17 | public/packs-test
18 |
19 | # Ignore all logfiles and tempfiles.
20 | log
21 | tmp
22 | !log/.keep
23 | !tmp/.keep
24 |
25 | # Ignore pidfiles, but keep the directory.
26 | !tmp/pids
27 | !tmp/pids/.keep
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # REQUIRED if you're self-hosting
2 | ALLOW_REGISTRATION=true # setting this to false disallows new sign ups
3 | DATABASE_NAME=
4 | DATABASE_USER=
5 | DATABASE_PASSWORD=
6 | SECRET_KEY_BASE= # Generate key with `rails secret` or manually (alphanumeric string with 128 chars)
7 |
8 |
9 | # OPTIONAL if you're self-hosting
10 | INIT_USER_EMAIL= # set this if you run with docker-compose and want to create an initial user
11 | INIT_USER_PASSWORD= # set this if you run with docker-compose and want to create an initial user
12 | DATABASE_URL= # required if you're running Fugu without docker-compose in production
13 | SENTRY_DSN= # if you want to use Sentry to track errors
14 | RAILS_LOG_TO_STDOUT=true # if you want to log errors in server logs
15 | DATABASE_NAME_TEST= # only needed if you are running automated tests during development
16 | FORCE_SSL_PROD= # sets config.force_ssl in production.rb - defaults to true.
17 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # See https://git-scm.com/docs/gitattributes for more about git attribute files.
2 |
3 | # Mark the database schema as having been generated.
4 | db/schema.rb linguist-generated
5 |
6 | # Mark the yarn lockfile as having been generated.
7 | yarn.lock linguist-generated
8 |
9 | # Mark any vendored files as having been vendored.
10 | vendor/* linguist-vendored
11 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: Docker
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | docker:
9 | if: github.repository == 'shafy/fugu'
10 | runs-on: ubuntu-latest
11 | steps:
12 | -
13 | name: Set up QEMU
14 | uses: docker/setup-qemu-action@v1
15 | -
16 | name: Set up Docker Buildx
17 | uses: docker/setup-buildx-action@v1
18 | -
19 | name: Login to DockerHub
20 | uses: docker/login-action@v1
21 | with:
22 | username: ${{ secrets.DOCKERHUB_USERNAME }}
23 | password: ${{ secrets.DOCKERHUB_TOKEN }}
24 | -
25 | name: Set Versions
26 | uses: actions/github-script@v4
27 | id: set_version
28 | with:
29 | script: |
30 | const tag = context.ref.substring(10)
31 | const no_v = tag.replace('v', '')
32 | const dash_index = no_v.lastIndexOf('-')
33 | const no_dash = (dash_index > -1) ? no_v.substring(0, dash_index) : no_v
34 | core.setOutput('tag', tag)
35 | core.setOutput('no-v', no_v)
36 | core.setOutput('no-dash', no_dash)
37 | -
38 | name: Build and push
39 | uses: docker/build-push-action@v2
40 | with:
41 | push: true
42 | file: Dockerfile.prod
43 | tags: |
44 | ${{secrets.DOCKERHUB_USERNAME}}/fugu:latest
45 | ${{secrets.DOCKERHUB_USERNAME}}/fugu:${{steps.set_version.outputs.no-dash}}
--------------------------------------------------------------------------------
/.github/workflows/linting.yml:
--------------------------------------------------------------------------------
1 | name: Linting
2 |
3 | on:
4 | push:
5 | branches: [ $default-branch ]
6 | pull_request:
7 |
8 | jobs:
9 | linting:
10 | runs-on: ubuntu-latest
11 |
12 | env:
13 | RAILS_ENV: test
14 | NODE_ENV: test
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v2
19 |
20 | - name: Set up Ruby
21 | uses: ruby/setup-ruby@v1
22 | with:
23 | ruby-version: 3.0.0
24 | bundler-cache: true
25 |
26 | - name: Run linters
27 | run: |
28 | bundle exec rubocop
--------------------------------------------------------------------------------
/.github/workflows/security.yml:
--------------------------------------------------------------------------------
1 | name: Security
2 |
3 | on:
4 | push:
5 | branches: [ $default-branch ]
6 | pull_request:
7 |
8 | jobs:
9 | security:
10 | runs-on: ubuntu-latest
11 |
12 | env:
13 | RAILS_ENV: test
14 | NODE_ENV: test
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v2
19 |
20 | - name: Set up Ruby
21 | uses: ruby/setup-ruby@v1
22 | with:
23 | ruby-version: 3.0.0
24 | bundler-cache: true
25 |
26 | - name: Check code vulnerabilities
27 | run: |
28 | bundle exec brakeman
29 | - name: Check gem vulnerabilities
30 | run: |
31 | bundle exec bundle-audit --update
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches: [ $default-branch ]
6 | pull_request:
7 |
8 | jobs:
9 | tests:
10 | runs-on: ubuntu-latest
11 |
12 | env:
13 | DATABASE_USER: postgres
14 | DATABASE_PASSWORD: postgres
15 | DATABASE_HOST: localhost
16 | RAILS_ENV: test
17 |
18 | services:
19 | postgres:
20 | image: postgres:14
21 | env:
22 | POSTGRES_PASSWORD: postgres
23 | ports:
24 | - 5432:5432
25 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
26 |
27 | steps:
28 | - name: Checkout code
29 | uses: actions/checkout@v2
30 |
31 | - name: Set up Ruby
32 | uses: ruby/setup-ruby@v1
33 | with:
34 | ruby-version: 3.0.0
35 | bundler-cache: true
36 |
37 | - name: Set up PostgreSQL client
38 | run: |
39 | sudo apt-get -yqq install libpq-dev
40 |
41 | - name: Setup test database
42 | run: |
43 | bin/rails db:setup
44 |
45 | - name: Build Tailwind
46 | run: |
47 | bin/rails tailwindcss:build
48 |
49 | - name: Run specs
50 | run: |
51 | bundle exec rails test
52 | env:
53 | STRIPE_ENDPOINT_SECRET: ${{ secrets.STRIPE_ENDPOINT_SECRET }}
54 | ALLOW_REGISTRATION: true
55 | FUGU_CLOUD: true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.rbc
2 | capybara-*.html
3 | .rspec
4 | /db/*.sqlite3
5 | /db/*.sqlite3-journal
6 | /db/*.sqlite3-[0-9]*
7 | /public/system
8 | /coverage/
9 | /spec/tmp
10 | *.orig
11 | rerun.txt
12 | pickle-email-*.html
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | /tmp/*
17 | !/log/.keep
18 | !/tmp/.keep
19 |
20 | config/initializers/secret_token.rb
21 | config/master.key
22 |
23 | .env
24 |
25 | ## Environment normalization:
26 | /.bundle
27 | /vendor/bundle
28 |
29 | # these should all be checked in to normalize the environment:
30 | # Gemfile.lock, .ruby-version, .ruby-gemset
31 |
32 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
33 | .rvmrc
34 |
35 | # if using bower-rails ignore default bower_components path bower.json files
36 | /vendor/assets/bower_components
37 | *.bowerrc
38 | bower.json
39 |
40 | # Ignore pow environment settings
41 | .powenv
42 |
43 | # Ignore Byebug command history file.
44 | .byebug_history
45 |
46 | # Ignore node_modules
47 | node_modules/
48 |
49 | # Ignore precompiled javascript packs
50 | /public/packs
51 | /public/packs-test
52 | /public/assets
53 |
54 | # Ignore yarn files
55 | /yarn-error.log
56 | yarn-debug.log*
57 | .yarn-integrity
58 |
59 | # Ignore uploaded files in development
60 | /storage/*
61 | !/storage/.keep
62 | /public/uploads
63 |
64 | .DS_Store
65 | /app/assets/builds/*
66 | !/app/assets/builds/.keep
67 |
--------------------------------------------------------------------------------
/.gitpod.Dockerfile:
--------------------------------------------------------------------------------
1 | FROM gitpod/workspace-postgres
2 | USER gitpod
3 |
4 | # Install the Ruby version specified in '.ruby-version'
5 | COPY --chown=gitpod:gitpod .ruby-version /tmp
6 | RUN echo "rvm_gems_path=/home/gitpod/.rvm" > ~/.rvmrc
7 | RUN bash -lc "rvm reinstall ruby-$(cat /tmp/.ruby-version) && rvm use ruby-$(cat /tmp/.ruby-version) --default && gem install rails rubocop rubocop-performance rubocop-rails rubocop-rspec"
8 | RUN echo "rvm_gems_path=/workspace/.rvm" > ~/.rvmrc
9 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | image:
2 | file: .gitpod.Dockerfile
3 |
4 | ports:
5 | # Rails server
6 | - port: 3000
7 | onOpen: open-browser
8 | # PostgreSQL server
9 | - port: 5432
10 | onOpen: ignore
11 |
12 | tasks:
13 | - init: >
14 | bundle install &&
15 | rails db:setup RAILS_ENV=development &&
16 | rails db:reset db:migrate RAILS_ENV=test
17 |
18 | github:
19 | prebuilds:
20 | branches: false
21 | pullRequests: true
22 |
23 | vscode:
24 | extensions:
25 | - rebornix.ruby
26 | - wingrunr21.vscode-ruby
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | require:
2 | - rubocop-performance
3 | - rubocop-rails
4 |
5 | AllCops:
6 | Exclude:
7 | - 'db/**/*'
8 | - 'script/**/*'
9 | - 'bin/**/*'
10 | - 'config/**/*'
11 | - 'vendor/**/*'
12 | NewCops: enable
13 |
14 | Layout/LineLength:
15 | Max: 100
16 |
17 | Metrics/ClassLength:
18 | Max: 150
19 |
20 | Metrics/MethodLength:
21 | Max: 20
22 |
23 | Metrics/AbcSize:
24 | Max: 26
25 |
26 | Metrics/BlockLength:
27 | Exclude:
28 | - config/**/*
29 | - test/**/*
30 |
31 | Metrics/ModuleLength:
32 | Max: 120
33 |
34 | Metrics/CyclomaticComplexity:
35 | Max: 10
36 |
37 | Lint/AmbiguousBlockAssociation:
38 | Exclude:
39 | - spec/**/*
40 |
41 | Style/Documentation:
42 | Enabled: false
43 |
44 | Style/StringLiterals:
45 | EnforcedStyle: double_quotes
46 |
47 | Style/StringLiteralsInInterpolation:
48 | EnforcedStyle: double_quotes
49 |
50 | Style/FormatStringToken:
51 | Enabled: false
52 |
53 | Rails/I18nLocaleTexts:
54 | Enabled: false
55 |
--------------------------------------------------------------------------------
/.ruby-version:
--------------------------------------------------------------------------------
1 | ruby-3.0.0
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contribution Guide
2 |
3 | ## Contribution Rules
4 |
5 | This is an open-source project and we welcome contributions that improve the code, UX/UI and add new features. However, this doesn't mean that we will accept all contributions. The best way to contribute is to check the open issues or ask me (canolcer@hey.com) if there's something you can contribute to.
6 | If you want to contribute, fork this repo and create a pull request with your changes that we can review.
7 |
8 | If you have any questions, head over to our [GitHub Discussions](https://github.com/shafy/fugu/discussions) page.
9 |
10 |
11 | ## Development
12 |
13 | ### Setup
14 |
15 | There are a few ways you can get started with Fugu on your local machine.
16 |
17 | ### Gitpod
18 |
19 | We recommend developing with the excellent cloud-based development environment provided by [Gitpod](gitpod.io/). To start your ready-to-code development enviroment just add `gitpod.io/#` in front of the GitHub URL for any branch, or [click here](https://gitpod.io/#https://github.com/shafy/fugu) to get started with the main branch. Don't forget to add the environment variables from `.env.example` to your Gitpod account.
20 |
21 | ### Docker
22 |
23 | You can build the Docker image using the `Dockerfile.dev` configuration. If you don't have an existing PostgreSQL service on your machine, you can use our `docker-compose` setup to also start a PostgreSQL database. Just run `docker-compose -f docker-compose.dev.yml up`.
24 |
25 | Don't forget to create an `.env` file using `.env.example` as a template.
26 |
27 | If you're not using `docker-compose`, make sure to create the database with `docker run --env-file .env your_image rails db:create` before starting the Rails server.
28 |
29 | ### Old school
30 |
31 | Dependencies:
32 | - ruby ~3.0.0
33 | - rails ~7.0.0
34 | - postgres ^13.2
35 |
36 | Install the dependencies on your machine & set up an user in Postgres. Then:
37 | ```
38 | # Set the .env files and fill in all env vars
39 | cp .env.example .env
40 |
41 | # Create and migrate the database
42 | rails db:create db:migrate
43 | ```
44 |
45 | ## Starting the server
46 | To start the server, run `./bin/dev` or run `rails s` and `rails tailwindcss:watch` as separate processes (the first command does basically the same with `foreman`). This is necessary because we need the [Tailwind](https://github.com/rails/tailwindcss-rails) watch process during development.
47 |
48 |
49 | ## Fugu Cloud
50 |
51 | There are a few places in the codebase where you will encounter logic that is only needed for Fugu Cloud, the hosted solution by us. These parts of the code are not needed if you are self-hosting, and probably also not relevant for you as a contributor. For the most part this concerns code that relates to payment and subscriptions.
52 |
53 | Currently, we use the environment variable `FUGU_CLOUD` to determine if the instance is being self-hosted or not. Later on, we might improve this structure by factoring out the main code base to a Rails Engine.
54 |
--------------------------------------------------------------------------------
/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Stage 1: Build
3 | FROM ruby:3.0.0 as builder
4 |
5 | # Install base packages
6 | RUN apt-get update -qq && \
7 | apt-get install -y \
8 | build-essential \
9 | vim \
10 | nano \
11 | postgresql-client && \
12 | rm -rf /var/lib/apt/lists
13 |
14 | # Set env variables
15 | ENV BUNDLER_VERSION 2.2.3
16 | ENV BUNDLE_JOBS 8
17 | ENV BUNDLE_RETRY 5
18 | ENV BUNDLE_CACHE_ALL true
19 | ENV RAILS_ROOT /app
20 | ENV RAILS_ENV development
21 | ENV RACK_ENV development
22 |
23 | # Set working directory
24 | WORKDIR $RAILS_ROOT
25 |
26 | # Install gems
27 | COPY Gemfile Gemfile.lock ./
28 | RUN gem install bundler -v $BUNDLER_VERSION --no-document
29 | RUN bundle config --global frozen 1 && \
30 | bundle install && \
31 | rm -rf /usr/local/bundle/cache/*.gem && \
32 | find /usr/local/bundle/gems/ -name "*.c" -delete && \
33 | find /usr/local/bundle/gems/ -name "*.o" -delete
34 |
35 | # Copy app files
36 | COPY . .
37 |
38 | # Precompile assets
39 | # SECRET_KEY_BASE=`bin/rake secret` is added here as a workaround for
40 | # https://github.com/rails/rails/issues/32947
41 | RUN SECRET_KEY_BASE=`bin/rake secret`
42 |
43 | ###############################################################################
44 | # Stage 2: Run
45 | FROM ruby:3.0.0
46 |
47 | ENV RAILS_ENV development
48 | ENV RAILS_ROOT /app
49 |
50 | EXPOSE 3000
51 |
52 | RUN mkdir -p $RAILS_ROOT
53 | WORKDIR $RAILS_ROOT
54 |
55 | # Copy necessary data at runtime
56 | COPY --from=builder /usr/lib /usr/lib
57 |
58 | # Copy gems
59 | COPY --from=builder /usr/local/bundle /usr/local/bundle
60 |
61 | # Copy app files
62 | COPY --from=builder $RAILS_ROOT $RAILS_ROOT
63 |
64 | # Default command (overriden if used with this project's docker-compose)
65 | CMD ["rails", "server", "-b", "0.0.0.0"]
66 |
--------------------------------------------------------------------------------
/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Stage 1: Build
3 | FROM ruby:3.0.0 as builder
4 |
5 | # Install base packages
6 | RUN apt-get update -qq && \
7 | apt-get install -y \
8 | build-essential \
9 | vim \
10 | nano \
11 | postgresql-client && \
12 | rm -rf /var/lib/apt/lists
13 |
14 |
15 | # Set env variables
16 | ENV BUNDLER_VERSION 2.2.3
17 | ENV BUNDLE_JOBS 8
18 | ENV BUNDLE_RETRY 5
19 | ENV BUNDLE_WITHOUT development:test
20 | ENV BUNDLE_CACHE_ALL true
21 | ENV RAILS_ROOT /app
22 | ENV RAILS_ENV production
23 | ENV RACK_ENV production
24 |
25 | # Set working directory
26 | WORKDIR $RAILS_ROOT
27 |
28 | # Install gems
29 | COPY Gemfile Gemfile.lock ./
30 | RUN gem install bundler -v $BUNDLER_VERSION --no-document
31 | RUN bundle config --global frozen 1 && \
32 | bundle install && \
33 | rm -rf /usr/local/bundle/cache/*.gem && \
34 | find /usr/local/bundle/gems/ -name "*.c" -delete && \
35 | find /usr/local/bundle/gems/ -name "*.o" -delete
36 |
37 | # Copy app files
38 | COPY . .
39 |
40 | # Precompile assets
41 | # SECRET_KEY_BASE=`bin/rake secret` is added here as a workaround for
42 | # https://github.com/rails/rails/issues/32947
43 | RUN SECRET_KEY_BASE=`bin/rake secret` rails assets:precompile --trace && \
44 | rm -rf tmp/cache vendor/assets test
45 |
46 | ###############################################################################
47 | # Stage 2: Run
48 | FROM ruby:3.0.0
49 |
50 | ENV RAILS_ENV production
51 | ENV RAILS_SERVE_STATIC_FILES true
52 | ENV RAILS_ROOT /app
53 |
54 | EXPOSE 3000
55 |
56 | RUN mkdir -p $RAILS_ROOT
57 | WORKDIR $RAILS_ROOT
58 |
59 | # Copy necessary data at runtime
60 | COPY --from=builder /usr/lib /usr/lib
61 |
62 | # Copy gems
63 | COPY --from=builder /usr/local/bundle /usr/local/bundle
64 |
65 | # Copy app files
66 | COPY --from=builder $RAILS_ROOT $RAILS_ROOT
67 |
68 | # Default entrypoint (overriden if used with this project's docker-compose)
69 | ENTRYPOINT ["sh", "/app/docker-entrypoint.sh"]
70 |
71 | # Default command (overriden if used with this project's docker-compose)
72 | CMD ["rails", "server", "-b", "0.0.0.0"]
73 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
5 |
6 | ruby "3.0.0"
7 |
8 | # Bundle edge Rails instead: gem 'rails', github: 'rails/rails', branch: 'main'
9 | gem "rails", "~> 7.0.0"
10 |
11 | gem "pg"
12 |
13 | # Use Puma as the app server
14 | gem "puma", "~> 5.6.2"
15 | gem "sprockets-rails"
16 |
17 | # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
18 | gem "jbuilder", "~> 2.7"
19 | # Use Redis adapter to run Action Cable in production
20 | gem "redis", "~> 4.0"
21 | # Use Active Model has_secure_password
22 | # gem 'bcrypt', '~> 3.1.7'
23 |
24 | # Use Active Storage variant
25 | # gem 'image_processing', '~> 1.2'
26 |
27 | # Reduces boot times through caching; required in config/boot.rb
28 | gem "bootsnap", ">= 1.4.4", require: false
29 |
30 | gem "importmap-rails", "~> 1.0.0"
31 |
32 | gem "stimulus-rails", "~> 1.0.2"
33 | gem "turbo-rails", "~> 1.0.0"
34 |
35 | gem "devise", "~> 4.8.0"
36 |
37 | gem "tailwindcss-rails", "~> 2.0.0"
38 |
39 | gem "sentry-ruby", "~> 4.7"
40 |
41 | gem "sentry-rails", "~> 4.7"
42 |
43 | gem "stripe", "~> 5.39.0"
44 |
45 | gem "logstop"
46 |
47 | gem "faraday", "~> 1.8.0"
48 |
49 | group :development, :test do
50 | # Call 'byebug' anywhere in the code to stop execution and get a debugger console
51 | gem "byebug", platforms: %i[mri mingw x64_mingw]
52 | gem "dotenv-rails"
53 |
54 | # Linting with rubocop
55 | gem "rubocop"
56 | gem "rubocop-performance"
57 | gem "rubocop-rails"
58 |
59 | gem "factory_bot_rails", "~> 6.2.0"
60 |
61 | # security
62 | gem "brakeman", "~> 5.1", require: false
63 | gem "bundler-audit", "~> 0.9", require: false
64 | end
65 |
66 | group :development do
67 | # Access an interactive console on exception pages or by calling 'console' anywhere in the code.
68 | gem "web-console", ">= 4.1.0"
69 | # Display performance information such as SQL time and flame graphs
70 | # for each request in your browser. Can be configured to work on production as well see:
71 | # https://github.com/MiniProfiler/rack-mini-profiler/blob/master/README.md
72 | # gem 'rack-mini-profiler', '~> 2.0'
73 |
74 | gem "listen", "~> 3.3"
75 | # Spring speeds up development by keeping your application running in the background.
76 | # Read more: https://github.com/rails/spring
77 | gem "spring"
78 |
79 | gem "annotate"
80 |
81 | gem "foreman", "~> 0.87.2"
82 | end
83 |
84 | group :test do
85 | # Adds support for Capybara system testing and selenium driver
86 | gem "capybara", ">= 3.26"
87 | gem "selenium-webdriver"
88 | # Easy installation and use of web drivers to run system tests with browsers
89 | gem "webdrivers"
90 |
91 | gem "shoulda-context", "~> 2.0.0"
92 | gem "shoulda-matchers", "~> 5.1.0"
93 |
94 | gem "database_cleaner-active_record", "~> 2.0.1"
95 |
96 | gem "stripe-ruby-mock", "~> 3.1.0.rc3", require: "stripe_mock"
97 | end
98 |
99 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
100 | gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby]
101 |
--------------------------------------------------------------------------------
/Procfile.dev:
--------------------------------------------------------------------------------
1 | web: bin/rails server -p 3000
2 | css: rails tailwindcss:watch
--------------------------------------------------------------------------------
/Procfile.docker.dev:
--------------------------------------------------------------------------------
1 | web: bundle exec rails server -p 3000 -b '0.0.0.0'
2 | css: bundle exec rails tailwindcss:watch
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Fugu 🐡
2 |
3 | Fugu is a free, simple, privacy-friendly, open source and self-hostable product analytics. Check out the [Fugu website](https://fugu.lol) to learn more and play with an interactive demo.
4 |
5 |
6 |
7 | ## Getting started
8 |
9 | Fugu is a simple Ruby on Rails app and includes configuration for Docker and Docker Compose to simplify self-hosting. You can run it wherever you want, from Heroku over Hetzner to your own bare metal machine. Read the [Self-Hosting guide](/SELFHOSTING.md) for detailed instructions on how to get running with few steps.
10 |
11 | ## Docs
12 |
13 | Visit the [Fugu Docs](https://docs.fugu.lol).
14 |
15 |
16 | ## Contributing
17 |
18 | We welcome contributions to Fugu. These can be bug fixes, UI improvements, new features, code quality improvements etc. Check out the [contribution guide](/CONTRIBUTING.md) to get started.
19 |
20 | ## License
21 |
22 | This project uses the [GNU Affero General Public License v3.0](https://github.com/mapzy/mapzy/blob/main/LICENSE) license.
23 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # Add your own tasks in files placed in lib/tasks ending in .rake,
4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
5 |
6 | require_relative "config/application"
7 |
8 | Rails.application.load_tasks
9 |
--------------------------------------------------------------------------------
/app/assets/builds/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/app/assets/builds/.keep
--------------------------------------------------------------------------------
/app/assets/config/manifest.js:
--------------------------------------------------------------------------------
1 | //= link_tree ../images
2 | //= link_directory ../stylesheets .css
3 | //= link_tree ../../javascript .js
4 | //= link_tree ../builds
5 |
--------------------------------------------------------------------------------
/app/assets/images/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/app/assets/images/.keep
--------------------------------------------------------------------------------
/app/assets/images/fugu_favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/app/assets/images/fugu_favicon.png
--------------------------------------------------------------------------------
/app/assets/images/fugu_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/app/assets/images/fugu_logo.png
--------------------------------------------------------------------------------
/app/assets/javascripts/libraries/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/app/assets/javascripts/libraries/.keep
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /* ...
2 | *= require_self
3 | *= require_tree .
4 | */
5 |
6 |
--------------------------------------------------------------------------------
/app/assets/stylesheets/application.tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 | .font-system {
7 | font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Oxygen-Sans', 'Ubuntu', 'Cantarell', 'Helvetica Neue', sans-serif;
8 | }
9 |
10 | select {
11 | @apply rounded border-2 border-gray-600 bg-amber-50;
12 | }
13 |
14 | select:focus {
15 | box-shadow: none;
16 | border-color: rgb(16, 185, 129);
17 | }
18 |
19 | input {
20 | @apply rounded border-2 border-gray-600;
21 | }
22 |
23 | a {
24 | text-decoration: underline;
25 | font-weight: 500;
26 | }
27 |
28 | a:hover {
29 | color: rgb(16, 185, 129);
30 | }
31 |
32 | h1 {
33 | font-size: 1.25rem;
34 | line-height: 1.75rem;
35 | margin-top: 1.25rem;
36 | margin-bottom: 1rem;
37 | font-weight: 500;
38 | }
39 |
40 | h2 {
41 | font-size: 1.125rem;
42 | line-height: 1.75rem;
43 | margin-top: 0.75rem;
44 | margin-bottom: 0.5rem;
45 | font-weight: 500;
46 | }
47 |
48 | code {
49 | @apply font-mono text-sm bg-gray-900 p-2 rounded text-white
50 | }
51 |
52 | .code--multiline {
53 | @apply p-4 rounded my-4 w-full bg-gray-900 text-white
54 | }
55 |
56 | .code--multiline > code {
57 | @apply bg-transparent p-0
58 | }
59 |
60 | .main-button {
61 | @apply py-2 px-4 bg-green-500 text-white font-semibold rounded-lg shadow-md
62 | focus:outline-none hover:bg-green-300 cursor-pointer w-full border-0;
63 | }
64 |
65 | .w-32rem {
66 | width: 32rem;
67 | }
68 |
69 | .flash-notice, .flash-info {
70 | background-color: rgb(147, 197, 253);
71 | border-color: rgb(96, 165, 250);
72 | }
73 |
74 | .flash-alert, .flash-not_active {
75 | background-color: rgb(252, 211, 77);
76 | border-color: rgb(251, 191, 36);
77 | }
78 |
79 | .flash-error {
80 | background-color: rgb(252, 165, 165);
81 | border-color: rgb(248, 113, 113);
82 | }
83 |
84 | .property-item-unselected--colorbox {
85 | @apply bg-gray-200;
86 | }
87 |
88 | .property-item-unselected--textbox {
89 | @apply text-gray-400;
90 | }
91 |
92 | .property-item-li {
93 | @apply flex flex-row gap-x-1 justify-center items-center cursor-pointer;
94 | }
--------------------------------------------------------------------------------
/app/channels/application_cable/channel.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Channel < ActionCable::Channel::Base
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/channels/application_cable/connection.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationCable
4 | class Connection < ActionCable::Connection::Base
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/controllers/api/api_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api
4 | class ApiController < ApplicationController
5 | skip_before_action :verify_authenticity_token
6 | around_action :handle_exceptions
7 |
8 | private
9 |
10 | def handle_exceptions
11 | begin
12 | yield
13 | rescue ActiveRecord::RecordNotFound => e
14 | @status = 404
15 | @error_type = e.class.to_s
16 | # @message = 'Record not found'
17 | rescue ArgumentError => e
18 | @status = 422
19 | @error_type = e.class.to_s
20 | rescue StandardError => e
21 | @status = 500
22 | @error_type = e.class.to_s
23 | end
24 |
25 | return if e.instance_of?(NilClass)
26 |
27 | render json: { error: { type: @error_type, message: e.message } },
28 | status: @status
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/app/controllers/api/v1/events_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Api
4 | module V1
5 | class EventsController < Api::ApiController
6 | before_action :validate_param_keys, only: %w[create]
7 |
8 | def create
9 | event = Event.create(
10 | name: params[:name],
11 | api_key: ApiKey.find_by!(key_value: params[:api_key]),
12 | properties: params[:properties]
13 | )
14 |
15 | raise ArgumentError, event.errors.full_messages.first unless event.errors.empty?
16 |
17 | render json: { success: true, event: event }, status: :ok
18 | end
19 |
20 | def validate_param_keys
21 | raise ArgumentError, "missing 'name' key" unless params.key?(:name)
22 |
23 | raise ArgumentError, "'name' can't be nil" if params[:name].nil?
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationController < ActionController::Base
4 | include InactiveAlertable
5 |
6 | private
7 |
8 | def show_test_alert
9 | return unless params[:test] == "true"
10 |
11 | flash.now[:info] =
12 | "Heads up: You are currently viewing test events. Test events are deleted after 14 days."
13 | end
14 |
15 | def set_project
16 | user = User.find_by(hash_id: params[:user_id])
17 | @project = Project.find_by(name: params[:project_slug]&.downcase, user: user)
18 | end
19 |
20 | def authorize_project_user
21 | return redirect_to user_projects_path(params[:user_id]) unless @project
22 |
23 | # don't show test data in public projects
24 | if !current_user && @project.public && params[:test] == "true"
25 | return redirect_to user_projects_path(params[:user_id])
26 | end
27 |
28 | # don't authorize is project if public
29 | return if @project.public
30 |
31 | return redirect_to user_projects_path(params[:user_id]) unless current_user == @project.user
32 | end
33 |
34 | def allow_iframe
35 | return unless params[:embed] == "true"
36 |
37 | response.headers.except! "X-Frame-Options"
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/app/controllers/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/app/controllers/concerns/.keep
--------------------------------------------------------------------------------
/app/controllers/concerns/api_keyable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApiKeyable
4 | extend ActiveSupport::Concern
5 |
6 | private
7 |
8 | def set_api_key
9 | @api_key = params[:test] == "true" ? @project.api_key_test : @project.api_key_live
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/controllers/concerns/dateable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Dateable
4 | extend ActiveSupport::Concern
5 |
6 | private
7 |
8 | def set_dates
9 | # defaults to 7days if no date selected or if url param not in the list
10 | @start_date = case params[:date] ? CGI.escapeHTML(params[:date]) : "7d"
11 | when "30d"
12 | 29.days.ago
13 | when "this_m"
14 | Time.zone.now.beginning_of_month
15 | when "6m"
16 | 6.months.ago
17 | when "3m"
18 | 3.months.ago
19 | when "12m"
20 | 12.months.ago
21 | when "1d"
22 | Time.zone.now
23 | else
24 | 6.days.ago
25 | end
26 | @end_date = Time.zone.now
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/app/controllers/concerns/event_nameable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module EventNameable
4 | extend ActiveSupport::Concern
5 |
6 | private
7 |
8 | def set_event_names
9 | @event_names = Event.distinct_events_names(@api_key)
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/controllers/concerns/inactive_alertable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # These alerts are only used in the Fugu Cloud version (not important if you're self-hosting)
4 | module InactiveAlertable
5 | extend ActiveSupport::Concern
6 |
7 | private
8 |
9 | def show_not_active_flash
10 | return if @project&.public
11 |
12 | return unless ENV["FUGU_CLOUD"] == "true"
13 |
14 | flash.now[:not_active] = user_canceled_flash if current_user.canceled?
15 | flash.now[:not_active] = user_inactive_flash if current_user.inactive?
16 | end
17 |
18 | # rubocop:disable Rails/OutputSafety
19 | def user_canceled_flash
20 | %(
21 | You have canceled your subscription and it will end soon.
22 | Make sure to renew it
23 | if you want to keep using Fugu.
24 | ).html_safe
25 | end
26 |
27 | def user_inactive_flash
28 | %(
29 | Hey there 👋 Make sure to subscribe in
30 | order to track events.
31 | You can use your test API key to give Fugu a spin without a subscription.
32 | ).html_safe
33 | end
34 | # rubocop:enable Rails/OutputSafety
35 | end
36 |
--------------------------------------------------------------------------------
/app/controllers/development/embed_mock_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Development
4 | class EmbedMockController < ApplicationController
5 | before_action :authenticate_user!
6 |
7 | def index
8 | @project = Project.where(user: current_user).first
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/app/controllers/funnels_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class FunnelsController < ApplicationController
4 | include ApiKeyable
5 | include Dateable
6 | include EventNameable
7 |
8 | before_action :set_project, only: %i[show index new create]
9 | before_action :authenticate_user!, unless: -> { @project.public }
10 | before_action :authorize_project_user, only: %i[index new]
11 | before_action :show_test_alert, only: %i[show index]
12 | before_action :show_test_funnel_creation_alert, only: %i[new]
13 | before_action :show_not_active_flash, only: %i[index]
14 | before_action :set_api_key, only: %i[index show new create]
15 | before_action :set_dates, only: %i[show]
16 | before_action :set_event_names, only: %i[new]
17 | before_action :set_funnel, only: %i[show]
18 | before_action :set_funnel_names, only: %i[index show]
19 | before_action :set_funnel_event_names, only: %i[show]
20 | before_action :build_funnel, only: %i[create]
21 |
22 | after_action :track_event, only: %i[index]
23 | after_action :allow_iframe, only: %i[index show]
24 |
25 | def index
26 | return render layout: "data_view" unless @funnel_names&.first
27 |
28 | redirect_to user_project_funnel_path(
29 | params[:user_id],
30 | @project.name,
31 | @funnel_names.first.parameterize,
32 | helpers.evergreen_params
33 | )
34 | end
35 |
36 | def show
37 | unless @funnel
38 | return redirect_to user_project_funnels_path(
39 | params[:user_id],
40 | @project.name,
41 | helpers.evergreen_params
42 | )
43 | end
44 |
45 | @funnel_data = @funnel_event_names.map do |e|
46 | Event.where(name: e, api_key: @api_key, created_at: @start_date..@end_date).count
47 | end
48 | render layout: "data_view"
49 | end
50 |
51 | def new
52 | @funnel = Funnel.new
53 | 5.times { @funnel.funnel_steps.build }
54 |
55 | show_no_events_alert if @event_names.empty?
56 | end
57 |
58 | def create
59 | if @funnel.save
60 | redirect_to user_project_funnel_path(
61 | current_user.hash_id,
62 | @project.name,
63 | @funnel.name.parameterize,
64 | params: { test: params[:test] }
65 | )
66 | else
67 | flash[:error] = "We couldn't create your funnel: #{@funnel.errors.full_messages.first}"
68 | redirect_to new_user_project_funnel_path(
69 | current_user.hash_id,
70 | @project.name,
71 | params: { test: params[:test] }
72 | )
73 | end
74 | end
75 |
76 | private
77 |
78 | def funnel_params
79 | params.require(:funnel).permit(:user_id, :name, funnel_steps_attributes: %w[event_name])
80 | end
81 |
82 | def track_event
83 | FuguService.track("Viewed Funnels")
84 | end
85 |
86 | def build_funnel
87 | @funnel = Funnel.new(funnel_params.merge(api_key: @api_key))
88 | end
89 |
90 | def set_funnel
91 | @funnel = Funnel.find_by(name: params[:slug].tr("-", " ").titleize.strip, api_key: @api_key)
92 | end
93 |
94 | def set_funnel_names
95 | @funnel_names = Funnel.where(api_key: @api_key).pluck(:name)
96 | end
97 |
98 | def set_funnel_event_names
99 | @funnel_event_names = @funnel&.funnel_steps&.pluck(:event_name)
100 | end
101 |
102 | def show_test_funnel_creation_alert
103 | return unless params[:test] == "true"
104 |
105 | flash.now[:info] = "You are creating a funnel in test mode. " \
106 | "This means that you can only select events tracked in test mode. " \
107 | "Unlike test events, test funnels are not deleted after 14 days."
108 | end
109 |
110 | def show_no_events_alert
111 | flash.now[:alert] = "You haven't tracked any events in the current mode. " \
112 | "Make sure to track events before creating a funnel."
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/app/controllers/projects_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ProjectsController < ApplicationController
4 | before_action :authenticate_user!
5 |
6 | before_action :set_project, only: %i[settings]
7 | before_action :show_not_active_flash, only: %i[settings]
8 |
9 | def index
10 | @projects = Project.where(user: current_user)
11 | end
12 |
13 | def new
14 | @project = Project.new
15 | end
16 |
17 | def create
18 | @project = Project.new(name: project_params[:name], user: current_user)
19 | if @project.save
20 | @project.create_api_keys
21 | redirect_to user_project_events_path(current_user.hash_id, @project.name)
22 | else
23 | flash.now[:error] = "We couldn't create your project: #{@project.errors.full_messages.first}"
24 | render "projects/new", status: :unprocessable_entity
25 | end
26 | end
27 |
28 | def edit
29 | user = User.find_by(hash_id: params[:user_id])
30 | @project = Project.find_by(name: params[:slug]&.downcase, user: user)
31 | end
32 |
33 | def update
34 | @project = Project.find(project_params[:project_id])
35 |
36 | if @project.update(name: project_params[:name], public: project_params[:public])
37 | redirect_to user_project_settings_path(current_user.hash_id, @project.name.downcase)
38 | else
39 | flash.now[:error] = "We couldn't update your project: #{@project.errors.full_messages.first}"
40 | render "projects/edit", status: :unprocessable_entity
41 | end
42 | end
43 |
44 | def destroy
45 | @project = Project.find_by(name: params[:slug]&.downcase, user: current_user)
46 | if @project.destroy
47 | flash[:notice] = "Your project was successfully deleted"
48 | redirect_to user_projects_path(current_user)
49 | else
50 | flash[:error] = "We couldn't delete the project: #{@project.errors.full_messages.first}"
51 | redirect_to user_project_events_path(current_user.hash_id, @project.name)
52 | end
53 | end
54 |
55 | def settings; end
56 |
57 | private
58 |
59 | def project_params
60 | params.require(:project).permit(:project_id, :user_id, :name, :public)
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/app/controllers/stripe_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class StripeController < ApplicationController
4 | before_action :authenticate_user!, except: %i[webhooks]
5 | skip_before_action :verify_authenticity_token, only: %i[webhooks]
6 | before_action :verify_webhook, only: %i[webhooks]
7 |
8 | def checkout_session
9 | session = Stripe::Checkout::Session.create(checkout_session_args)
10 | redirect_to session.url, allow_other_host: true
11 | rescue StandardError => e
12 | Sentry.capture_exception(e)
13 | flash[:alert] = error_message
14 | redirect_to users_settings_url
15 | end
16 |
17 | def success_callback
18 | session = Stripe::Checkout::Session.retrieve(params[:session_id])
19 | customer = Stripe::Customer.retrieve(session.customer)
20 | current_user.update(status: "active", stripe_customer_id: customer.id)
21 |
22 | FuguService.track("New Subscription")
23 |
24 | flash[:notice] = success_message
25 | redirect_to users_settings_url
26 | rescue StandardError => e
27 | Sentry.capture_exception(e)
28 | flash[:alert] = error_message
29 | redirect_to users_settings_url
30 | end
31 |
32 | def customer_portal
33 | customer_portal_session = Stripe::BillingPortal::Session.create(
34 | {
35 | customer: current_user.stripe_customer_id,
36 | return_url: users_settings_url
37 | }
38 | )
39 | redirect_to customer_portal_session.url, allow_other_host: true
40 | rescue StandardError
41 | flash[:alert] = error_message
42 | redirect_to users_settings_url
43 | end
44 |
45 | def webhooks
46 | # cases:
47 | # - customer cancels subscription for the end of the billing cycle
48 | # (customer.subscription.updated)
49 | # - subscription is canceled at the end of billing cycle (customer.subscription.deleted)
50 | # - customer re-activates canceled account before end of billing cycle
51 | # (customer.subscription.updated)
52 |
53 | case params[:type]
54 | when "customer.subscription.updated"
55 | handle_subscription_updated
56 | when "customer.subscription.deleted"
57 | handle_subscription_deleted
58 | else
59 | head :ok
60 | end
61 | end
62 |
63 | def verify_webhook
64 | Stripe::Webhook.construct_event(
65 | request.raw_post,
66 | request.env["HTTP_STRIPE_SIGNATURE"],
67 | ENV.fetch("STRIPE_ENDPOINT_SECRET", nil)
68 | )
69 | rescue StandardError => e
70 | Sentry.capture_exception(e)
71 | head :not_found
72 | end
73 |
74 | private
75 |
76 | def success_message
77 | "Subscription successful. Enjoy Fugu 🎉🐡"
78 | end
79 |
80 | def error_message
81 | "Something went wrong. Please try again or contact me at canolcer@hey.com"
82 | end
83 |
84 | def sub_cancel_at_period_end
85 | params[:data][:object][:cancel_at_period_end]
86 | end
87 |
88 | def sub_customer_id
89 | params[:data][:object][:customer]
90 | end
91 |
92 | def handle_subscription_updated
93 | user = User.find_by(stripe_customer_id: sub_customer_id)
94 | if sub_cancel_at_period_end && !user.canceled?
95 | user.update!(status: "canceled")
96 | elsif !sub_cancel_at_period_end
97 | user.update!(status: "active")
98 | end
99 |
100 | head :ok
101 | rescue StandardError => e
102 | Sentry.capture_exception(e)
103 | head :internal_server_error
104 | end
105 |
106 | def handle_subscription_deleted
107 | User.find_by(stripe_customer_id: sub_customer_id).update!(status: "inactive")
108 | head :ok
109 | rescue StandardError => e
110 | Sentry.capture_exception(e)
111 | head :internal_server_error
112 | end
113 |
114 | def checkout_session_args
115 | {
116 | payment_method_types: ["card"],
117 | customer_email: current_user.stripe_customer_id ? nil : current_user.email,
118 | customer: current_user.stripe_customer_id,
119 | line_items: [{
120 | price: ENV.fetch("STRIPE_STANDARD_PRICE_ID", nil),
121 | quantity: 1
122 | }],
123 | mode: "subscription",
124 | success_url: "#{stripe_success_callback_url}?session_id={CHECKOUT_SESSION_ID}",
125 | cancel_url: users_settings_url
126 | }
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/app/controllers/users/confirmations_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Users
4 | class ConfirmationsController < Devise::ConfirmationsController
5 | # GET /resource/confirmation/new
6 | # def new
7 | # super
8 | # end
9 |
10 | # POST /resource/confirmation
11 | # def create
12 | # super
13 | # end
14 |
15 | # GET /resource/confirmation?confirmation_token=abcdef
16 | # def show
17 | # super
18 | # end
19 |
20 | # protected
21 |
22 | # The path used after resending confirmation instructions.
23 | # def after_resending_confirmation_instructions_path_for(resource_name)
24 | # super(resource_name)
25 | # end
26 |
27 | # The path used after confirmation.
28 | # def after_confirmation_path_for(resource_name, resource)
29 | # super(resource_name, resource)
30 | # end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/app/controllers/users/omniauth_callbacks_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Users
4 | class OmniauthCallbacksController < Devise::OmniauthCallbacksController
5 | # You should configure your model like this:
6 | # devise :omniauthable, omniauth_providers: [:twitter]
7 |
8 | # You should also create an action method in this controller like this:
9 | # def twitter
10 | # end
11 |
12 | # More info at:
13 | # https://github.com/heartcombo/devise#omniauth
14 |
15 | # GET|POST /resource/auth/twitter
16 | # def passthru
17 | # super
18 | # end
19 |
20 | # GET|POST /users/auth/twitter/callback
21 | # def failure
22 | # super
23 | # end
24 |
25 | # protected
26 |
27 | # The path used when OmniAuth fails
28 | # def after_omniauth_failure_path_for(scope)
29 | # super(scope)
30 | # end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/app/controllers/users/passwords_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Users
4 | class PasswordsController < Devise::PasswordsController
5 | # GET /resource/password/new
6 | # def new
7 | # super
8 | # end
9 |
10 | # POST /resource/password
11 | # def create
12 | # super
13 | # end
14 |
15 | # GET /resource/password/edit?reset_password_token=abcdef
16 | # def edit
17 | # super
18 | # end
19 |
20 | # PUT /resource/password
21 | # def update
22 | # super
23 | # end
24 |
25 | # protected
26 |
27 | # def after_resetting_password_path_for(resource)
28 | # super(resource)
29 | # end
30 |
31 | # The path used after sending reset password instructions
32 | # def after_sending_reset_password_instructions_path_for(resource_name)
33 | # super(resource_name)
34 | # end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/controllers/users/registrations_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Users
4 | class RegistrationsController < Devise::RegistrationsController
5 | # before_action :configure_sign_up_params, only: [:create]
6 | # before_action :configure_account_update_params, only: [:update]
7 |
8 | # GET /resource/sign_up
9 | def new
10 | super
11 | FuguService.track("Viewed Sign Up")
12 | end
13 |
14 | def create
15 | return unless ENV["ALLOW_REGISTRATION"] == "true"
16 |
17 | super do |resource|
18 | FuguService.track("New User") if resource.persisted?
19 |
20 | if resource.errors.present?
21 | flash.now[:alert] =
22 | resource.errors.full_messages.join(", ")
23 | end
24 | end
25 | end
26 |
27 | # GET /resource/edit
28 | # def edit
29 | # super
30 | # end
31 |
32 | # PUT /resource
33 | # def update
34 | # super
35 | # end
36 |
37 | # DELETE /resource
38 | # def destroy
39 | # super
40 | # end
41 |
42 | # GET /resource/cancel
43 | # Forces the session data which is usually expired after sign
44 | # in to be expired now. This is useful if the user wants to
45 | # cancel oauth signing in/up in the middle of the process,
46 | # removing all OAuth session data.
47 | # def cancel
48 | # super
49 | # end
50 |
51 | # protected
52 |
53 | # If you have extra params to permit, append them to the sanitizer.
54 | # def configure_sign_up_params
55 | # devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
56 | # end
57 |
58 | # If you have extra params to permit, append them to the sanitizer.
59 | # def configure_account_update_params
60 | # devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
61 | # end
62 |
63 | # The path used after sign up.
64 | # def after_sign_up_path_for(resource)
65 | # super(resource)
66 | # end
67 |
68 | # The path used after sign up for inactive accounts.
69 | # def after_inactive_sign_up_path_for(resource)
70 | # super(resource)
71 | # end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/app/controllers/users/sessions_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Users
4 | class SessionsController < Devise::SessionsController
5 | # before_action :configure_sign_in_params, only: [:create]
6 |
7 | # GET /resource/sign_in
8 | def new
9 | super
10 | FuguService.track("Viewed Sign In")
11 | end
12 |
13 | # POST /resource/sign_in
14 | # def create
15 | # super
16 | # end
17 |
18 | # DELETE /resource/sign_out
19 | # def destroy
20 | # super
21 | # end
22 |
23 | # protected
24 |
25 | # If you have extra params to permit, append them to the sanitizer.
26 | # def configure_sign_in_params
27 | # devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
28 | # end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/app/controllers/users/settings_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Users
4 | class SettingsController < ApplicationController
5 | before_action :authenticate_user!
6 | before_action :show_not_active_flash, only: %i[show]
7 |
8 | def show
9 | return unless ENV["FUGU_CLOUD"] == "true"
10 |
11 | # following code is irrelevant if you're self-hosting
12 | return unless current_user.canceled? && current_user.stripe_customer_id.present?
13 |
14 | customer = retrieve_stripe_customer
15 |
16 | return unless customer.subscriptions.data.first
17 |
18 | @cancel_at = format_cancel_time(customer.subscriptions.data.first.cancel_at)
19 | end
20 |
21 | private
22 |
23 | def retrieve_stripe_customer
24 | Stripe::Customer.retrieve(
25 | id: current_user.stripe_customer_id,
26 | expand: ["subscriptions"]
27 | )
28 | end
29 |
30 | def format_cancel_time(unix_time)
31 | return unless unix_time
32 |
33 | Time.at(unix_time).utc.to_datetime.strftime("%b %-d, %Y")
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/controllers/users/unlocks_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Users
4 | class UnlocksController < Devise::UnlocksController
5 | # GET /resource/unlock/new
6 | # def new
7 | # super
8 | # end
9 |
10 | # POST /resource/unlock
11 | # def create
12 | # super
13 | # end
14 |
15 | # GET /resource/unlock?unlock_token=abcdef
16 | # def show
17 | # super
18 | # end
19 |
20 | # protected
21 |
22 | # The path used after sending unlock password instructions
23 | # def after_sending_unlock_instructions_path_for(resource)
24 | # super(resource)
25 | # end
26 |
27 | # The path used after unlocking the resource
28 | # def after_unlock_path_for(resource)
29 | # super(resource)
30 | # end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module ApplicationHelper
4 | def evergreen_params
5 | # params that we often pass along
6 | { test: params[:test], embed: params[:embed] }
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/app/javascript/application.js:
--------------------------------------------------------------------------------
1 | // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
2 | import "@hotwired/stimulus-importmap-autoloader"
3 | import "controllers"
4 | import "@hotwired/turbo-rails"
5 |
--------------------------------------------------------------------------------
/app/javascript/controllers/application.js:
--------------------------------------------------------------------------------
1 | import { Application } from "@hotwired/stimulus"
2 |
3 | const application = Application.start()
4 |
5 | // Configure Stimulus development experience
6 | application.warnings = true
7 | application.debug = false
8 | window.Stimulus = application
9 |
10 | export { application }
11 |
--------------------------------------------------------------------------------
/app/javascript/controllers/chart_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus"
2 | import { Chart, registerables } from "chart.js";
3 | import {
4 | LS_VISIBLE_PROP_VALUES,
5 | formatDates,
6 | formatLocalStorageName,
7 | htmlDecode,
8 | htmlLegendPlugin,
9 | initLocalStoragePropValues,
10 | localStorageItemJSON
11 | } from "chart_helpers";
12 |
13 | Chart.register(...registerables);
14 |
15 | export default class extends Controller {
16 | static targets = [ "chart"]
17 | static values = {
18 | dates: Array,
19 | events: Object,
20 | agg: String,
21 | eventName: String,
22 | hasPropValues: Boolean,
23 | propertyName: String,
24 | projectId: String
25 | }
26 |
27 | connect() {
28 | initLocalStoragePropValues(this.eventsValue, this.projectIdValue, this.propertyNameValue);
29 | this.visiblePropValues = localStorageItemJSON(
30 | formatLocalStorageName(
31 | this.projectIdValue,
32 | this.propertyNameValue,
33 | LS_VISIBLE_PROP_VALUES
34 | )
35 | );
36 | this.initColorPalette();
37 | this.showChart();
38 | }
39 |
40 | showChart() {
41 | console.log(this.eventsValue)
42 | const data = {
43 | labels: formatDates(this.datesValue, this.aggValue),
44 | datasets: Object.keys(this.eventsValue).map((e, i) => this.createDataSet(e, this.eventsValue[e], i))
45 | };
46 |
47 | const config = {
48 | type: "line",
49 | data,
50 | options: {
51 | layout: {
52 | padding: 10,
53 | },
54 | maintainAspectRatio: false,
55 | scales: {
56 | y: {
57 | grace: "10%",
58 | beginAtZero: true,
59 | ticks: {
60 | format: { style: "decimal" },
61 | precision: 0,
62 | padding: 10
63 | },
64 | grid: {
65 | color: "rgba(254, 243, 199, 0.7)",
66 | lineWidth: 2,
67 | drawBorder: false
68 | }
69 | },
70 | x: {
71 | grid: {
72 | display: false
73 | }
74 | }
75 | },
76 | spanGaps: true,
77 | plugins: {
78 | htmlLegend: {
79 | display: this.hasPropValuesValue,
80 | projectId: this.projectIdValue,
81 | propertyName: this.propertyNameValue
82 | },
83 | legend: {
84 | display: false
85 | }
86 | },
87 | },
88 | plugins: [ htmlLegendPlugin ]
89 | }
90 | Chart.defaults.font.family = "'-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Oxygen-Sans', 'Ubuntu', 'Cantarell', 'Helvetica Neue', sans-serif";
91 | this.chart = new Chart(
92 | this.chartTarget,
93 | config
94 | );
95 | }
96 |
97 | createDataSet(labelData, data, index) {
98 | let label = ""
99 | if (labelData !== "") {
100 | label = `${htmlDecode(labelData)} (${data["total_count"]})`
101 | }
102 |
103 | return {
104 | label,
105 | backgroundColor: this.colorPalette[index % this.colorPalette.length],
106 | borderColor: this.colorPalette[index % this.colorPalette.length],
107 | borderJointStyle: "round",
108 | borderCapStyle: "round",
109 | borderWidth: 4.5,
110 | tension: 0.15,
111 | pointRadius: this.datesValue.length == 1 ? 1 : 0,
112 | pointHitRadius: 5,
113 | hoverBorderWidth: 4,
114 | data: data["data"],
115 | //hidden: !data["visible"]
116 | hidden: !this.visiblePropValues.includes(htmlDecode(labelData))
117 | }
118 | }
119 |
120 | initColorPalette() {
121 | this.colorPalette = [
122 | "rgb(39, 125, 161)",
123 | "rgb(87, 117, 144)",
124 | "rgb(77, 144, 142)",
125 | "rgb(67, 170, 139)",
126 | "rgb(144, 190, 109)",
127 | "rgb(249, 199, 79)",
128 | "rgb(249, 132, 74)",
129 | "rgb(248, 150, 30)",
130 | "rgb(243, 114, 44)",
131 | "rgb(249, 65, 68)"
132 | ]
133 | }
134 |
135 | selectAllPropValues(e) {
136 | e.preventDefault();
137 |
138 | this.chart.data.datasets.forEach((dataset, index) => {
139 | this.chart.setDatasetVisibility(index, true);
140 | });
141 | this.chart.update();
142 | }
143 |
144 | deselectAllPropValues(e) {
145 | e.preventDefault();
146 |
147 | this.chart.data.datasets.forEach((dataset, index) => {
148 | this.chart.setDatasetVisibility(index, false);
149 | });
150 | this.chart.update();
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/app/javascript/controllers/event_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus"
2 |
3 | export default class extends Controller {
4 | static values = {
5 | aggregation: String
6 | }
7 |
8 | connect() {
9 | this.correctAggValue();
10 | }
11 |
12 | correctAggValue() {
13 | if(new URLSearchParams(window.location.search).get("agg") != null) {
14 | const url = new URL(window.location.href);
15 | url.searchParams.set("agg", this.aggregationValue);
16 | history.replaceState(null, "", url);
17 | } else {
18 | return
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/app/javascript/controllers/funnel_chart_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus"
2 | import { Chart, registerables } from "chart.js";
3 | import ChartDataLabels from "chartjs-plugin-datalabels";
4 |
5 | Chart.register(...registerables);
6 |
7 | export default class extends Controller {
8 | static targets = ["chart"]
9 | static values = {
10 | funnelEventNames: Array,
11 | funnelData: Array,
12 | }
13 |
14 | connect() {
15 | this.initColorPalette();
16 | this.showChart();
17 | }
18 |
19 | showChart() {
20 | const data = {
21 | labels: this.funnelEventNamesValue,
22 | datasets: this.createDataSet(this.funnelDataValue)
23 | };
24 |
25 | const config = {
26 | type: "bar",
27 | data,
28 | plugins: [ChartDataLabels],
29 | options: {
30 | events: [],
31 | layout: {
32 | padding: 10,
33 | },
34 | maintainAspectRatio: false,
35 | scales: {
36 | y: {
37 | grace: "10%",
38 | beginAtZero: true,
39 | ticks: {
40 | format: { style: "decimal" },
41 | precision: 0,
42 | padding: 10
43 | },
44 | grid: {
45 | color: "rgba(254, 243, 199, 0.7)",
46 | lineWidth: 2,
47 | drawBorder: false
48 | }
49 | },
50 | x: {
51 | grid: {
52 | display: false
53 | }
54 | }
55 | },
56 | plugins: {
57 | legend: {
58 | display: this.displayLegend(),
59 | position: "bottom"
60 | },
61 | datalabels: {
62 | labels: {
63 | title: {
64 | font: {
65 | weight: "bold",
66 | lineHeight: 1.3
67 | },
68 | textAlign: "center",
69 | align: "top",
70 | anchor: "end",
71 | offset: 5
72 | }
73 | },
74 | formatter: calcConversionRates
75 | }
76 | }
77 | }
78 | }
79 | Chart.defaults.font.family = "'-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Oxygen-Sans', 'Ubuntu', 'Cantarell', 'Helvetica Neue', sans-serif";
80 | new Chart(
81 | this.chartTarget,
82 | config
83 | );
84 | }
85 |
86 | displayLegend() {
87 | return false;
88 | }
89 |
90 | createDataSet(data) {
91 | return [{
92 | backgroundColor: this.colorPalette[0],
93 | borderColor: this.colorPalette[0],
94 | borderWidth: 0,
95 | borderRadius: 4,
96 | hoverBorderWidth: 4,
97 | hoverBorderColor: "rgb(35, 112, 144)",
98 | maxBarThickness: 100,
99 | data: data,
100 | }]
101 | }
102 |
103 | initColorPalette() {
104 | this.colorPalette = [
105 | "rgb(39, 125, 161)",
106 | "rgb(87, 117, 144)",
107 | "rgb(77, 144, 142)",
108 | "rgb(67, 170, 139)",
109 | "rgb(144, 190, 109)",
110 | "rgb(249, 199, 79)",
111 | "rgb(249, 132, 74)",
112 | "rgb(248, 150, 30)",
113 | "rgb(243, 114, 44)",
114 | "rgb(249, 65, 68)"
115 | ]
116 | }
117 |
118 | htmlDecode(input) {
119 | var doc = new DOMParser().parseFromString(input, "text/html");
120 | return doc.documentElement.textContent;
121 | }
122 | }
123 |
124 | const calcConversionRates = (value, context) => {
125 | let percentages = context.chart.data.datasets[0].data.map((element, index, array) => Math.round(element/array[index-1]*100));
126 | percentages[0] = "";
127 | if (context.dataIndex == 0) {
128 | return value;
129 | }
130 | return `${value}\n(${percentages[context.dataIndex] || 0}%)`;
131 | }
132 |
--------------------------------------------------------------------------------
/app/javascript/controllers/index.js:
--------------------------------------------------------------------------------
1 | // Import and register all your controllers from the importmap under controllers/*
2 |
3 | import { application } from "controllers/application"
4 | import { registerControllersFrom } from "@hotwired/stimulus-importmap-autoloader"
5 | registerControllersFrom("controllers", application)
6 |
--------------------------------------------------------------------------------
/app/javascript/controllers/selection_controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from "@hotwired/stimulus"
2 |
3 | export default class extends Controller {
4 | navigateToSelectUrl(event) {
5 | window.location.href = event.currentTarget.selectedOptions[0].dataset.url;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/jobs/application_job.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationJob < ActiveJob::Base
4 | # Automatically retry jobs that encountered a deadlock
5 | # retry_on ActiveRecord::Deadlocked
6 |
7 | # Most jobs are safe to ignore if the underlying records are no longer available
8 | # discard_on ActiveJob::DeserializationError
9 | end
10 |
--------------------------------------------------------------------------------
/app/mailers/application_mailer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationMailer < ActionMailer::Base
4 | default from: "from@example.com"
5 | layout "mailer"
6 | end
7 |
--------------------------------------------------------------------------------
/app/models/api_key.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: api_keys
6 | #
7 | # id :bigint not null, primary key
8 | # key_value :string not null
9 | # test :boolean default(FALSE)
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | # project_id :bigint not null
13 | #
14 | # Indexes
15 | #
16 | # index_api_keys_on_key_value (key_value) UNIQUE
17 | # index_api_keys_on_project_id (project_id)
18 | #
19 | # Foreign Keys
20 | #
21 | # fk_rails_... (project_id => projects.id)
22 | #
23 |
24 | class ApiKey < ApplicationRecord
25 | has_many :events, dependent: :destroy
26 | has_many :funnels, dependent: :destroy
27 |
28 | belongs_to :project, validate: true
29 |
30 | validates :key_value, presence: true, uniqueness: true
31 |
32 | before_validation :generate_key
33 |
34 | def generate_key
35 | self.key_value ||= SecureRandom.hex
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/app/models/application_record.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ApplicationRecord < ActiveRecord::Base
4 | self.abstract_class = true
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/concerns/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/app/models/concerns/.keep
--------------------------------------------------------------------------------
/app/models/concerns/inactivable.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This validation is only run for the Fugu Cloud version (does not apply for self-hosting)
4 | module Inactivable
5 | extend ActiveSupport::Concern
6 |
7 | included do
8 | validate :user_cannot_be_inactive, if: -> { ENV["FUGU_CLOUD"] == "true" }
9 | end
10 |
11 | private
12 |
13 | def user_cannot_be_inactive
14 | return unless api_key
15 |
16 | return unless !api_key.test && api_key.project.user.inactive?
17 |
18 | errors.add(:base, "You need an active subscription to capture events with your live API key")
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/models/funnel.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: funnels
4 | #
5 | # id :bigint not null, primary key
6 | # name :string not null
7 | # created_at :datetime not null
8 | # updated_at :datetime not null
9 | # api_key_id :bigint not null
10 | #
11 | # Indexes
12 | #
13 | # index_funnels_on_api_key_id (api_key_id)
14 | # index_funnels_on_name_and_api_key_id (name,api_key_id) UNIQUE
15 | #
16 | # Foreign Keys
17 | #
18 | # fk_rails_... (api_key_id => api_keys.id)
19 | #
20 |
21 | # frozen_string_literal: true
22 |
23 | class Funnel < ApplicationRecord
24 | belongs_to :api_key
25 |
26 | has_many :funnel_steps, dependent: :destroy
27 | accepts_nested_attributes_for :funnel_steps,
28 | reject_if: ->(attr) { attr[:event_name].blank? }
29 |
30 | validates :name,
31 | presence: true,
32 | uniqueness: {
33 | scope: :api_key_id,
34 | message: "You already have a funnel with this name",
35 | case_insensitive: true
36 | },
37 | format:
38 | {
39 | with: /\A[a-zA-Z0-9\s]*\z/,
40 | message: "can only contain numbers, letters and spaces"
41 | }
42 |
43 | validates :funnel_steps, presence: { message: "- Add at least one funnel step" }
44 |
45 | before_validation :titleize_name
46 | before_validation :strip_name
47 |
48 | FUNNEL_PARAMS = %i[user_id project_slug slug test event prop date embed].freeze
49 |
50 | private
51 |
52 | def titleize_name
53 | self.name = name.downcase.titleize if name
54 | end
55 |
56 | def strip_name
57 | self.name = name.strip if name
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/app/models/funnel_step.rb:
--------------------------------------------------------------------------------
1 | # == Schema Information
2 | #
3 | # Table name: funnel_steps
4 | #
5 | # id :bigint not null, primary key
6 | # event_name :string not null
7 | # order :integer
8 | # created_at :datetime not null
9 | # updated_at :datetime not null
10 | # funnel_id :bigint not null
11 | #
12 | # Indexes
13 | #
14 | # index_funnel_steps_on_funnel_id (funnel_id)
15 | #
16 | # Foreign Keys
17 | #
18 | # fk_rails_... (funnel_id => funnels.id)
19 | #
20 |
21 | # frozen_string_literal: true
22 |
23 | class FunnelStep < ApplicationRecord
24 | belongs_to :funnel
25 |
26 | validates :event_name, presence: true
27 | end
28 |
--------------------------------------------------------------------------------
/app/models/project.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: projects
6 | #
7 | # id :bigint not null, primary key
8 | # name :string not null
9 | # public :boolean default(FALSE)
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | # user_id :bigint not null
13 | #
14 | # Indexes
15 | #
16 | # index_projects_on_name_and_user_id (name,user_id) UNIQUE
17 | # index_projects_on_user_id (user_id)
18 | #
19 | # Foreign Keys
20 | #
21 | # fk_rails_... (user_id => users.id)
22 | #
23 |
24 | class Project < ApplicationRecord
25 | has_many :api_keys, dependent: :destroy
26 | belongs_to :user
27 |
28 | validates :name,
29 | presence: true,
30 | length: { maximum: 40 },
31 | uniqueness: {
32 | scope: :user_id,
33 | message: "You already have a project with this name",
34 | case_insensitive: true
35 | },
36 | format:
37 | {
38 | with: /\A[a-zA-Z0-9-]*\z/,
39 | message: "can only contain numbers, letters and hyphens"
40 | },
41 | exclusion:
42 | {
43 | in: %w[project projects],
44 | message: "'%{value}' is a reversed event name by Fugu and can't be used"
45 | }
46 |
47 | before_validation :downcase_name
48 | before_validation :strip_name
49 |
50 | def downcase_name
51 | self.name = name.downcase if name
52 | end
53 |
54 | def strip_name
55 | self.name = name.strip if name
56 | end
57 |
58 | def create_api_keys
59 | ApiKey.create(project: self, test: false)
60 | ApiKey.create(project: self, test: true)
61 | end
62 |
63 | def api_key_live
64 | api_keys.find_by(test: false)
65 | end
66 |
67 | def api_key_test
68 | api_keys.find_by(test: true)
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/app/models/user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: users
6 | #
7 | # id :bigint not null, primary key
8 | # confirmation_sent_at :datetime
9 | # confirmation_token :string
10 | # confirmed_at :datetime
11 | # email :string default(""), not null
12 | # encrypted_password :string default(""), not null
13 | # remember_created_at :datetime
14 | # reset_password_sent_at :datetime
15 | # reset_password_token :string
16 | # status :integer default("inactive"), not null
17 | # unconfirmed_email :string
18 | # created_at :datetime not null
19 | # updated_at :datetime not null
20 | # hash_id :string not null
21 | # stripe_customer_id :string
22 | #
23 | # Indexes
24 | #
25 | # index_users_on_confirmation_token (confirmation_token) UNIQUE
26 | # index_users_on_email (email) UNIQUE
27 | # index_users_on_hash_id (hash_id) UNIQUE
28 | # index_users_on_reset_password_token (reset_password_token) UNIQUE
29 | #
30 |
31 | class User < ApplicationRecord
32 | has_many :projects, dependent: :destroy
33 |
34 | validates :hash_id, presence: true, allow_blank: false
35 |
36 | enum status: {
37 | inactive: 0,
38 | active: 1,
39 | canceled: 2
40 | }
41 |
42 | # Include default devise modules. Others available are:
43 | # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
44 | devise :database_authenticatable, :registerable,
45 | :recoverable, :rememberable, :validatable
46 |
47 | before_validation :add_hash_id, on: :create
48 |
49 | def add_hash_id
50 | # generate random hash_id for the user before being created
51 | return if hash_id.present?
52 |
53 | hash = SecureRandom.alphanumeric(4).downcase
54 | unless User.exists?(hash_id: hash)
55 | self.hash_id = hash
56 | return
57 | end
58 | add_hash_id
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/app/services/fugu_service.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class FuguService
4 | def self.track(name, properties = {})
5 | return unless ENV.fetch("FUGU_URL", nil)
6 |
7 | return if Rails.env.test?
8 |
9 | body = {
10 | api_key: ENV.fetch("FUGU_API_KEY", nil),
11 | name: name,
12 | properties: properties
13 | }
14 |
15 | begin
16 | Faraday.post(ENV.fetch("FUGU_URL", nil)) do |req|
17 | req.headers["Content-Type"] = "application/json"
18 | req.body = body.to_json
19 | end
20 | rescue StandardError => e
21 | logger = Logger.new($stdout)
22 | logger.error("Error while calling Fugu API: #{e}")
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/app/views/development/embed_mock/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :main do %>
2 |
3 |
Embedding testing ground
4 |
14 | <% end %>
--------------------------------------------------------------------------------
/app/views/events/_chart.html.erb:
--------------------------------------------------------------------------------
1 |
12 |
13 | Total events
14 |
15 | <%= @total_count %>
16 |
17 |
18 |
19 |
20 |
21 | <% if @property_values.present? %>
22 |
23 |
Property values
24 |
25 |
26 |
Selected
27 | <%= link_to "(deselect all)",
28 | "",
29 | class: "bg-transparent underline text-sm font-light hover:text-green-500 cursor-pointer",
30 | data: { action: "chart#deselectAllPropValues" }
31 | %>
32 |
33 |
35 |
36 |
37 |
38 |
Not selected
39 | <%= link_to "(select all)",
40 | "",
41 | class: "bg-transparent underline text-sm font-light hover:text-green-500 cursor-pointer",
42 | data: { action: "chart#selectAllPropValues" }
43 | %>
44 |
45 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | <% end %>
54 |
--------------------------------------------------------------------------------
/app/views/events/_selections.html.erb:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 | <%= label_tag 'events', 'Events', class: "text-sm font-medium mb-2" %>
9 | <%= select_tag "events",
10 | raw(event_select_options(param_data, aggregation, event_names)),
11 | data: { action: "change->selection#navigateToSelectUrl" },
12 | class: "w-56 text-sm"
13 | %>
14 |
15 |
16 | <%= label_tag 'properties', 'Properties', class: "text-sm font-medium mb-2" %>
17 | <%= select_tag "properties",
18 | raw(property_select_options(param_data, aggregation, properties)),
19 | data: { action: "change->selection#navigateToSelectUrl" },
20 | class: "w-44 text-sm"
21 | %>
22 |
23 |
24 | <%= label_tag 'aggregation', 'Aggregation', class: "text-sm font-medium mb-2" %>
25 | <%= select_tag "aggregation",
26 | raw(agg_select_options(param_data, aggregation, possible_aggregations)),
27 | data: { action: "change->selection#navigateToSelectUrl" },
28 | class: "text-sm"
29 | %>
30 |
31 |
32 |
33 |
34 | <%= label_tag 'date_range', 'Date Range', class: "text-sm font-medium mb-2" %>
35 | <%= select_tag "date_range",
36 | raw(date_select_options(param_data, aggregation, "event")),
37 | data: { action: "change->selection#navigateToSelectUrl" },
38 | class: "text-sm"
39 | %>
40 |
41 |
42 |
--------------------------------------------------------------------------------
/app/views/events/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :project_header do %>
2 | <%= render partial: "partials/project_header", locals: { project_name: @project.name } %>
3 | <% end %>
4 |
5 | <% content_for :no_data do %>
6 |
7 | You haven't tracked any events for this project yet.
8 | <% if current_user %>
9 | <%# we don't want to show the rest for public projects %>
10 | To get started, call the Fugu API from your mobile or web app.
11 | Check out the
12 | docs to learn more.
13 | Tip: You'll find your API keys in your
14 | <%= link_to "project settings", user_project_settings_path(current_user.hash_id) %>.
15 | <% if params[:test] == "true" %>
16 | Did you track events with your live API key?
17 | <%= link_to "Toggle live mode!", user_project_events_path(current_user.hash_id, @project.name) %>
18 | <% else %>
19 | Did you track events with your test API key?
20 | <%= link_to "Toggle test mode!", user_project_events_path(current_user.hash_id, @project.name, params: { test: true }) %>
21 | <% end %>
22 | <% end %>
23 |
24 | <% end %>
--------------------------------------------------------------------------------
/app/views/events/show.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :project_header do %>
2 | <%= render partial: "partials/project_header", locals: { project_name: @project.name } %>
3 | <% end %>
4 |
5 | <% content_for :data_controls do %>
6 | <%= render partial: "selections",
7 | locals: {
8 | param_data: params,
9 | event_names: @event_names,
10 | properties: @properties,
11 | possible_aggregations: @possible_aggregations,
12 | aggregation: @aggregation
13 | }
14 | %>
15 | <% end%>
16 |
17 | <% content_for :chart_view do %>
18 | <%= render partial: "chart",
19 | locals: {
20 | dates: @dates,
21 | events: @events,
22 | selected_event: @selected_event,
23 | property_values: @property_values,
24 | property_name: @property,
25 | project_id: @project.id
26 | }
27 | %>
28 | <% end %>
29 |
30 | <% content_for :project_footer do %>
31 |
32 | <%= link_to "Project settings", user_project_settings_path(params[:user_id]), class: "font-normal" %>
33 | <%= render partial: "partials/mode_toggle",
34 | locals: {
35 | project_name: @project.name,
36 | test_mode: params[:test] == "true",
37 | aggregation: @aggregation,
38 | chart_type: "event"
39 | }
40 | %>
41 |
42 | <% end %>
--------------------------------------------------------------------------------
/app/views/funnels/_chart.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/funnels/_selections.html.erb:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 | <%= label_tag 'funnels', 'Funnels', class: "text-sm font-medium mb-2" %>
8 | <%= select_tag "funnels",
9 | raw(funnel_select_options(param_data, funnel_names)),
10 | data: { action: "change->selection#navigateToSelectUrl" },
11 | class: "w-56 text-sm"
12 | %>
13 |
14 |
15 |
16 |
17 | <%= label_tag 'date_range', 'Date Range', class: "text-sm font-medium mb-2" %>
18 | <%= select_tag "date_range",
19 | raw(date_select_options(param_data, nil, "funnel")),
20 | data: { action: "change->selection#navigateToSelectUrl" },
21 | class: "text-sm"
22 | %>
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/views/funnels/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :project_header do %>
2 | <%= render partial: "partials/project_header", locals: { project_name: @project.name } %>
3 | <% end %>
4 |
5 | <% content_for :no_data do %>
6 |
7 | You haven't created any funnels for this project yet.
8 | <% if current_user %>
9 | <%# we don't want to show the rest for public projects %>
10 | To get started, <%= link_to "create a funnel", new_user_project_funnel_path(current_user.hash_id, @project.name, params: { test: params[:test] }) %>.
11 | <% if params[:test] == "true" %>
12 | Did you create funnels in live mode?
13 | <%= link_to "Toggle live mode!", user_project_funnels_path(current_user.hash_id, @project.name) %>
14 | <% else %>
15 | Did you create funnels in test mode?
16 | <%= link_to "Toggle test mode!", user_project_funnels_path(current_user.hash_id, @project.name, params: { test: true }) %>
17 | <% end %>
18 | <% end %>
19 |
20 | <% end %>
--------------------------------------------------------------------------------
/app/views/funnels/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :main do %>
2 | Create new funnel
3 | <%= render "partials/narrow_box" do %>
4 | <%= form_with model: @funnel, url: user_project_funnels_path do |funnel_form| %>
5 | Give your funnel a name:
6 |
7 |
8 | <%= funnel_form.label :name, "Name" %>
9 |
10 | <%= funnel_form.text_field :name, required: true, placeholder: "E.g., Sign up to subscription", class: "w-64 rounded border-2 border-gray-600" %>
11 |
12 | Select an event for each funnel step:
13 |
14 | <%= funnel_form.fields_for :funnel_steps do |funnel_steps_form| %>
15 |
16 |
17 | <%= funnel_steps_form.label :event_name, "Step #{funnel_steps_form.index + 1}" %>
18 |
19 | <%=
20 | funnel_steps_form.select :event_name,
21 | options_for_select(@event_names, "Select event"),
22 | {include_blank: "Select event"},
23 | required: false, #funnel_steps_form.index == 0,
24 | class: "w-64"
25 | %>
26 |
27 | <% end %>
28 |
29 | <%= hidden_field_tag "test", params[:test] %>
30 | <%= funnel_form.submit "Create funnel", class: "main-button" %>
31 | <% end %>
32 | <% end %>
33 | <%= link_to "Cancel and go back to #{@project.name}", user_project_events_path(current_user.hash_id, @project.name) %>
34 | <% if params[:test] == "true" %>
35 | <%= link_to "Toggle LIVE mode", new_user_project_funnel_path(current_user.hash_id, @project.name, params: { test: false }), class: "font-normal mt-2" %>
36 | <% else %>
37 | <%= link_to "Toggle TEST mode", new_user_project_funnel_path(current_user.hash_id, @project.name, params: { test: true }), class: "font-normal mt-2" %>
38 | <% end %>
39 | <% end %>
--------------------------------------------------------------------------------
/app/views/funnels/show.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :project_header do %>
2 | <%= render partial: "partials/project_header", locals: { project_name: @project.name } %>
3 | <% end %>
4 |
5 | <% content_for :data_controls do %>
6 | <%= render partial: "selections",
7 | locals: {
8 | param_data: params,
9 | funnel_names: @funnel_names,
10 | properties: @properties
11 | }
12 | %>
13 | <% end%>
14 |
15 | <% content_for :chart_view do %>
16 | <%= render partial: "chart",
17 | locals: {
18 | funnel_event_names: @funnel_event_names,
19 | funnel_data: @funnel_data,
20 | }
21 | %>
22 | <% end %>
23 |
24 | <% content_for :project_footer do %>
25 |
26 | <%= link_to "Create new funnel",
27 | new_user_project_funnel_path(params[:user_id], @project.name, params: { test: params[:test] }),
28 | class: "font-normal"
29 | %>
30 | <%= link_to "Project settings", user_project_settings_path(params[:user_id]), class: "font-normal" %>
31 | <%= render partial: "partials/mode_toggle",
32 | locals: {
33 | project_name: @project.name,
34 | test_mode: params[:test] == "true",
35 | aggregation: nil,
36 | chart_type: "funnel"
37 | } if current_user
38 | %>
39 |
40 | <% end %>
--------------------------------------------------------------------------------
/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Fugu
5 |
6 |
7 | <%= csrf_meta_tags %>
8 | <%= csp_meta_tag %>
9 |
10 | <%= favicon_link_tag asset_path('fugu_favicon.png') %>
11 |
12 | <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
13 | <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
14 | <%= javascript_importmap_tags %>
15 |
16 | <%= yield :head %>
17 |
18 |
19 |
20 | <% unless params[:embed] == "true" %>
21 | <%= render "partials/header" %>
22 | <% flash.each do |key, value| %>
23 | <%= content_tag :div,
24 | value,
25 | class: "flex flex-row text-center justify-center border-2 p-4 shadow-sm flash-#{key}" unless value.blank? %>
26 | <% end %>
27 | <% end %>
28 |
29 | <%= content_for?(:data_view) ? yield(:data_view) : yield(:main) %>
30 |
31 | <%= render "partials/footer" unless params[:embed] == "true" %>
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/views/layouts/data_view.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :head do %>
2 |
3 | <% end %>
4 |
5 | <% content_for :data_view do %>
6 | <%= yield :project_header %>
7 |
8 | <% unless content_for?(:no_data) %>
9 |
10 | <%= yield :data_controls %>
11 |
12 |
13 | <%= yield :chart_view %>
14 |
15 |
16 | <% else %>
17 |
18 | <%= yield :no_data %>
19 |
20 | <% end %>
21 |
22 | <%= yield :project_footer if current_user && params[:embed] != "true" %>
23 | <% end %>
24 |
25 | <%= render template: "layouts/application" %>
--------------------------------------------------------------------------------
/app/views/layouts/mailer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/views/layouts/mailer.text.erb:
--------------------------------------------------------------------------------
1 | <%= yield %>
2 |
--------------------------------------------------------------------------------
/app/views/partials/_footer.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Fugu is 100% self-funded, independent and hosted in the EU.
5 | By
@canolcer .
6 |
7 |
16 |
17 |
--------------------------------------------------------------------------------
/app/views/partials/_header.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/partials/_mode_toggle.html.erb:
--------------------------------------------------------------------------------
1 | <% if test_mode %>
2 | <%= link_to "Toggle LIVE mode", build_test_toggle_url(params, aggregation, false, chart_type), class: "font-normal" %>
3 | <% else %>
4 | <%= link_to "Toggle TEST mode", build_test_toggle_url(params, aggregation, true, chart_type), class: "font-normal" %>
5 | <% end %>
--------------------------------------------------------------------------------
/app/views/partials/_narrow_box.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= yield %>
3 |
--------------------------------------------------------------------------------
/app/views/partials/_project_header.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
<%= project_name %>
4 |
5 |
6 | <%= link_to "Events",
7 | user_project_events_path(params[:user_id], @project.name, evergreen_params)
8 | %>
9 | <%= link_to "Funnels",
10 | user_project_funnels_path(params[:user_id], @project.name, evergreen_params),
11 | class: "ml-4"
12 | %>
13 |
--------------------------------------------------------------------------------
/app/views/projects/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :main do %>
2 | Edit project
3 | <%= form_with model: @project, url: user_project_path do |form| %>
4 |
5 |
6 | <%= form.label :name, 'Project name' %>
7 |
8 | <%= form.text_field :name, autofocus: true, required: true, class: "rounded-lg w-full" %>
9 |
10 | Project name can't include any special characters except "-".
11 |
12 |
13 |
14 |
15 | <%= form.label :public, 'Make public' %>
16 | <%= form.check_box :public, class: 'ml-2' %>
17 |
18 |
19 | Public projects can be viewed by anyone who has this URL.
20 |
21 |
22 | <%= form.hidden_field :project_id, value: @project.id %>
23 |
24 | <%= form.submit 'Update', class: 'main-button' %>
25 |
26 | <% end %>
27 | <% end %>
--------------------------------------------------------------------------------
/app/views/projects/index.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :main do %>
2 | Projects
3 | <%= render "partials/narrow_box" do %>
4 | <% if @projects.any? %>
5 | <% @projects.each do |project| %>
6 |
7 | <%= link_to project.name, user_project_events_path(current_user.hash_id, project.name), class: "no-underline text-lg" %>
8 |
9 | <% end %>
10 | <% else %>
11 |
12 | Welcome to Fugu 👋 To get started, add a new project
13 | and track your first event by calling the Fugu API.
14 | Have a look at the
15 | docs for more info.
16 |
17 | <% end %>
18 | <%= link_to 'Add a new project', new_user_project_path(current_user.hash_id), class: "mt-4" %>
19 | <% end %>
20 | <% end %>
--------------------------------------------------------------------------------
/app/views/projects/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :main do %>
2 | Add a new project
3 | <%= form_with model: @project, url: user_projects_path do |form| %>
4 |
5 |
6 | <%= form.label :name, 'Project name' %>
7 |
8 | <%= form.text_field :name, autofocus: true, required: true, class: 'rounded-lg w-full' %>
9 |
10 | Project name can't include any special characters except "-".
11 |
12 |
13 |
14 | <%= form.submit 'Add', class: 'main-button' %>
15 |
16 | <% end %>
17 | <% end %>
--------------------------------------------------------------------------------
/app/views/projects/settings.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :main do %>
2 | Project settings
3 | <%= render "partials/narrow_box" do %>
4 | Project name
5 |
6 | <%= @project.name %>
7 |
8 | Visibility
9 |
10 | <% unless @project.public %>
11 | This project is private.
12 | <% else %>
13 | This project is public. Share the project URL with your friends to impress them.
14 | <% end %>
15 |
16 | API keys
17 |
18 |
Test API key
<%= @project.api_key_test.key_value %>
19 |
Test events are deleted after 14 days
20 |
Live API key
<%= @project.api_key_live.key_value %>
21 |
22 | Embedding
23 | <% if @project.public %>
24 |
25 | Copy and paste the embed code anywhere to display your project.
26 | Your project's visibility needs to be set to public.
27 | Check out the docs
28 | to learn how to customize your embed code.
29 |
30 |
31 |
32 | <%=
33 | %()
34 | %>
35 |
36 |
37 | <% else %>
38 | To display your embed code, edit your project and set it's visibility to public.
39 | <% end %>
40 |
41 | <%= link_to "Edit project",
42 | edit_user_project_path(current_user.hash_id, @project.name),
43 | class: "bg-transparent underline font-medium hover:text-green-500 cursor-pointer"
44 | %>
45 | <%= button_to "Delete project",
46 | user_project_path(current_user.hash_id, @project.name),
47 | method: :delete,
48 | class: "bg-transparent underline font-medium hover:text-green-500 cursor-pointer mt-2",
49 | onclick: "return confirm('This will delete this project and all its events forever. Are you sure?');"
50 | %>
51 |
52 | <% end %>
53 | <%= link_to "Back to #{@project.name}", user_project_events_path(current_user.hash_id, @project.name) %>
54 | <% end %>
--------------------------------------------------------------------------------
/app/views/users/confirmations/new.html.erb:
--------------------------------------------------------------------------------
1 | Resend confirmation instructions
2 |
3 | <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
4 | <%= render "devise/shared/error_messages", resource: resource %>
5 |
6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
9 |
10 |
11 |
12 | <%= f.submit "Resend confirmation instructions" %>
13 |
14 | <% end %>
15 |
16 | <%= render "devise/shared/links" %>
17 |
--------------------------------------------------------------------------------
/app/views/users/mailer/confirmation_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Welcome <%= @email %>!
2 |
3 | You can confirm your account email through the link below:
4 |
5 | <%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>
6 |
--------------------------------------------------------------------------------
/app/views/users/mailer/email_changed.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @email %>!
2 |
3 | <% if @resource.try(:unconfirmed_email?) %>
4 | We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.
5 | <% else %>
6 | We're contacting you to notify you that your email has been changed to <%= @resource.email %>.
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/app/views/users/mailer/password_change.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | We're contacting you to notify you that your password has been changed.
4 |
--------------------------------------------------------------------------------
/app/views/users/mailer/reset_password_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | Someone has requested a link to change your password. You can do this through the link below.
4 |
5 | <%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>
6 |
7 | If you didn't request this, please ignore this email.
8 | Your password won't change until you access the link above and create a new one.
9 |
--------------------------------------------------------------------------------
/app/views/users/mailer/unlock_instructions.html.erb:
--------------------------------------------------------------------------------
1 | Hello <%= @resource.email %>!
2 |
3 | Your account has been locked due to an excessive number of unsuccessful sign in attempts.
4 |
5 | Click the link below to unlock your account:
6 |
7 | <%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>
8 |
--------------------------------------------------------------------------------
/app/views/users/passwords/edit.html.erb:
--------------------------------------------------------------------------------
1 | Change your password
2 |
3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
4 | <%= render "devise/shared/error_messages", resource: resource %>
5 | <%= f.hidden_field :reset_password_token %>
6 |
7 |
8 | <%= f.label :password, "New password" %>
9 | <% if @minimum_password_length %>
10 | (<%= @minimum_password_length %> characters minimum)
11 | <% end %>
12 | <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %>
13 |
14 |
15 |
16 | <%= f.label :password_confirmation, "Confirm new password" %>
17 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
18 |
19 |
20 |
21 | <%= f.submit "Change my password" %>
22 |
23 | <% end %>
24 |
25 | <%= render "devise/shared/links" %>
26 |
--------------------------------------------------------------------------------
/app/views/users/passwords/new.html.erb:
--------------------------------------------------------------------------------
1 | Forgot your password?
2 |
3 | <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %>
4 | <%= render "devise/shared/error_messages", resource: resource %>
5 |
6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
9 |
10 |
11 |
12 | <%= f.submit "Send me reset password instructions" %>
13 |
14 | <% end %>
15 |
16 | <%= render "devise/shared/links" %>
17 |
--------------------------------------------------------------------------------
/app/views/users/registrations/edit.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :main do %>
2 | Edit <%= resource_name.to_s.humanize %>
3 |
4 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
5 | <%= render "devise/shared/error_messages", resource: resource %>
6 |
7 |
8 | <%= f.label :email %>
9 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
10 |
11 |
12 | <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
13 | Currently waiting confirmation for: <%= resource.unconfirmed_email %>
14 | <% end %>
15 |
16 |
17 | <%= f.label :password %> (leave blank if you don't want to change it)
18 | <%= f.password_field :password, autocomplete: "new-password" %>
19 | <% if @minimum_password_length %>
20 |
21 | <%= @minimum_password_length %> characters minimum
22 | <% end %>
23 |
24 |
25 |
26 | <%= f.label :password_confirmation %>
27 | <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
28 |
29 |
30 |
31 | <%= f.label :current_password %> (we need your current password to confirm your changes)
32 | <%= f.password_field :current_password, autocomplete: "current-password" %>
33 |
34 |
35 |
36 | <%= f.submit "Update" %>
37 |
38 | <% end %>
39 |
40 | Cancel my account
41 |
42 | Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>
43 |
44 | <%= link_to "Back", :back %>
45 | <% end %>
46 |
--------------------------------------------------------------------------------
/app/views/users/registrations/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :main do %>
2 |
3 | Sign up
4 |
5 |
6 | <% unless ENV["ALLOW_REGISTRATION"] == "true" %>
7 |
8 |
9 | The admin of this instance currently doesn't allow new registrations for Fugu.
10 |
11 |
12 | <% else %>
13 |
14 |
15 | Welcome to Fugu! To get started, create an account.
16 |
17 |
18 | You can start tracking test events for free, but will need to subscribe for live mode.
19 |
20 |
21 |
22 | <%= form_for(resource, as: resource_name, url: registration_path(resource_name), data: { turbo: false }) do |f| %>
23 |
24 | <%= f.label :email %>
25 | <%= f.email_field :email, autocomplete: "email", class: "mt-2 rounded-lg w-full" %>
26 |
27 |
28 |
29 | <%= f.label :password %>
30 | <% if @minimum_password_length %>
31 | (<%= @minimum_password_length %> characters minimum)
32 | <% end %>
33 | <%= f.password_field :password, autocomplete: "new-password", class: "mt-2 rounded-lg w-full" %>
34 |
35 |
36 |
37 | <%= f.label :password_confirmation %>
38 | <%= f.password_field :password_confirmation, autocomplete: "new-password", class: "mt-2 rounded-lg w-full" %>
39 |
40 |
41 |
42 | <%= f.submit "Sign up", class: "main-button border-none" %>
43 |
44 | <% end %>
45 | <% end %>
46 | <%= render "devise/shared/links" %>
47 |
48 | <% end %>
49 |
--------------------------------------------------------------------------------
/app/views/users/sessions/new.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :main do %>
2 | Log in
3 |
4 |
5 |
6 |
7 | Welcome back! Log in to access your account.
8 |
9 |
10 |
11 | <%= form_for(resource, as: resource_name, url: session_path(resource_name), data: { turbo: false }) do |f| %>
12 |
13 | <%= f.label :email %>
14 | <%= f.email_field :email, autofocus: true, autocomplete: "email", class: "mt-2 rounded-lg w-full" %>
15 |
16 |
17 |
18 | <%= f.label :password %>
19 | <%= f.password_field :password, autocomplete: "current-password", class: "mt-2 rounded-lg w-full" %>
20 |
21 |
22 | <% if devise_mapping.rememberable? %>
23 |
24 | <%= f.check_box :remember_me, class: "cursor-pointer" %>
25 | <%= f.label :remember_me, class: "cursor-pointer" %>
26 |
27 | <% end %>
28 |
29 |
30 | <%= f.submit "Log in", class: "main-button border-none" %>
31 |
32 | <% end %>
33 |
34 | <%= render "devise/shared/links" %>
35 |
36 |
37 | <% end %>
--------------------------------------------------------------------------------
/app/views/users/settings/_subscription.html.erb:
--------------------------------------------------------------------------------
1 | <%# Only needed by Fugu Cloud %>
2 |
3 | Subscription
4 |
5 | <% if current_user.active? %>
6 | Your Fugu subscription is active and renews monthly.
7 | <%= link_to "Cancel subscription or update payment method", stripe_customer_portal_url,
8 | data: { turbo: false }
9 | %>
10 | <% elsif current_user.canceled? %>
11 | Your Fugu subscription is canceled, but will remain active until <%= cancel_at %>.
12 | <%= link_to "Renew subscription", stripe_customer_portal_url,
13 | data: { turbo: false }
14 | %>
15 | <% else %>
16 | You don't have an active subscription.
17 | Fugu costs $9/month and includes 1 million events/month.
18 | <%= link_to "Start your subscription now", stripe_checkout_session_url,
19 | data: { turbo: false }
20 | %>
21 | <% if current_user.stripe_customer_id.present? %>
22 |
23 | <%= link_to "View past invoices", stripe_customer_portal_url,
24 | data: { turbo: false }
25 | %>
26 | <% end %>
27 | <% end %>
28 |
--------------------------------------------------------------------------------
/app/views/users/settings/_support.html.erb:
--------------------------------------------------------------------------------
1 | <%# Only needed by Fugu Cloud %>
2 |
3 | Support
4 |
5 | Thanks for being an early Fugu user ❤️
6 | Contact me if you have questions or need help: canolcer@hey.com or
7 | @canolcer .
8 |
9 |
--------------------------------------------------------------------------------
/app/views/users/settings/show.html.erb:
--------------------------------------------------------------------------------
1 | <% content_for :main do %>
2 | Account settings
3 | <%= render "partials/narrow_box" do %>
4 | Email
5 |
6 | <%= current_user.email %>
7 |
8 |
9 | <%# only needed by Fugu Cloud (not needed if you're self-hosting) %>
10 | <%= render partial: "subscription", locals: { cancel_at: @cancel_at } if ENV["FUGU_CLOUD"] == "true" %>
11 | <%# only needed by Fugu Cloud (not needed if you're self-hosting) %>
12 | <%= render partial: "support" if ENV["FUGU_CLOUD"] == "true" %>
13 |
14 | <%= button_to "Log out",
15 | destroy_user_session_path,
16 | method: :delete,
17 | class: "bg-transparent underline font-medium hover:text-green-500 cursor-pointer",
18 | data: { turbo: false }
19 | %>
20 | <% end %>
21 | <% end %>
--------------------------------------------------------------------------------
/app/views/users/shared/_error_messages.html.erb:
--------------------------------------------------------------------------------
1 | <% if resource.errors.any? %>
2 |
3 |
4 | <%= I18n.t("errors.messages.not_saved",
5 | count: resource.errors.count,
6 | resource: resource.class.model_name.human.downcase)
7 | %>
8 |
9 |
10 | <% resource.errors.full_messages.each do |message| %>
11 | <%= message %>
12 | <% end %>
13 |
14 |
15 | <% end %>
16 |
--------------------------------------------------------------------------------
/app/views/users/shared/_links.html.erb:
--------------------------------------------------------------------------------
1 | <%- if controller_name != 'sessions' %>
2 | <%= link_to "Log in", new_session_path(resource_name) %>
3 | <% end %>
4 |
5 | <%- if devise_mapping.registerable? && controller_name != 'registrations' %>
6 | <%= link_to "Sign up", new_registration_path(resource_name) %>
7 | <% end %>
8 |
9 | <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
10 | <%= link_to "Forgot your password?", new_password_path(resource_name) %>
11 | <% end %>
12 |
13 | <%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
14 | <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
15 | <% end %>
16 |
17 | <%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
18 | <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
19 | <% end %>
20 |
21 | <%- if devise_mapping.omniauthable? %>
22 | <%- resource_class.omniauth_providers.each do |provider| %>
23 | <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), method: :post %>
24 | <% end %>
25 | <% end %>
26 |
--------------------------------------------------------------------------------
/app/views/users/unlocks/new.html.erb:
--------------------------------------------------------------------------------
1 | Resend unlock instructions
2 |
3 | <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
4 | <%= render "devise/shared/error_messages", resource: resource %>
5 |
6 |
7 | <%= f.label :email %>
8 | <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
9 |
10 |
11 |
12 | <%= f.submit "Resend unlock instructions" %>
13 |
14 | <% end %>
15 |
16 | <%= render "devise/shared/links" %>
17 |
--------------------------------------------------------------------------------
/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # frozen_string_literal: true
3 |
4 | #
5 | # This file was generated by Bundler.
6 | #
7 | # The application 'bundle' is installed as part of a gem, and
8 | # this file is here to facilitate running it.
9 | #
10 |
11 | require "rubygems"
12 |
13 | m = Module.new do
14 | module_function
15 |
16 | def invoked_as_script?
17 | File.expand_path($0) == File.expand_path(__FILE__)
18 | end
19 |
20 | def env_var_version
21 | ENV["BUNDLER_VERSION"]
22 | end
23 |
24 | def cli_arg_version
25 | return unless invoked_as_script? # don't want to hijack other binstubs
26 | return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
27 | bundler_version = nil
28 | update_index = nil
29 | ARGV.each_with_index do |a, i|
30 | if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
31 | bundler_version = a
32 | end
33 | next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
34 | bundler_version = $1
35 | update_index = i
36 | end
37 | bundler_version
38 | end
39 |
40 | def gemfile
41 | gemfile = ENV["BUNDLE_GEMFILE"]
42 | return gemfile if gemfile && !gemfile.empty?
43 |
44 | File.expand_path("../../Gemfile", __FILE__)
45 | end
46 |
47 | def lockfile
48 | lockfile =
49 | case File.basename(gemfile)
50 | when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
51 | else "#{gemfile}.lock"
52 | end
53 | File.expand_path(lockfile)
54 | end
55 |
56 | def lockfile_version
57 | return unless File.file?(lockfile)
58 | lockfile_contents = File.read(lockfile)
59 | return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
60 | Regexp.last_match(1)
61 | end
62 |
63 | def bundler_version
64 | @bundler_version ||=
65 | env_var_version || cli_arg_version ||
66 | lockfile_version
67 | end
68 |
69 | def bundler_requirement
70 | return "#{Gem::Requirement.default}.a" unless bundler_version
71 |
72 | bundler_gem_version = Gem::Version.new(bundler_version)
73 |
74 | requirement = bundler_gem_version.approximate_recommendation
75 |
76 | return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0")
77 |
78 | requirement += ".a" if bundler_gem_version.prerelease?
79 |
80 | requirement
81 | end
82 |
83 | def load_bundler!
84 | ENV["BUNDLE_GEMFILE"] ||= gemfile
85 |
86 | activate_bundler
87 | end
88 |
89 | def activate_bundler
90 | gem_error = activation_error_handling do
91 | gem "bundler", bundler_requirement
92 | end
93 | return if gem_error.nil?
94 | require_error = activation_error_handling do
95 | require "bundler/version"
96 | end
97 | return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
98 | warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
99 | exit 42
100 | end
101 |
102 | def activation_error_handling
103 | yield
104 | nil
105 | rescue StandardError, LoadError => e
106 | e
107 | end
108 | end
109 |
110 | m.load_bundler!
111 |
112 | if m.invoked_as_script?
113 | load Gem.bin_path("bundler", "bundle")
114 | end
115 |
--------------------------------------------------------------------------------
/bin/dev:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | if ! command -v foreman &> /dev/null
4 | then
5 | echo "Installing foreman..."
6 | gem install foreman
7 | fi
8 |
9 | foreman start -f Procfile.dev
10 |
--------------------------------------------------------------------------------
/bin/importmap:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require_relative "../config/application"
4 | require "importmap/commands"
5 |
--------------------------------------------------------------------------------
/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_PATH = File.expand_path("../config/application", __dir__)
3 | require_relative "../config/boot"
4 | require "rails/commands"
5 |
--------------------------------------------------------------------------------
/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require_relative "../config/boot"
3 | require "rake"
4 | Rake.application.run
5 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require "fileutils"
3 |
4 | # path to your application root.
5 | APP_ROOT = File.expand_path("..", __dir__)
6 |
7 | def system!(*args)
8 | system(*args) || abort("\n== Command #{args} failed ==")
9 | end
10 |
11 | FileUtils.chdir APP_ROOT do
12 | # This script is a way to set up or update your development environment automatically.
13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome.
14 | # Add necessary setup steps to this file.
15 |
16 | puts "== Installing dependencies =="
17 | system! "gem install bundler --conservative"
18 | system("bundle check") || system!("bundle install")
19 |
20 | # puts "\n== Copying sample files =="
21 | # unless File.exist?("config/database.yml")
22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml"
23 | # end
24 |
25 | puts "\n== Preparing database =="
26 | system! "bin/rails db:prepare"
27 |
28 | puts "\n== Removing old logs and tempfiles =="
29 | system! "bin/rails log:clear tmp:clear"
30 |
31 | puts "\n== Restarting application server =="
32 | system! "bin/rails restart"
33 | end
34 |
--------------------------------------------------------------------------------
/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | if !defined?(Spring) && [nil, "development", "test"].include?(ENV["RAILS_ENV"])
3 | gem "bundler"
4 | require "bundler"
5 |
6 | # Load Spring without loading other gems in the Gemfile, for speed.
7 | Bundler.locked_gems&.specs&.find { |spec| spec.name == "spring" }&.tap do |spring|
8 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
9 | gem "spring", spring.version
10 | require "spring/binstub"
11 | rescue Gem::LoadError
12 | # Ignore when Spring is not installed.
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/bin/yarn:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | APP_ROOT = File.expand_path('..', __dir__)
3 | Dir.chdir(APP_ROOT) do
4 | yarn = ENV["PATH"].split(File::PATH_SEPARATOR).
5 | select { |dir| File.expand_path(dir) != __dir__ }.
6 | product(["yarn", "yarn.cmd", "yarn.ps1"]).
7 | map { |dir, file| File.expand_path(file, dir) }.
8 | find { |file| File.executable?(file) }
9 |
10 | if yarn
11 | exec yarn, *ARGV
12 | else
13 | $stderr.puts "Yarn executable was not detected in the system."
14 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install"
15 | exit 1
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # This file is used by Rack-based servers to start the application.
4 |
5 | require_relative "config/environment"
6 |
7 | run Rails.application
8 | Rails.application.load_server
9 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative "boot"
2 |
3 | require "rails/all"
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module Eventtracker
10 | class Application < Rails::Application
11 | # Initialize configuration defaults for originally generated Rails version.
12 | config.load_defaults 7.0
13 |
14 | # Configuration for the application, engines, and railties goes here.
15 | #
16 | # These settings can be overridden in specific environments using the files
17 | # in config/environments, which are processed later.
18 | #
19 | # config.time_zone = "Central Time (US & Canada)"
20 | # config.eager_load_paths << Rails.root.join("extras")
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
2 |
3 | require "bundler/setup" # Set up gems listed in the Gemfile.
4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations.
5 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: redis
3 | url: redis://localhost:6379/1
4 |
5 | test:
6 | adapter: test
7 |
8 | production:
9 | adapter: redis
10 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
11 | channel_prefix: eventtracker_production
12 |
--------------------------------------------------------------------------------
/config/credentials.yml.enc:
--------------------------------------------------------------------------------
1 | C4EOAgibZJ8dZvNg/wLywde3Pkj0sSGCs39X/UnadbDiM25u+sgX4w7n9dM2b7coF/uDE/FTG7aQ/oMMO40tjXEZ26WxuU2hZFnu+Wo4XzqOr0vvARYmHHjYuCllAlvGoaldTQt05f1mlg07FIETmdAyPO1FpT274yxWqrYhRMjhim1HvJP3KGERuFv+XD8Rr+nxg26AmQl3LcixFgTWuRgNf5HhgMJiA4Pozs7dnDsg5U48ZgodMmQqc4eJOpLPLTpqlica0OSeyNxO7ouPYW1MlrPVxR4pjOPmznYpvmCd4D5/ImSwRhEIiEZmUrJWzZ7uVRpGkGbpQJCAQckXlNgg0d5GNo2mHH7cFODDCTv9aA+p0ynxwghwV6KryQQZNJMdV+Ap0d/68p+SK8IFMUcitRMLjIFuy76y--RffyMbSCMWOm3zAX--zfAIgEFygLLXO5St8CYSlA==
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | default: &default
2 | adapter: postgresql
3 | encoding: unicode
4 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
5 |
6 |
7 | development:
8 | <<: *default
9 | database: <%= ENV.fetch("DATABASE_NAME") { "fugu_dev" } %>
10 | username: <%= ENV["DATABASE_USER"] %>
11 | password: <%= ENV["DATABASE_PASSWORD"] %>
12 | host: <%= ENV.fetch("DATABASE_HOST") { "localhost" } %>
13 | #port: 5432
14 |
15 | # Warning: The database defined as "test" will be erased and
16 | # re-generated from your development database when you run "rake".
17 | # Do not set this db to the same as development or production.
18 | test:
19 | <<: *default
20 | database: <%= ENV.fetch("DATABASE_NAME_TEST") { "fugu_test" } %>
21 | username: <%= ENV["DATABASE_USER"] %>
22 | password: <%= ENV["DATABASE_PASSWORD"] %>
23 | host: <%= ENV.fetch("DATABASE_HOST") { "localhost" } %>
24 |
25 | production:
26 | <<: *default
27 | url: <%= ENV['DATABASE_URL'] %>
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative "application"
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # In the development environment your application's code is reloaded any time
7 | # it changes. This slows down response time but is perfect for development
8 | # since you don't have to restart the web server when you make code changes.
9 | config.cache_classes = false
10 |
11 | # Do not eager load code on boot.
12 | config.eager_load = false
13 |
14 | # Show full error reports.
15 | config.consider_all_requests_local = true
16 |
17 | # Enable server timing
18 | config.server_timing = true
19 |
20 | # Allow requests from the Gitpod subdomain
21 | config.hosts.clear
22 |
23 | # Enable/disable caching. By default caching is disabled.
24 | # Run rails dev:cache to toggle caching.
25 | if Rails.root.join("tmp/caching-dev.txt").exist?
26 | config.action_controller.perform_caching = true
27 | config.action_controller.enable_fragment_cache_logging = true
28 |
29 | config.cache_store = :memory_store
30 | config.public_file_server.headers = {
31 | "Cache-Control" => "public, max-age=#{2.days.to_i}"
32 | }
33 | else
34 | config.action_controller.perform_caching = false
35 |
36 | config.cache_store = :null_store
37 | end
38 |
39 | # Store uploaded files on the local file system (see config/storage.yml for options).
40 | config.active_storage.service = :local
41 |
42 | # Don't care if the mailer can't send.
43 | config.action_mailer.raise_delivery_errors = false
44 |
45 | config.action_mailer.perform_caching = false
46 |
47 | config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
48 |
49 | # Print deprecation notices to the Rails logger.
50 | config.active_support.deprecation = :log
51 |
52 | # Raise exceptions for disallowed deprecations.
53 | config.active_support.disallowed_deprecation = :raise
54 |
55 | # Tell Active Support which deprecation messages to disallow.
56 | config.active_support.disallowed_deprecation_warnings = []
57 |
58 | # Raise an error on page load if there are pending migrations.
59 | config.active_record.migration_error = :page_load
60 |
61 | # Highlight code that triggered database queries in logs.
62 | config.active_record.verbose_query_logs = true
63 |
64 | # Suppress logger output for asset requests.
65 | config.assets.quiet = true
66 |
67 | # Raises error for missing translations.
68 | # config.i18n.raise_on_missing_translations = true
69 |
70 | # Annotate rendered view with file names.
71 | # config.action_view.annotate_rendered_view_with_filenames = true
72 |
73 | # Uncomment if you wish to allow Action Cable access from any origin.
74 | # config.action_cable.disable_request_forgery_protection = true
75 | end
76 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | Rails.application.configure do
4 | # Settings specified here will take precedence over those in config/application.rb.
5 |
6 | # Code is not reloaded between requests.
7 | config.cache_classes = true
8 |
9 | # Eager load code on boot. This eager loads most of Rails and
10 | # your application in memory, allowingserve_static_files both threaded web servers
11 | # and those relying on copy on write to perform better.
12 | # Rake tasks automatically ignore this option for performance.
13 | config.eager_load = true
14 |
15 | # Full error reports are disabled and caching is turned on.
16 | config.consider_all_requests_local = false
17 | config.action_controller.perform_caching = true
18 |
19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
21 | # config.require_master_key = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
26 |
27 | # Compress CSS using a preprocessor.
28 | # config.assets.css_compressor = :sass
29 |
30 | # Do not fallback to assets pipeline if a precompiled asset is missed.
31 | config.assets.compile = false
32 |
33 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
34 | # config.asset_host = "http://assets.example.com"
35 |
36 | # Specifies the header that your server uses for sending files.
37 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
38 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
39 |
40 | # Store uploaded files on the local file system (see config/storage.yml for options).
41 | config.active_storage.service = :local
42 |
43 | # Mount Action Cable outside main process or domain.
44 | # config.action_cable.mount_path = nil
45 | # config.action_cable.url = "wss://example.com/cable"
46 | # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
47 |
48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
49 | config.force_ssl = ENV.fetch("FORCE_SSL_PROD", true)
50 |
51 | config.ssl_options = {hsts: {subdomains: true, preload: true, expires: 1.year}}
52 |
53 | # Include generic and useful information about system operation, but avoid logging too much
54 | # information to avoid inadvertent exposure of personally identifiable information (PII).
55 | config.log_level = :info
56 |
57 | # Prepend all log lines with the following tags.
58 | config.log_tags = [ :request_id ]
59 |
60 | # Use a different cache store in production.
61 | # config.cache_store = :mem_cache_store
62 |
63 | # Use a real queuing backend for Active Job (and separate queues per environment).
64 | # config.active_job.queue_adapter = :resque
65 | # config.active_job.queue_name_prefix = "eventtracker_production"
66 |
67 | config.action_mailer.perform_caching = false
68 |
69 | config.action_mailer.default_url_options = { host: ENV["APP_DOMAIN"] }
70 |
71 | # Ignore bad email addresses and do not raise email delivery errors.
72 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
73 | # config.action_mailer.raise_delivery_errors = false
74 |
75 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
76 | # the I18n.default_locale when a translation cannot be found).
77 | config.i18n.fallbacks = true
78 |
79 | # Don't log any deprecations.
80 | config.active_support.report_deprecations = false
81 |
82 | # Use default logging formatter so that PID and timestamp are not suppressed.
83 | config.log_formatter = ::Logger::Formatter.new
84 |
85 | # Use a different logger for distributed setups.
86 | # require "syslog/logger"
87 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
88 |
89 | if ENV["RAILS_LOG_TO_STDOUT"].present?
90 | logger = ActiveSupport::Logger.new(STDOUT)
91 | logger.formatter = config.log_formatter
92 | config.logger = ActiveSupport::TaggedLogging.new(logger)
93 | end
94 |
95 | # Do not dump schema after migrations.
96 | config.active_record.dump_schema_after_migration = false
97 | end
98 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | require "active_support/core_ext/integer/time"
2 |
3 | # The test environment is used exclusively to run your application's
4 | # test suite. You never need to work with it otherwise. Remember that
5 | # your test database is "scratch space" for the test suite and is wiped
6 | # and recreated between test runs. Don't rely on the data there!
7 |
8 | Rails.application.configure do
9 | # Settings specified here will take precedence over those in config/application.rb.
10 |
11 | # Turn false under Spring and add config.action_view.cache_template_loading = true
12 | config.cache_classes = true
13 |
14 | # Eager loading loads your whole application. When running a single test locally,
15 | # this probably isn't necessary. It's a good idea to do in a continuous integration
16 | # system, or in some way before deploying your code.
17 | config.eager_load = ENV["CI"].present?
18 |
19 | # Configure public file server for tests with Cache-Control for performance.
20 | config.public_file_server.enabled = true
21 | config.public_file_server.headers = {
22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}"
23 | }
24 |
25 | # Show full error reports and disable caching.
26 | config.consider_all_requests_local = true
27 | config.action_controller.perform_caching = false
28 | config.cache_store = :null_store
29 |
30 | # Raise exceptions instead of rendering exception templates.
31 | config.action_dispatch.show_exceptions = false
32 |
33 | # Disable request forgery protection in test environment.
34 | config.action_controller.allow_forgery_protection = false
35 |
36 | # Store uploaded files on the local file system in a temporary directory.
37 | config.active_storage.service = :test
38 |
39 | config.action_mailer.perform_caching = false
40 | config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
41 |
42 | # Tell Action Mailer not to deliver emails to the real world.
43 | # The :test delivery method accumulates sent emails in the
44 | # ActionMailer::Base.deliveries array.
45 | config.action_mailer.delivery_method = :test
46 |
47 | # Print deprecation notices to the stderr.
48 | config.active_support.deprecation = :stderr
49 |
50 | # Raise exceptions for disallowed deprecations.
51 | config.active_support.disallowed_deprecation = :raise
52 |
53 | # Tell Active Support which deprecation messages to disallow.
54 | config.active_support.disallowed_deprecation_warnings = []
55 |
56 | # Raises error for missing translations.
57 | # config.i18n.raise_on_missing_translations = true
58 |
59 | # Annotate rendered view with file names.
60 | # config.action_view.annotate_rendered_view_with_filenames = true
61 | end
62 |
--------------------------------------------------------------------------------
/config/importmap.rb:
--------------------------------------------------------------------------------
1 | # Use direct uploads for Active Storage (remember to import "@rails/activestorage" in your application.js)
2 | # pin "@rails/activestorage", to: "activestorage.esm.js"
3 |
4 | # Use node modules from a JavaScript CDN by running ./bin/importmap
5 |
6 | pin "application"
7 |
8 | pin "chart.js", to: "https://ga.jspm.io/npm:chart.js@3.7.0/dist/chart.esm.js"
9 | pin "chartjs-plugin-datalabels", to: "https://ga.jspm.io/npm:chartjs-plugin-datalabels@2.0.0/dist/chartjs-plugin-datalabels.esm.js"
10 | pin "chart.js/helpers", to: "https://ga.jspm.io/npm:chart.js@3.7.0/helpers/helpers.esm.js"
11 | pin "@hotwired/stimulus", to: "stimulus.js"
12 | pin "@hotwired/stimulus-importmap-autoloader", to: "stimulus-importmap-autoloader.js"
13 |
14 | pin_all_from "app/javascript/controllers", under: "controllers"
15 | pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
16 |
17 | pin "chart_helpers", to: "helpers/chart_helpers.js", preload: true
18 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ActiveSupport::Reloader.to_prepare do
4 | # ApplicationController.renderer.defaults.merge!(
5 | # http_host: 'example.org',
6 | # https: false
7 | # )
8 | # end
9 |
--------------------------------------------------------------------------------
/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = "1.0"
5 |
6 | # Add additional assets to the asset load path.
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in the app/assets
11 | # folder are already added.
12 | # Rails.application.config.assets.precompile += %w( admin.js admin.css )
13 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code
7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'".
8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"]
9 |
--------------------------------------------------------------------------------
/config/initializers/content_security_policy.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Define an application-wide content security policy
4 | # For further information see the following documentation
5 | # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
6 |
7 | # Rails.application.configure do
8 | # config.content_security_policy do |policy|
9 | # policy.default_src :self, :https
10 | # policy.font_src :self, :https, :data
11 | # policy.img_src :self, :https, :data
12 | # policy.object_src :none
13 | # policy.script_src :self, :https
14 | # policy.style_src :self, :https
15 | # # Specify URI for violation reports
16 | # # policy.report_uri "/csp-violation-report-endpoint"
17 | # end
18 | #
19 | # # Generate session nonces for permitted importmap and inline scripts
20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
21 | # config.content_security_policy_nonce_directives = %w(script-src)
22 | #
23 | # # Report CSP violations to a specified URI. See:
24 | # # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
25 | # # config.content_security_policy_report_only = true
26 | # end
27 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [
5 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn,
6 | :api_key, :name, :properties, :email
7 | ]
8 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, "\\1en"
8 | # inflect.singular /^(ox)en/i, "\\1"
9 | # inflect.irregular "person", "people"
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym "RESTful"
16 | # end
17 |
--------------------------------------------------------------------------------
/config/initializers/logstop.rb:
--------------------------------------------------------------------------------
1 | Logstop.guard(Rails.logger, ip: true)
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/config/initializers/permissions_policy.rb:
--------------------------------------------------------------------------------
1 | # Define an application-wide HTTP permissions policy. For further
2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy
3 | #
4 | # Rails.application.config.permissions_policy do |f|
5 | # f.camera :none
6 | # f.gyroscope :none
7 | # f.microphone :none
8 | # f.usb :none
9 | # f.fullscreen :self
10 | # f.payment :self, "https://secure.example.com"
11 | # end
12 |
--------------------------------------------------------------------------------
/config/initializers/sentry.rb:
--------------------------------------------------------------------------------
1 | Sentry.init do |config|
2 | config.dsn = ENV["SENTRY_DSN"]
3 | config.enabled_environments = %w[production]
4 | config.breadcrumbs_logger = [:active_support_logger, :http_logger]
5 |
6 | # Set tracesSampleRate to 1.0 to capture 100%
7 | # of transactions for performance monitoring.
8 | # We recommend adjusting this value in production
9 | config.traces_sample_rate = 0.5
10 | # or
11 | config.traces_sampler = lambda do |context|
12 | true
13 | end
14 | end
--------------------------------------------------------------------------------
/config/initializers/stripe.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Stripe.api_key = ENV["STRIPE_SECRET_KEY"]
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/config/locales/devise.en.yml:
--------------------------------------------------------------------------------
1 | # Additional translations at https://github.com/heartcombo/devise/wiki/I18n
2 |
3 | en:
4 | devise:
5 | confirmations:
6 | confirmed: "Your email address has been successfully confirmed."
7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
9 | failure:
10 | already_authenticated: "You are already signed in."
11 | inactive: "Your account is not activated yet."
12 | invalid: "Invalid %{authentication_keys} or password."
13 | locked: "Your account is locked."
14 | last_attempt: "You have one more attempt before your account is locked."
15 | not_found_in_database: "Invalid %{authentication_keys} or password."
16 | timeout: "Your session expired. Please sign in again to continue."
17 | unauthenticated: "You need to sign in or sign up before continuing."
18 | unconfirmed: "You have to confirm your email address before continuing."
19 | mailer:
20 | confirmation_instructions:
21 | subject: "Confirmation instructions"
22 | reset_password_instructions:
23 | subject: "Reset password instructions"
24 | unlock_instructions:
25 | subject: "Unlock instructions"
26 | email_changed:
27 | subject: "Email Changed"
28 | password_change:
29 | subject: "Password Changed"
30 | omniauth_callbacks:
31 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
32 | success: "Successfully authenticated from %{kind} account."
33 | passwords:
34 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
35 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
36 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
37 | updated: "Your password has been changed successfully. You are now signed in."
38 | updated_not_active: "Your password has been changed successfully."
39 | registrations:
40 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
41 | signed_up: "Welcome! You have signed up successfully."
42 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
43 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
44 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
45 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address."
46 | updated: "Your account has been updated successfully."
47 | updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again."
48 | sessions:
49 | signed_in: "Signed in successfully."
50 | signed_out: "Signed out successfully."
51 | already_signed_out: "Signed out successfully."
52 | unlocks:
53 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
54 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
55 | unlocked: "Your account has been unlocked successfully. Please sign in to continue."
56 | errors:
57 | messages:
58 | already_confirmed: "was already confirmed, please try signing in"
59 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
60 | expired: "has expired, please request a new one"
61 | not_found: "not found"
62 | not_locked: "was not locked"
63 | not_saved:
64 | one: "1 error prohibited this %{resource} from being saved:"
65 | other: "%{count} errors prohibited this %{resource} from being saved:"
66 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at https://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
9 | threads min_threads_count, max_threads_count
10 |
11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before
12 | # terminating a worker in development environments.
13 | #
14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
15 |
16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
17 | #
18 | port ENV.fetch("PORT") { 3000 }
19 |
20 | # Specifies the `environment` that Puma will run in.
21 | #
22 | environment ENV.fetch("RAILS_ENV") { "development" }
23 |
24 | # Specifies the `pidfile` that Puma will use.
25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
26 |
27 | # Specifies the number of `workers` to boot in clustered mode.
28 | # Workers are forked web server processes. If using threads and workers together
29 | # the concurrency of the application would be max `threads` * `workers`.
30 | # Workers do not work on JRuby or Windows (both of which do not support
31 | # processes).
32 | #
33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
34 |
35 | # Use the `preload_app!` method when specifying a `workers` number.
36 | # This directive tells Puma to first boot the application and load code
37 | # before forking the application. This takes advantage of Copy On Write
38 | # process behavior so workers use less memory.
39 | #
40 | # preload_app!
41 |
42 | # Allow puma to be restarted by `rails restart` command.
43 | plugin :tmp_restart
44 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | Rails.application.routes.draw do
4 | root to: "projects#index"
5 |
6 | scope "/:user_id", as: :user do
7 | resources :projects, only: %i[index new edit update create destroy], param: :slug do
8 | resources :events, only: %i[index show], param: :slug
9 | resources :funnels, param: :slug
10 | get "settings", to: "projects#settings"
11 | end
12 | end
13 |
14 | scope module: "users" do
15 | get "settings/", to: "settings#show", as: :users_settings
16 | end
17 |
18 | scope "stripe/" do
19 | get "/checkout_session/", to: "stripe#checkout_session", as: "stripe_checkout_session"
20 | get "/success_callback/", to: "stripe#success_callback", as: "stripe_success_callback"
21 | get "/customer_portal/", to: "stripe#customer_portal", as: "stripe_customer_portal"
22 | post "/webhooks/", to: "stripe#webhooks", as: "stripe_webhooks"
23 | end
24 |
25 | devise_for :users, controllers: {
26 | sessions: "users/sessions",
27 | registrations: "users/registrations",
28 | passwords: "users/passwords"
29 | }
30 |
31 | namespace :api do
32 | namespace :v1 do
33 | resources :events, only: %i[create]
34 | end
35 | end
36 |
37 | if Rails.env.development?
38 | namespace :development do
39 | resources :embed_mock, only: [:index]
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | Spring.watch(
2 | ".ruby-version",
3 | ".rbenv-vars",
4 | "tmp/restart.txt",
5 | "tmp/caching-dev.txt"
6 | )
7 |
--------------------------------------------------------------------------------
/config/storage.yml:
--------------------------------------------------------------------------------
1 | test:
2 | service: Disk
3 | root: <%= Rails.root.join("tmp/storage") %>
4 |
5 | local:
6 | service: Disk
7 | root: <%= Rails.root.join("storage") %>
8 |
9 | # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
10 | # amazon:
11 | # service: S3
12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
14 | # region: us-east-1
15 | # bucket: your_own_bucket
16 |
17 | # Remember not to checkin your GCS keyfile to a repository
18 | # google:
19 | # service: GCS
20 | # project: your_project
21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
22 | # bucket: your_own_bucket
23 |
24 | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
25 | # microsoft:
26 | # service: AzureStorage
27 | # storage_account_name: your_account_name
28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
29 | # container: your_container_name
30 |
31 | # mirror:
32 | # service: Mirror
33 | # primary: local
34 | # mirrors: [ amazon, google, microsoft ]
35 |
--------------------------------------------------------------------------------
/config/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme')
2 |
3 | module.exports = {
4 | darkMode: 'media',
5 | content: [
6 | './app/helpers/**/*.rb',
7 | './app/javascript/**/*.js',
8 | './app/views/**/*'
9 | ],
10 | theme: {
11 | extend: {
12 | fontFamily: {
13 | sans: ['Inter var', ...defaultTheme.fontFamily.sans],
14 | },
15 | },
16 | },
17 | plugins: [
18 | require('@tailwindcss/forms'),
19 | require('@tailwindcss/aspect-ratio'),
20 | require('@tailwindcss/typography'),
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/db/migrate/20210512145433_create_events.rb:
--------------------------------------------------------------------------------
1 | class CreateEvents < ActiveRecord::Migration[6.1]
2 | def change
3 | create_table :events do |t|
4 | t.string :name
5 | t.json :properties
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20210512164800_create_projects.rb:
--------------------------------------------------------------------------------
1 | class CreateProjects < ActiveRecord::Migration[6.1]
2 | def change
3 | create_table :projects do |t|
4 | t.string :name
5 |
6 | t.timestamps
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/db/migrate/20210617151514_add_project_reference_to_events.rb:
--------------------------------------------------------------------------------
1 | class AddProjectReferenceToEvents < ActiveRecord::Migration[6.1]
2 | def change
3 | add_reference :events, :project, index: true, foreign_key: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20210617152330_create_api_keys.rb:
--------------------------------------------------------------------------------
1 | class CreateApiKeys < ActiveRecord::Migration[6.1]
2 | def change
3 | create_table :api_keys do |t|
4 | t.string :key_value
5 | t.references :project, null: false, foreign_key: true
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20210617152542_add_not_null_constraint_to_events.rb:
--------------------------------------------------------------------------------
1 | class AddNotNullConstraintToEvents < ActiveRecord::Migration[6.1]
2 | def change
3 | change_column_null :events, :project_id, false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20210617154111_add_more_not_null_constraints.rb:
--------------------------------------------------------------------------------
1 | class AddMoreNotNullConstraints < ActiveRecord::Migration[6.1]
2 | def change
3 | change_column_null :events, :name, false
4 | change_column_null :api_keys, :key_value, false
5 | change_column_null :projects, :name, false
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20210617235020_add_dev_to_events.rb:
--------------------------------------------------------------------------------
1 | class AddDevToEvents < ActiveRecord::Migration[6.1]
2 | def change
3 | add_column :events, :staging, :boolean, default: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20210618081647_add_unique_index_to_api_key.rb:
--------------------------------------------------------------------------------
1 | class AddUniqueIndexToApiKey < ActiveRecord::Migration[6.1]
2 | def change
3 | add_index :api_keys, :key_value, unique: true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20210729144755_move_staging_to_api_key.rb:
--------------------------------------------------------------------------------
1 | class MoveStagingToApiKey < ActiveRecord::Migration[6.1]
2 | def change
3 | remove_column :events, :staging, :boolean, default: false
4 |
5 | add_column :api_keys, :test, :boolean, default: false
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20210729145810_move_reference_to_api_key.rb:
--------------------------------------------------------------------------------
1 | class MoveReferenceToApiKey < ActiveRecord::Migration[6.1]
2 | def change
3 | remove_reference :events, :project, index: true, foreign_key: true
4 |
5 | add_reference :events, :api_key, index: true, foreign_key: true, null: false
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/db/migrate/20210730092821_devise_create_users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DeviseCreateUsers < ActiveRecord::Migration[6.1]
4 | def change
5 | create_table :users do |t|
6 | ## Database authenticatable
7 | t.string :email, null: false, default: ""
8 | t.string :encrypted_password, null: false, default: ""
9 |
10 | ## Recoverable
11 | t.string :reset_password_token
12 | t.datetime :reset_password_sent_at
13 |
14 | ## Rememberable
15 | t.datetime :remember_created_at
16 |
17 | ## Trackable
18 | # t.integer :sign_in_count, default: 0, null: false
19 | # t.datetime :current_sign_in_at
20 | # t.datetime :last_sign_in_at
21 | # t.string :current_sign_in_ip
22 | # t.string :last_sign_in_ip
23 |
24 | ## Confirmable
25 | t.string :confirmation_token
26 | t.datetime :confirmed_at
27 | t.datetime :confirmation_sent_at
28 | t.string :unconfirmed_email # Only if using reconfirmable
29 |
30 | ## Lockable
31 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
32 | # t.string :unlock_token # Only if unlock strategy is :email or :both
33 | # t.datetime :locked_at
34 |
35 |
36 | t.timestamps null: false
37 | end
38 |
39 | add_index :users, :email, unique: true
40 | add_index :users, :reset_password_token, unique: true
41 | add_index :users, :confirmation_token, unique: true
42 | # add_index :users, :unlock_token, unique: true
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/db/migrate/20210730101807_add_user_reference_to_project.rb:
--------------------------------------------------------------------------------
1 | class AddUserReferenceToProject < ActiveRecord::Migration[6.1]
2 | def change
3 | add_reference :projects, :user, index: true, foreign_key: true, null: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20210922065433_change_event_properties_json_to_json_b.rb:
--------------------------------------------------------------------------------
1 | class ChangeEventPropertiesJsonToJsonB < ActiveRecord::Migration[6.1]
2 | def change
3 | change_column :events, :properties, :jsonb
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20211016133119_add_stripe_and_active_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddStripeAndActiveToUser < ActiveRecord::Migration[6.1]
2 | def change
3 | add_column :users, :stripe_customer_id, :string
4 | add_column :users, :status, :integer, default: 0, null: false
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20211227170605_create_funnels.rb:
--------------------------------------------------------------------------------
1 | class CreateFunnels < ActiveRecord::Migration[7.0]
2 | def change
3 | create_table :funnels do |t|
4 | t.string :name, null: false
5 | t.references :api_key, null: false, foreign_key: true
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20211227171015_create_funnel_steps.rb:
--------------------------------------------------------------------------------
1 | class CreateFunnelSteps < ActiveRecord::Migration[7.0]
2 | def change
3 | create_table :funnel_steps do |t|
4 | t.string :event_name, null: false
5 | t.references :funnel, null: false, foreign_key: true
6 | t.integer :order
7 |
8 | t.timestamps
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/db/migrate/20220216191234_add_unique_index.rb:
--------------------------------------------------------------------------------
1 | class AddUniqueIndex < ActiveRecord::Migration[7.0]
2 | def change
3 | add_index :funnels, %i[name api_key_id], unique: true
4 | add_index :projects, %i[name user_id], unique: true
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20220314175006_add_hash_id_to_user.rb:
--------------------------------------------------------------------------------
1 | class AddHashIdToUser < ActiveRecord::Migration[7.0]
2 | def change
3 | add_column :users, :hash_id, :string
4 | add_index :users, :hash_id, unique: true
5 |
6 | Rake::Task["fugu:migration:generate_hash_ids"].invoke
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/db/migrate/20220315181147_add_public_bool_to_projects.rb:
--------------------------------------------------------------------------------
1 | class AddPublicBoolToProjects < ActiveRecord::Migration[7.0]
2 | def change
3 | add_column :projects, :public, :boolean, default: false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20220320112745_user_hash_id_null_false.rb:
--------------------------------------------------------------------------------
1 | class UserHashIdNullFalse < ActiveRecord::Migration[7.0]
2 | def change
3 | change_column_null :users, :hash_id, false
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # This file is the source Rails uses to define your schema when running `bin/rails
6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7 | # be faster and is potentially less error prone than running all of your
8 | # migrations from scratch. Old migrations may fail to apply correctly if those
9 | # migrations use external dependencies or application code.
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema[7.0].define(version: 2022_03_20_112745) do
14 | # These are extensions that must be enabled in order to support this database
15 | enable_extension "plpgsql"
16 |
17 | create_table "api_keys", force: :cascade do |t|
18 | t.string "key_value", null: false
19 | t.bigint "project_id", null: false
20 | t.datetime "created_at", null: false
21 | t.datetime "updated_at", null: false
22 | t.boolean "test", default: false
23 | t.index ["key_value"], name: "index_api_keys_on_key_value", unique: true
24 | t.index ["project_id"], name: "index_api_keys_on_project_id"
25 | end
26 |
27 | create_table "events", force: :cascade do |t|
28 | t.string "name", null: false
29 | t.jsonb "properties"
30 | t.datetime "created_at", null: false
31 | t.datetime "updated_at", null: false
32 | t.bigint "api_key_id", null: false
33 | t.index ["api_key_id"], name: "index_events_on_api_key_id"
34 | end
35 |
36 | create_table "funnel_steps", force: :cascade do |t|
37 | t.string "event_name", null: false
38 | t.bigint "funnel_id", null: false
39 | t.integer "order"
40 | t.datetime "created_at", null: false
41 | t.datetime "updated_at", null: false
42 | t.index ["funnel_id"], name: "index_funnel_steps_on_funnel_id"
43 | end
44 |
45 | create_table "funnels", force: :cascade do |t|
46 | t.string "name", null: false
47 | t.bigint "api_key_id", null: false
48 | t.datetime "created_at", null: false
49 | t.datetime "updated_at", null: false
50 | t.index ["api_key_id"], name: "index_funnels_on_api_key_id"
51 | t.index ["name", "api_key_id"], name: "index_funnels_on_name_and_api_key_id", unique: true
52 | end
53 |
54 | create_table "projects", force: :cascade do |t|
55 | t.string "name", null: false
56 | t.datetime "created_at", null: false
57 | t.datetime "updated_at", null: false
58 | t.bigint "user_id", null: false
59 | t.boolean "public", default: false
60 | t.index ["name", "user_id"], name: "index_projects_on_name_and_user_id", unique: true
61 | t.index ["user_id"], name: "index_projects_on_user_id"
62 | end
63 |
64 | create_table "users", force: :cascade do |t|
65 | t.string "email", default: "", null: false
66 | t.string "encrypted_password", default: "", null: false
67 | t.string "reset_password_token"
68 | t.datetime "reset_password_sent_at", precision: nil
69 | t.datetime "remember_created_at", precision: nil
70 | t.string "confirmation_token"
71 | t.datetime "confirmed_at", precision: nil
72 | t.datetime "confirmation_sent_at", precision: nil
73 | t.string "unconfirmed_email"
74 | t.datetime "created_at", null: false
75 | t.datetime "updated_at", null: false
76 | t.string "stripe_customer_id"
77 | t.integer "status", default: 0, null: false
78 | t.string "hash_id", null: false
79 | t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
80 | t.index ["email"], name: "index_users_on_email", unique: true
81 | t.index ["hash_id"], name: "index_users_on_hash_id", unique: true
82 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
83 | end
84 |
85 | add_foreign_key "api_keys", "projects"
86 | add_foreign_key "events", "api_keys"
87 | add_foreign_key "funnel_steps", "funnels"
88 | add_foreign_key "funnels", "api_keys"
89 | add_foreign_key "projects", "users"
90 | end
91 |
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 | services:
3 | db:
4 | image: postgres
5 | volumes:
6 | - ./tmp/db:/var/lib/postgresql/data
7 | ports:
8 | - "5432:5432"
9 | restart: unless-stopped
10 | environment:
11 | - POSTGRES_PASSWORD=${DATABASE_PASSWORD}
12 | - POSTGRES_USER=${DATABASE_USER}
13 | - POSTGRES_DB=${DATABASE_NAME}
14 | web:
15 | build:
16 | context: .
17 | dockerfile: Dockerfile.dev
18 | entrypoint: ["sh", "/app/docker-entrypoint.sh"]
19 | command: bash -c "rm -f tmp/pids/server.pid && bundle exec foreman start -f Procfile.docker.dev"
20 | tty: true
21 | working_dir: /app
22 | ports:
23 | - "3000:3000"
24 | depends_on:
25 | - db
26 | environment:
27 | - RAILS_ENV=development
28 | - RACK_ENV=development
29 | - DATABASE_PASSWORD=${DATABASE_PASSWORD}
30 | - DATABASE_USER=${DATABASE_USER}
31 | - DATABASE_HOST=db
32 | - DATABASE_NAME=${DATABASE_NAME}
33 | - DATABASE_NAME_TEST=${DATABASE_NAME}
34 | - SENTRY_DSN=${SENTRY_DSN}
35 | - ALLOW_REGISTRATION=${ALLOW_REGISTRATION}
36 |
--------------------------------------------------------------------------------
/docker-compose.prod-remote.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 | services:
3 | db:
4 | image: postgres
5 | volumes:
6 | - ./tmp/db:/var/lib/postgresql/data
7 | ports:
8 | - "5432:5432"
9 | restart: unless-stopped
10 | environment:
11 | - POSTGRES_PASSWORD=${DATABASE_PASSWORD}
12 | - POSTGRES_USER=${DATABASE_USER}
13 | - POSTGRES_DB=${DATABASE_NAME}
14 | web:
15 | image: shafyy/fugu:latest
16 | entrypoint: ["sh", "/app/docker-entrypoint.sh"]
17 | command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
18 | tty: true
19 | working_dir: /app
20 | ports:
21 | - "3000:3000"
22 | depends_on:
23 | - db
24 | restart: unless-stopped
25 | environment:
26 | - RAILS_ENV=production
27 | - RACK_ENV=production
28 | - RAILS_SERVE_STATIC_FILES=true
29 | - DATABASE_NAME=${DATABASE_NAME}
30 | - DATABASE_URL=postgres://${DATABASE_USER}:${DATABASE_PASSWORD}@db:5432/${DATABASE_NAME}
31 | - SECRET_KEY_BASE=${SECRET_KEY_BASE}
32 | - RAILS_LOG_TO_STDOUT=${RAILS_LOG_TO_STDOUT}
33 | - SENTRY_DSN=${SENTRY_DSN}
34 | - ALLOW_REGISTRATION=${ALLOW_REGISTRATION}
35 | - INIT_USER_EMAIL=${INIT_USER_EMAIL}
36 | - INIT_USER_PASSWORD=${INIT_USER_PASSWORD}
37 | - FORCE_SSL_PROD=${FORCE_SSL_PROD}
38 |
--------------------------------------------------------------------------------
/docker-compose.prod.yml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 | services:
3 | db:
4 | image: postgres
5 | volumes:
6 | - ./tmp/db:/var/lib/postgresql/data
7 | ports:
8 | - "5432:5432"
9 | restart: unless-stopped
10 | environment:
11 | - POSTGRES_PASSWORD=${DATABASE_PASSWORD}
12 | - POSTGRES_USER=${DATABASE_USER}
13 | - POSTGRES_DB=${DATABASE_NAME}
14 | web:
15 | build:
16 | context: .
17 | dockerfile: Dockerfile.prod
18 | #image: fugu_image:latest
19 | entrypoint: ["sh", "/app/docker-entrypoint.sh"]
20 | command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
21 | tty: true
22 | working_dir: /app
23 | ports:
24 | - "3000:3000"
25 | depends_on:
26 | - db
27 | restart: unless-stopped
28 | environment:
29 | - RAILS_ENV=production
30 | - RACK_ENV=production
31 | - RAILS_SERVE_STATIC_FILES=true
32 | - DATABASE_NAME=${DATABASE_NAME}
33 | - DATABASE_URL=postgres://${DATABASE_USER}:${DATABASE_PASSWORD}@db:5432/${DATABASE_NAME}
34 | - SECRET_KEY_BASE=${SECRET_KEY_BASE}
35 | - RAILS_LOG_TO_STDOUT=${RAILS_LOG_TO_STDOUT}
36 | - SENTRY_DSN=${SENTRY_DSN}
37 | - ALLOW_REGISTRATION=${ALLOW_REGISTRATION}
38 | - INIT_USER_EMAIL=${INIT_USER_EMAIL}
39 | - INIT_USER_PASSWORD=${INIT_USER_PASSWORD}
40 | - FORCE_SSL_PROD=${FORCE_SSL_PROD}
41 |
--------------------------------------------------------------------------------
/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # create database if it doesn't exist
5 | rails db:create
6 |
7 | # run migrations if necessary
8 | rails db:migrate
9 |
10 | # create initial user if env vars are set and user doesn't exist
11 | if [ -n "${INIT_USER_EMAIL}" ] && [ -n "${INIT_USER_PASSWORD}" ] && [ "$(rails runner "puts User.exists?(email: '${INIT_USER_EMAIL}')")" = 'false' ]
12 | then
13 | echo "Creating initial user..."
14 | rails runner "User.create!({email: '${INIT_USER_EMAIL}', password: '${INIT_USER_PASSWORD}', password_confirmation: '${INIT_USER_PASSWORD}', status: 'active' })"
15 | fi
16 |
17 | exec "$@"
18 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/lib/assets/.keep
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/lib/tasks/.keep
--------------------------------------------------------------------------------
/lib/tasks/auto_annotate_models.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # NOTE: only doing this in development as some production environments (Heroku)
4 | # NOTE: are sensitive to local FS writes, and besides -- it's just not proper
5 | # NOTE: to have a dev-mode tool do its thing in production.
6 | # rubocop:disable Metrics/BlockLength
7 | if Rails.env.development?
8 | require "annotate"
9 | task set_annotation_options: :environment do
10 | # You can override any of these by setting an environment variable of the
11 | # same name.
12 | Annotate.set_defaults(
13 | "active_admin" => "false",
14 | "additional_file_patterns" => [],
15 | "routes" => "false",
16 | "models" => "true",
17 | "position_in_routes" => "before",
18 | "position_in_class" => "before",
19 | "position_in_test" => "before",
20 | "position_in_fixture" => "before",
21 | "position_in_factory" => "before",
22 | "position_in_serializer" => "before",
23 | "show_foreign_keys" => "true",
24 | "show_complete_foreign_keys" => "false",
25 | "show_indexes" => "true",
26 | "simple_indexes" => "false",
27 | "model_dir" => "app/models",
28 | "root_dir" => "",
29 | "include_version" => "false",
30 | "require" => "",
31 | "exclude_tests" => "false",
32 | "exclude_fixtures" => "false",
33 | "exclude_factories" => "false",
34 | "exclude_serializers" => "false",
35 | "exclude_scaffolds" => "true",
36 | "exclude_controllers" => "true",
37 | "exclude_helpers" => "true",
38 | "exclude_sti_subclasses" => "false",
39 | "ignore_model_sub_dir" => "false",
40 | "ignore_columns" => nil,
41 | "ignore_routes" => nil,
42 | "ignore_unknown_models" => "false",
43 | "hide_limit_column_types" => "integer,bigint,boolean",
44 | "hide_default_column_types" => "json,jsonb,hstore",
45 | "skip_on_db_migrate" => "false",
46 | "format_bare" => "true",
47 | "format_rdoc" => "false",
48 | "format_yard" => "false",
49 | "format_markdown" => "false",
50 | "sort" => "false",
51 | "force" => "false",
52 | "frozen" => "false",
53 | "classified_sort" => "true",
54 | "trace" => "false",
55 | "wrapper_open" => nil,
56 | "wrapper_close" => nil,
57 | "with_comment" => "true"
58 | )
59 | end
60 | # rubocop:enable Metrics/BlockLength
61 | Annotate.load_tasks
62 | end
63 |
--------------------------------------------------------------------------------
/lib/tasks/fugu_migration_tasks.rake:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | namespace :fugu do
4 | namespace :migration do
5 | desc "Generate hash_id for Users who don't have one (< schema version 20220314175006)"
6 | # run automatically by 20220314175006 migration code
7 | task generate_hash_ids: :environment do
8 | User.find_each do |user|
9 | user.add_hash_id if user.hash_id.blank?
10 | user.save
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/log/.keep
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/public/favicon.ico
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
--------------------------------------------------------------------------------
/storage/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/storage/.keep
--------------------------------------------------------------------------------
/test/application_system_test_case.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
6 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
7 | end
8 |
--------------------------------------------------------------------------------
/test/channels/application_cable/connection_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | module ApplicationCable
6 | class ConnectionTest < ActionCable::Connection::TestCase
7 | # test "connects with cookies" do
8 | # cookies.signed[:user_id] = 42
9 | #
10 | # connect
11 | #
12 | # assert_equal connection.user_id, "42"
13 | # end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/test/controllers/.keep
--------------------------------------------------------------------------------
/test/controllers/api/v1/events_controller_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class EventsControllerTest < ActionDispatch::IntegrationTest
6 | class PostCreate < EventsControllerTest
7 | setup do
8 | @user = FactoryBot.create(:user)
9 | project = FactoryBot.create(:project, user: @user)
10 | @api_key_test = FactoryBot.create(:api_key, project: project, test: true)
11 | @api_key_live = FactoryBot.create(:api_key, project: project, test: false)
12 |
13 | @name = "Test Event"
14 | @properties = %({
15 | "color": "Blue",
16 | "size": "12"
17 | })
18 |
19 | @event_params = {
20 | api_key: @api_key_live.key_value,
21 | name: @name,
22 | properties: @properties
23 | }
24 | end
25 |
26 | test "POST create should respond with success" do
27 | post api_v1_events_path, params: @event_params
28 | assert_response :success
29 | end
30 |
31 | test "POST create should add event" do
32 | assert_difference("Event.count") do
33 | post api_v1_events_path, params: @event_params
34 | end
35 | end
36 |
37 | test "POST create should create correct event" do
38 | post api_v1_events_path, params: @event_params
39 | assert_not_empty(Event.where(name: @name))
40 | assert_not_empty(Event.where("properties->>'color' = 'Blue'"))
41 | end
42 |
43 | test "POST create should respond with ArgumentError for name = nil" do
44 | @event_params[:name] = nil
45 | post api_v1_events_path, params: @event_params
46 | assert_response 422
47 | assert_match("'name' can't be nil", @response.body)
48 | end
49 |
50 | test "POST create should respond with ArgumentError for missing name key" do
51 | @event_params.delete(:name)
52 | post api_v1_events_path, params: @event_params
53 | assert_response 422
54 | assert_match("missing 'name' key", @response.body)
55 | end
56 |
57 | test "POST create should respond with JSON error" do
58 | @event_params[:properties] = "{incorrect_json: hehe}"
59 | post api_v1_events_path, params: @event_params
60 | assert_response 422
61 | assert_match("Properties must be valid JSON", @response.body)
62 | end
63 |
64 | test "POST create should not create event for inactive user" do
65 | @user.update(status: "inactive")
66 | assert_no_difference "Event.count" do
67 | post api_v1_events_path, params: @event_params
68 | end
69 | end
70 |
71 | test "POST create should return correct ArgumentError for inactive user" do
72 | @user.update(status: "inactive")
73 | post api_v1_events_path, params: @event_params
74 | assert_response 422
75 | assert_match("You need an active subscription", @response.body)
76 | end
77 |
78 | test "POST create should be successful for inactive user if self-hosted" do
79 | ENV["FUGU_CLOUD"] = "false"
80 | @user.update(status: "inactive")
81 | assert_difference "Event.count" do
82 | post api_v1_events_path, params: @event_params
83 | end
84 | ENV["FUGU_CLOUD"] = "true"
85 | end
86 |
87 | test "POST create should add event with test api key for inactive user" do
88 | @user.update(status: "inactive")
89 | @event_params[:api_key] = @api_key_test.key_value
90 | assert_difference("Event.count") do
91 | post api_v1_events_path, params: @event_params
92 | end
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/test/controllers/projects_controller_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class ProjectsControllerTest < ActionDispatch::IntegrationTest
6 | def basic_setup
7 | @user = FactoryBot.create(:user)
8 | sign_in @user
9 | end
10 |
11 | def create_project
12 | @project = FactoryBot.create(:project, user: @user)
13 | @api_key_test = FactoryBot.create(:api_key, project: @project, test: true)
14 | @api_key_live = FactoryBot.create(:api_key, project: @project, test: false)
15 | end
16 |
17 | class GetIndex < ProjectsControllerTest
18 | setup do
19 | basic_setup
20 | create_project
21 | get user_projects_path(@user.hash_id)
22 | end
23 |
24 | test "is succesful" do
25 | assert :success
26 | end
27 |
28 | test "contains created project" do
29 | assert_match(@project.name, @response.body)
30 | end
31 | end
32 |
33 | class PostCreate < ProjectsControllerTest
34 | setup do
35 | basic_setup
36 | @project_attributes = {
37 | name: "test-project"
38 | }
39 | end
40 |
41 | test "is succesful" do
42 | post user_projects_path(@user.hash_id), params: { project: @project_attributes }
43 | assert_redirected_to user_project_events_path(@user.hash_id,
44 | @project_attributes[:name].parameterize)
45 | end
46 |
47 | test "contains created project" do
48 | post user_projects_path(@user.hash_id), params: { project: @project_attributes }
49 | assert_not_empty(Project.where(name: @project_attributes[:name]))
50 | end
51 |
52 | test "is not succesful" do
53 | @project_attributes[:name] = "Name With Space"
54 | post user_projects_path(@user.hash_id), params: { project: @project_attributes }
55 | assert :unprocessable_entity
56 | end
57 | end
58 |
59 | class PatchUpdate < ProjectsControllerTest
60 | setup do
61 | basic_setup
62 | create_project
63 | @new_project_attr = {
64 | name: "new-name",
65 | public: true,
66 | project_id: @project.id
67 | }
68 | end
69 |
70 | test "is succesful" do
71 | patch user_project_path(@user.hash_id, @project.name), params: { project: @new_project_attr }
72 | assert_redirected_to user_project_settings_path(@user.hash_id,
73 | @new_project_attr[:name].parameterize)
74 | end
75 |
76 | test "not succesful with invalid name" do
77 | @new_project_attr[:name] = "Nope Nope$"
78 | patch user_project_path(@user.hash_id, @project.name), params: { project: @new_project_attr }
79 | assert :unprocessable_entity
80 | end
81 |
82 | test "contains updated project" do
83 | patch user_project_path(@user.hash_id, @project.name), params: { project: @new_project_attr }
84 | assert_not_empty(
85 | Project.where(name: @new_project_attr[:name], public: @new_project_attr[:public])
86 | )
87 | end
88 | end
89 | end
90 |
--------------------------------------------------------------------------------
/test/controllers/stripe_controller_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 | require "stripe_mock"
5 |
6 | class StripeControllerTest < ActionDispatch::IntegrationTest
7 | class PostWebhooksUpdateCancel < StripeControllerTest
8 | # customer cancels subscription for end of billing cycle
9 | setup do
10 | @user = FactoryBot.create(:user, status: "active", stripe_customer_id: "abc1")
11 | event = customer_subscription_updated(true, @user.stripe_customer_id)
12 | post stripe_webhooks_path, params: event, headers: stripe_headers(event), as: :json
13 | end
14 |
15 | test "be successful" do
16 | assert_response :success
17 | end
18 |
19 | test "sets account to canceled" do
20 | assert_equal(User.find(@user.id).status, "canceled")
21 | end
22 | end
23 |
24 | class PostWebhooksUpdateCanceled < StripeControllerTest
25 | # subscription is canceled at the end of billing cycle
26 | setup do
27 | @user = FactoryBot.create(:user, status: "canceled", stripe_customer_id: "abc1")
28 | event = customer_subscription_deleted(@user.stripe_customer_id)
29 | post stripe_webhooks_path, params: event, headers: stripe_headers(event), as: :json
30 | end
31 |
32 | test "be successful" do
33 | assert_response :success
34 | end
35 |
36 | test "sets account to inactive" do
37 | assert_equal(User.find(@user.id).status, "inactive")
38 | end
39 | end
40 |
41 | class PostWebhooksReactivates < StripeControllerTest
42 | # customer reactivates previously canceled subscription before end of billing cycle
43 | setup do
44 | @user = FactoryBot.create(:user, status: "canceled", stripe_customer_id: "abc1")
45 | event = customer_subscription_updated(false, @user.stripe_customer_id)
46 | post stripe_webhooks_path, params: event, headers: stripe_headers(event), as: :json
47 | end
48 |
49 | test "be successful" do
50 | assert_response :success
51 | end
52 |
53 | test "sets account to active" do
54 | assert_equal(User.find(@user.id).status, "active")
55 | end
56 | end
57 |
58 | class GetSuccessCallback < StripeControllerTest
59 | # customer reactivates previously canceled subscription before end of billing cycle
60 | setup do
61 | StripeMock.start
62 | customer = create_stripe_customer
63 | product = create_stripe_product
64 | price = create_stripe_price(product.id)
65 | session = create_stripe_checkout_session(customer.id, price.id)
66 | @user = FactoryBot.create(:user, status: "inactive", stripe_customer_id: customer.id)
67 |
68 | sign_in @user
69 | get stripe_success_callback_path, params: { session_id: session.id }
70 | end
71 |
72 | teardown { StripeMock.stop }
73 |
74 | test "be successful" do
75 | assert_response :redirect
76 | end
77 |
78 | test "sets account to active" do
79 | assert_equal(User.find(@user.id).status, "active")
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/test/controllers/users/registrations_controller_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class RegistrationsControllerTest < ActionDispatch::IntegrationTest
6 | class CreateUser < RegistrationsControllerTest
7 | setup do
8 | @user = FactoryBot.build(:user)
9 | @user_params = {
10 | email: @user.email,
11 | password: @user.password,
12 | password_confirmation: @user.password
13 | }
14 |
15 | post user_registration_path, params: { user: @user_params }
16 | end
17 |
18 | test "to redirect to projects overview" do
19 | assert_redirected_to root_path
20 | end
21 |
22 | test "creates the correct user" do
23 | assert_not_empty(User.where(email: @user.email))
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/test/controllers/users/settings_controller_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "test_helper"
4 |
5 | class SettingsControllerTest < ActionDispatch::IntegrationTest
6 | def setup
7 | FactoryBot.create(:project, user: @user)
8 | sign_in @user
9 | get users_settings_path
10 | end
11 |
12 | class GetShowInactiveUser < SettingsControllerTest
13 | setup do
14 | @user = FactoryBot.create(:user, status: "inactive")
15 | setup
16 | end
17 |
18 | test "be successful" do
19 | assert_response :success
20 | end
21 |
22 | test "display correct email" do
23 | assert_match(@user.email, @response.body)
24 | end
25 |
26 | test "display correct text" do
27 | assert_match("You don't have an active subscription", @response.body)
28 | end
29 |
30 | test "display correct links" do
31 | assert_match("/stripe/checkout_session", @response.body)
32 | assert_match("/stripe/checkout_session", @response.body)
33 | end
34 | end
35 |
36 | class GetShowActiveUser < SettingsControllerTest
37 | setup do
38 | @user = FactoryBot.create(:user, status: "active")
39 | setup
40 | end
41 |
42 | test "be successful" do
43 | assert_response :success
44 | end
45 |
46 | test "display correct text" do
47 | assert_match("Your Fugu subscription is active and renews monthly", @response.body)
48 | end
49 |
50 | test "display correct link" do
51 | assert_match("/stripe/customer_portal", @response.body)
52 | end
53 | end
54 |
55 | class GetShowCanceledUser < SettingsControllerTest
56 | setup do
57 | @user = FactoryBot.create(:user, status: "canceled")
58 | setup
59 | end
60 |
61 | test "be successful" do
62 | assert_response :success
63 | end
64 |
65 | test "display correct text" do
66 | assert_match("Your Fugu subscription is canceled", @response.body)
67 | end
68 |
69 | test "display correct link" do
70 | assert_match("/stripe/customer_portal", @response.body)
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/test/factories/api_keys.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: api_keys
6 | #
7 | # id :bigint not null, primary key
8 | # key_value :string not null
9 | # test :boolean default(FALSE)
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | # project_id :bigint not null
13 | #
14 | # Indexes
15 | #
16 | # index_api_keys_on_key_value (key_value) UNIQUE
17 | # index_api_keys_on_project_id (project_id)
18 | #
19 | # Foreign Keys
20 | #
21 | # fk_rails_... (project_id => projects.id)
22 | #
23 |
24 | FactoryBot.define do
25 | factory :api_key do
26 | sequence :key_value do |n|
27 | "12345678kjdhfdsa-#{n}"
28 | end
29 |
30 | project
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/factories/events.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: events
6 | #
7 | # id :bigint not null, primary key
8 | # name :string not null
9 | # properties :jsonb
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | # api_key_id :bigint not null
13 | #
14 | # Indexes
15 | #
16 | # index_events_on_api_key_id (api_key_id)
17 | #
18 | # Foreign Keys
19 | #
20 | # fk_rails_... (api_key_id => api_keys.id)
21 | #
22 |
23 | FactoryBot.define do
24 | factory :event do
25 | sequence :name do |n|
26 | "Test Event #{n}"
27 | end
28 |
29 | properties do
30 | %({
31 | "color": "Blue",
32 | "size": "12"
33 | })
34 | end
35 |
36 | api_key
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/test/factories/funnel_steps.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: funnel_steps
6 | #
7 | # id :bigint not null, primary key
8 | # event_name :string not null
9 | # order :integer
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | # funnel_id :bigint not null
13 | #
14 | # Indexes
15 | #
16 | # index_funnel_steps_on_funnel_id (funnel_id)
17 | #
18 | # Foreign Keys
19 | #
20 | # fk_rails_... (funnel_id => funnels.id)
21 | #
22 |
23 | FactoryBot.define do
24 | factory :funnel_step do
25 | sequence :event_name do |n|
26 | "Test Event #{n}"
27 | end
28 |
29 | sequence :order do |n|
30 | n
31 | end
32 |
33 | funnel
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/test/factories/funnels.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: funnels
6 | #
7 | # id :bigint not null, primary key
8 | # name :string not null
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | # api_key_id :bigint not null
12 | #
13 | # Indexes
14 | #
15 | # index_funnels_on_api_key_id (api_key_id)
16 | # index_funnels_on_name_and_api_key_id (name,api_key_id) UNIQUE
17 | #
18 | # Foreign Keys
19 | #
20 | # fk_rails_... (api_key_id => api_keys.id)
21 | #
22 |
23 | FactoryBot.define do
24 | factory :funnel do
25 | sequence :name do |n|
26 | "My Funnel #{n}"
27 | end
28 | api_key
29 |
30 | before(:create) do |funnel|
31 | funnel.funnel_steps = build_list(:funnel_step, 5, funnel: funnel)
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/test/factories/projects.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: projects
6 | #
7 | # id :bigint not null, primary key
8 | # name :string not null
9 | # public :boolean default(FALSE)
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | # user_id :bigint not null
13 | #
14 | # Indexes
15 | #
16 | # index_projects_on_name_and_user_id (name,user_id) UNIQUE
17 | # index_projects_on_user_id (user_id)
18 | #
19 | # Foreign Keys
20 | #
21 | # fk_rails_... (user_id => users.id)
22 | #
23 |
24 | FactoryBot.define do
25 | factory :project do
26 | sequence :name do |n|
27 | "test-project-#{n}"
28 | end
29 |
30 | public { false }
31 |
32 | user
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/test/factories/users.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: users
6 | #
7 | # id :bigint not null, primary key
8 | # confirmation_sent_at :datetime
9 | # confirmation_token :string
10 | # confirmed_at :datetime
11 | # email :string default(""), not null
12 | # encrypted_password :string default(""), not null
13 | # remember_created_at :datetime
14 | # reset_password_sent_at :datetime
15 | # reset_password_token :string
16 | # status :integer default("inactive"), not null
17 | # unconfirmed_email :string
18 | # created_at :datetime not null
19 | # updated_at :datetime not null
20 | # hash_id :string not null
21 | # stripe_customer_id :string
22 | #
23 | # Indexes
24 | #
25 | # index_users_on_confirmation_token (confirmation_token) UNIQUE
26 | # index_users_on_email (email) UNIQUE
27 | # index_users_on_hash_id (hash_id) UNIQUE
28 | # index_users_on_reset_password_token (reset_password_token) UNIQUE
29 | #
30 |
31 | FactoryBot.define do
32 | factory :user do
33 | sequence :email do |n|
34 | "#{n}@fugu.lol"
35 | end
36 |
37 | sequence :hash_id do |n|
38 | "abc#{n}"
39 | end
40 |
41 | password { "secure_password" }
42 |
43 | status { "active" }
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/test/helpers/.keep
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/test/integration/.keep
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/test/mailers/.keep
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/test/models/.keep
--------------------------------------------------------------------------------
/test/models/api_key_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: api_keys
6 | #
7 | # id :bigint not null, primary key
8 | # key_value :string not null
9 | # test :boolean default(FALSE)
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | # project_id :bigint not null
13 | #
14 | # Indexes
15 | #
16 | # index_api_keys_on_key_value (key_value) UNIQUE
17 | # index_api_keys_on_project_id (project_id)
18 | #
19 | # Foreign Keys
20 | #
21 | # fk_rails_... (project_id => projects.id)
22 | #
23 |
24 | require "test_helper"
25 |
26 | class ApiKeyTest < ActiveSupport::TestCase
27 | test "has a valid factory" do
28 | assert build(:api_key)
29 | end
30 |
31 | context "validations" do
32 | subject { build(:api_key) }
33 |
34 | should belong_to(:project)
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/test/models/event_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: events
6 | #
7 | # id :bigint not null, primary key
8 | # name :string not null
9 | # properties :jsonb
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | # api_key_id :bigint not null
13 | #
14 | # Indexes
15 | #
16 | # index_events_on_api_key_id (api_key_id)
17 | #
18 | # Foreign Keys
19 | #
20 | # fk_rails_... (api_key_id => api_keys.id)
21 | #
22 |
23 | require "test_helper"
24 |
25 | class EventTest < ActiveSupport::TestCase
26 | test "has a valid factory" do
27 | assert build(:event)
28 | end
29 |
30 | context "validations" do
31 | subject { build(:event) }
32 |
33 | should validate_presence_of(:name)
34 |
35 | should belong_to(:api_key)
36 |
37 | should_not allow_value("a-", "a$", "a_").for(:name)
38 | should validate_length_of(:name).is_at_most(25)
39 | should validate_exclusion_of(:name)
40 | .in_array(%w[all])
41 | .with_message("'all' is a reserved event name by Fugu and can't be used")
42 | should validate_exclusion_of(:name)
43 | .in_array(%w[All])
44 | .with_message("'All' is a reserved event name by Fugu and can't be used")
45 | end
46 |
47 | context "property validations general" do
48 | should_not allow_value("Non JSON String").for(:properties)
49 | should_not allow_value("{invalid_json: \"12\"}").for(:properties)
50 | should_not allow_value("{\"all\": \"test\"}")
51 | .for(:properties)
52 | .with_message("You've used a property name that's prohibited by Fugu (such as 'all'). " \
53 | "Learn more about property constraints in the Fugu docs: https://docs.fugu.lol")
54 | should_not allow_value("{\"All\": \"test\"}")
55 | .for(:properties)
56 | .with_message("You've used a property name that's prohibited by Fugu (such as 'all'). " \
57 | "Learn more about property constraints in the Fugu docs: https://docs.fugu.lol")
58 | should_not allow_value("{\"email\": \"test\"}")
59 | .for(:properties)
60 | .with_message("You've used a property name that's prohibited by Fugu (such as 'all'). " \
61 | "Learn more about property constraints in the Fugu docs: https://docs.fugu.lol")
62 | should_not allow_value("{\"Test\": \"192.128.0.12\"}")
63 | .for(:properties)
64 | .with_message("You've used a property value that's prohibited by Fugu " \
65 | "(such as an email address). " \
66 | "Learn more about property constraints in the Fugu docs: https://docs.fugu.lol")
67 | should_not allow_value("{\"Test\": \"2345:0425:2CA1:0000:0000:0567:5673:23b5\"}")
68 | .for(:properties)
69 | .with_message("You've used a property value that's prohibited by Fugu " \
70 | "(such as an email address). " \
71 | "Learn more about property constraints in the Fugu docs: https://docs.fugu.lol")
72 | should_not allow_value("{\"this_propertyname_is_too_long\": \"test\"}")
73 | .for(:properties)
74 | .with_message("You've used a property name that's too long (> 15 characters). " \
75 | "Please choose a shorter name.")
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/test/models/funnel_step_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: funnel_steps
6 | #
7 | # id :bigint not null, primary key
8 | # event_name :string not null
9 | # order :integer
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | # funnel_id :bigint not null
13 | #
14 | # Indexes
15 | #
16 | # index_funnel_steps_on_funnel_id (funnel_id)
17 | #
18 | # Foreign Keys
19 | #
20 | # fk_rails_... (funnel_id => funnels.id)
21 | #
22 |
23 | require "test_helper"
24 |
25 | class FunnelStepTest < ActiveSupport::TestCase
26 | test "has a valid factory" do
27 | assert build(:funnel_step)
28 | end
29 |
30 | context "validations" do
31 | subject { build(:funnel_step) }
32 |
33 | should validate_presence_of(:event_name)
34 |
35 | should belong_to(:funnel)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/test/models/funnel_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: funnels
6 | #
7 | # id :bigint not null, primary key
8 | # name :string not null
9 | # created_at :datetime not null
10 | # updated_at :datetime not null
11 | # api_key_id :bigint not null
12 | #
13 | # Indexes
14 | #
15 | # index_funnels_on_api_key_id (api_key_id)
16 | # index_funnels_on_name_and_api_key_id (name,api_key_id) UNIQUE
17 | #
18 | # Foreign Keys
19 | #
20 | # fk_rails_... (api_key_id => api_keys.id)
21 | #
22 |
23 | require "test_helper"
24 |
25 | class FunnelTest < ActiveSupport::TestCase
26 | test "has a valid factory" do
27 | assert build(:funnel)
28 | end
29 |
30 | context "validations" do
31 | subject { build(:funnel) }
32 |
33 | should validate_presence_of(:name)
34 |
35 | should validate_uniqueness_of(:name)
36 | .scoped_to(:api_key_id)
37 | .with_message("You already have a funnel with this name")
38 | .case_insensitive
39 |
40 | should belong_to(:api_key)
41 |
42 | should_not allow_value("My $$$").for(:name)
43 |
44 | should accept_nested_attributes_for(:funnel_steps)
45 |
46 | should have_many(:funnel_steps)
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/test/models/project_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: projects
6 | #
7 | # id :bigint not null, primary key
8 | # name :string not null
9 | # public :boolean default(FALSE)
10 | # created_at :datetime not null
11 | # updated_at :datetime not null
12 | # user_id :bigint not null
13 | #
14 | # Indexes
15 | #
16 | # index_projects_on_name_and_user_id (name,user_id) UNIQUE
17 | # index_projects_on_user_id (user_id)
18 | #
19 | # Foreign Keys
20 | #
21 | # fk_rails_... (user_id => users.id)
22 | #
23 |
24 | require "test_helper"
25 |
26 | class ProjectTest < ActiveSupport::TestCase
27 | test "has a valid factory" do
28 | assert build(:project)
29 | end
30 |
31 | context "validations" do
32 | subject { build(:project) }
33 |
34 | should validate_presence_of(:name)
35 |
36 | should validate_uniqueness_of(:name)
37 | .scoped_to(:user_id)
38 | .with_message("You already have a project with this name")
39 | .case_insensitive
40 |
41 | should validate_length_of(:name).is_at_most(40)
42 |
43 | should belong_to(:user)
44 |
45 | should_not allow_value("Test Project").for(:name)
46 | should_not allow_values("project", "projects", "Project").for(:name)
47 | end
48 |
49 | test ".creat_api_keys should create api keys" do
50 | project = create(:project)
51 | project.create_api_keys
52 |
53 | assert_not_nil(ApiKey.where(project: project).first)
54 | assert_not_nil(ApiKey.where(project: project).second)
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/test/models/user_test.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | # == Schema Information
4 | #
5 | # Table name: users
6 | #
7 | # id :bigint not null, primary key
8 | # confirmation_sent_at :datetime
9 | # confirmation_token :string
10 | # confirmed_at :datetime
11 | # email :string default(""), not null
12 | # encrypted_password :string default(""), not null
13 | # remember_created_at :datetime
14 | # reset_password_sent_at :datetime
15 | # reset_password_token :string
16 | # status :integer default("inactive"), not null
17 | # unconfirmed_email :string
18 | # created_at :datetime not null
19 | # updated_at :datetime not null
20 | # hash_id :string not null
21 | # stripe_customer_id :string
22 | #
23 | # Indexes
24 | #
25 | # index_users_on_confirmation_token (confirmation_token) UNIQUE
26 | # index_users_on_email (email) UNIQUE
27 | # index_users_on_hash_id (hash_id) UNIQUE
28 | # index_users_on_reset_password_token (reset_password_token) UNIQUE
29 | #
30 |
31 | require "test_helper"
32 |
33 | class UserTest < ActiveSupport::TestCase
34 | test "has a valid factory" do
35 | assert build(:user)
36 | end
37 |
38 | should validate_presence_of(:email)
39 | end
40 |
--------------------------------------------------------------------------------
/test/support/stripe_test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module StripeTestHelper
4 | def stripe_event_signature(event_json)
5 | secret = ENV.fetch("STRIPE_ENDPOINT_SECRET", nil)
6 | timestamp = Time.zone.now
7 | signature = Stripe::Webhook::Signature.send(:compute_signature, timestamp, event_json, secret)
8 | scheme = Stripe::Webhook::Signature::EXPECTED_SCHEME
9 | "t=#{timestamp.to_i},#{scheme}=#{signature}"
10 | end
11 |
12 | def customer_subscription_updated(cancel_at_period_end, customer)
13 | {
14 | type: "customer.subscription.updated",
15 | data: {
16 | object: {
17 | cancel_at_period_end: cancel_at_period_end,
18 | customer: customer
19 | }
20 | }
21 | }
22 | end
23 |
24 | def customer_subscription_deleted(customer)
25 | {
26 | type: "customer.subscription.deleted",
27 | data: {
28 | object: {
29 | customer: customer
30 | }
31 | }
32 | }
33 | end
34 |
35 | def stripe_headers(event)
36 | {
37 | "Stripe-Signature" => stripe_event_signature(event.to_json)
38 | }
39 | end
40 |
41 | def checkout_session_args(price, customer)
42 | {
43 | payment_method_types: ["card"],
44 | customer: customer,
45 | line_items: [{
46 | price: price,
47 | quantity: 1
48 | }],
49 | mode: "subscription",
50 | success_url: "/",
51 | cancel_url: "/"
52 | }
53 | end
54 |
55 | def create_stripe_customer
56 | Stripe::Customer.create(source: StripeMock.generate_card_token(last4: "9191", exp_year: 2030))
57 | end
58 |
59 | def create_stripe_product
60 | Stripe::Product.create({ name: "Fugu" })
61 | end
62 |
63 | def create_stripe_price(product)
64 | Stripe::Price.create({
65 | unit_amount: 900,
66 | currency: "usd",
67 | recurring: { interval: "month" },
68 | product: product
69 | })
70 | end
71 |
72 | def create_stripe_checkout_session(customer, price)
73 | Stripe::Checkout::Session.create(
74 | payment_method_types: ["card"],
75 | customer: customer,
76 | line_items: [{
77 | price: price,
78 | quantity: 1
79 | }],
80 | mode: "subscription",
81 | success_url: "/",
82 | cancel_url: "/"
83 | )
84 | end
85 | end
86 |
--------------------------------------------------------------------------------
/test/system/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/test/system/.keep
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | ENV["RAILS_ENV"] ||= "test"
4 | require_relative "../config/environment"
5 | require "rails/test_help"
6 | require "database_cleaner/active_record"
7 |
8 | Dir[Rails.root.join("test/support/**/*.rb")].each { |f| require f }
9 |
10 | module ActiveSupport
11 | class TestCase
12 | # Run tests in parallel with specified workers
13 | parallelize(workers: :number_of_processors)
14 |
15 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
16 | # fixtures :all
17 |
18 | # Add more helper methods to be used by all tests here...
19 | include FactoryBot::Syntax::Methods
20 | end
21 | end
22 |
23 | module ActionDispatch
24 | class IntegrationTest
25 | include Devise::Test::IntegrationHelpers
26 | include StripeTestHelper
27 | end
28 | end
29 |
30 | # Shoulda Matchers
31 | Shoulda::Matchers.configure do |config|
32 | config.integrate do |with|
33 | with.test_framework :minitest
34 | with.library :rails
35 | end
36 | end
37 |
38 | # Database Cleaner
39 | DatabaseCleaner.strategy = :transaction
40 |
41 | module AroundEachTest
42 | def before_setup
43 | super
44 | DatabaseCleaner.start
45 | end
46 |
47 | def after_teardown
48 | super
49 | DatabaseCleaner.clean
50 | end
51 | end
52 |
53 | module Minitest
54 | class Test
55 | include AroundEachTest
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/tmp/.keep
--------------------------------------------------------------------------------
/vendor/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shafy/fugu/610c06390f650d485b389b4d375354b9a943ce57/vendor/.keep
--------------------------------------------------------------------------------