Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .kamal/secrets
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,10 @@ POSTGRES_PASSWORD=$NATIVEAPPTEMPLATEAPI_POSTGRES_PASSWORD
# Kamal fails the deploy if either is missing or not a valid PEM.
NATIVEAPPTEMPLATEAPI_ORIGIN_CERT=$(cat $HOME/.config/nativeapptemplateapi/origin-cert.pem)
NATIVEAPPTEMPLATEAPI_ORIGIN_KEY=$(cat $HOME/.config/nativeapptemplateapi/origin-key.pem)

# Sentry DSN (config/initializers/sentry.rb). Read from an app-specific variable
# like the Postgres password, so it can't collide with another app's DSN in the
# same shell. Unlike the secrets above it is optional: unset, the container gets
# an empty SENTRY_DSN and Sentry stays off.
# export NATIVEAPPTEMPLATEAPI_SENTRY_DSN=https://<key>@<org>.ingest.sentry.io/<project>
SENTRY_DSN=$NATIVEAPPTEMPLATEAPI_SENTRY_DSN
9 changes: 8 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ Cross-platform push via the `noticed` (v3) and `action_push_native` gems. APNs f
- **Config & secrets**: `config/push.yml` reads everything from credentials (`bin/rails credentials:edit --environment <env>`) under `action_push_native:apns:{key_id, team_id, topic, encryption_key}` and `action_push_native:fcm:{project_id, encryption_key}`. APNs `encryption_key` is the full `.p8` PEM; FCM `encryption_key` is the entire service-account JSON. `topic` stays in credentials (not source) because the agent's rename pipeline would otherwise desync the bundle id.
- **APNs sandbox vs production**: `connect_to_development_server: <%= Rails.env.development? %>`. The token's environment is fixed at iOS build time — Xcode debug builds get **sandbox** tokens, TestFlight/App Store get **production**. They must match the server: test Xcode debug builds against a **local development** server; use TestFlight to test against the deployed server. A mismatch returns APNs `400 BadDeviceToken`, which the gem treats as `TokenError` and **destroys the device row** (the default `rescue_from`). FCM has no such split.

### Error Monitoring
- Optional Sentry (`sentry-ruby` + `sentry-rails`), configured in `config/initializers/sentry.rb` (`NativeapptemplateSentry`). Off unless `SENTRY_DSN` is set; events are sent only from production. README "Error monitoring (Sentry)" has what is sent and how to turn it on. Never commit a DSN: this repo is public.
- Only real bugs: 4xx answers (`rescue_from` handlers, `RecordInvalid`, Pundit denials) are not sent. Add a new expected exception to `NativeapptemplateSentry::EXPECTED_EXCEPTIONS` rather than letting it reach Sentry.
- Personal data stays out: the Sentry user is the shopkeeper's ID only and `account_id` is a tag (`SetCurrentRequestDetails`); `before_send` drops the devise_token_auth headers, masks email addresses and applies `filter_parameters`. Never pass an email or name to `Sentry.set_user`, `set_extras` or `capture_message`.
- `Sentry`, `sentry-ruby`, `sentry-rails`, `SENTRY_DSN` and `sentry/test_helper` are third-party names; the agent's rename must not touch them.

### Testing Strategy
- Minitest for all tests (models, controllers, integration, policies)
- WebMock for stubbing external HTTP requests
Expand All @@ -104,7 +110,7 @@ Cross-platform push via the `noticed` (v3) and `action_push_native` gems. APNs f
- `json_response` for parsing JSON API responses
- `create_new_auth_token` for generating auth headers (Devise Token Auth)
- Fixtures in test/fixtures/ and seed data in db/fixtures/test/
- Run tests: `bin/rails test` (602 tests, 1514 assertions)
- Run tests: `bin/rails test` (614 tests, 1594 assertions)

### Internationalization
- Responses are in English (default) or Japanese, picked by the `SetLocale` concern (included in `ApplicationController` and `NonApiApplicationController`). It wraps `process_action`, not `around_action`, so `rescue_from` handlers and `rate_limit` responses are localized too.
Expand Down Expand Up @@ -133,6 +139,7 @@ Cross-platform push via the `noticed` (v3) and `action_push_native` gems. APNs f
- Deployed with [Kamal 2](https://kamal-deploy.org) (`bin/kamal`): `Dockerfile` builds the image, `config/deploy.yml` describes the server, and `.kamal/secrets` reads secrets from the environment and `config/credentials/production.key`. Images go to GitHub Container Registry (`ghcr.io`).
- One server runs everything: the app (Puma behind Thruster on port 80, fronted by kamal-proxy serving a Cloudflare Origin Certificate with `forward_headers: true` and `ssl_redirect: false`; Cloudflare's ranges are in `config.action_dispatch.trusted_proxies` via `config/cloudflare_ips.yml` so `rate_limit` sees the caller, not the edge), and PostgreSQL as the `db` accessory on the private Docker network (`DB_HOST=<service>-db`; never published to the internet). Solid Queue runs inside Puma via `SOLID_QUEUE_IN_PUMA=true`, so there is no separate job server.
- Everything named `nativeapptemplateapi` in `config/deploy.yml`, `.kamal/secrets` and `config/database.yml` is renamed together by the agent; the accessory's `POSTGRES_USER`/`POSTGRES_DB` must keep matching `database.yml`'s production `username`/`database` (`test/config/deploy_config_test.rb` checks this).
- `SENTRY_DSN` is an optional `env.secret`, read by `.kamal/secrets` from `NATIVEAPPTEMPLATEAPI_SENTRY_DSN`; unset, the container gets an empty value and Sentry stays off.
- The only environments are `development`, `test` and `production`. There is no staging: Render PR previews were the only thing that used it.

## Code Quality Checks Before Committing
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [Unreleased]

- Add optional Sentry error monitoring (`sentry-ruby` / `sentry-rails` 7.1). Off unless `SENTRY_DSN` is set (a Kamal secret read from `NATIVEAPPTEMPLATEAPI_SENTRY_DSN`, optional); sends only from production, only unhandled errors (no 4xx, no tracing, profiling or breadcrumbs), with the shopkeeper's ID as the only user data, the account ID as a tag, devise_token_auth headers dropped, email addresses masked and `filter_parameters` applied. The README's "Error monitoring (Sentry)" covers turning it on, what is sent and the recommended Sentry privacy settings. New tests: `test/config/sentry_test.rb`, `test/integration/sentry_context_test.rb` (#154)
- API change: tell the apps when the organization in the URL is gone. An `/<account uuid>/api/...` request for a deleted account now gets `404 {"code":404,"error_code":"account_not_found","error_message":...}` JSON (was a `302` to `/`), and a signed-in shopkeeper who is no longer a member gets `403 {"code":403,"error_code":"not_account_member","error_message":...}` (was Pundit's generic 401, which the apps treated as a sign-out). Messages are in English and Japanese and follow the usual locale rules. Pundit denials inside an organization the shopkeeper belongs to stay 401; non-API paths still redirect (#153)
- Call a shop 店舗 rather than ショップ in the Japanese messages (model name, item tag's shop attribute, the shop limit error, the sample shop description), matching 店舗名
- API change: add a Japanese locale. A signed-in shopkeeper gets the language in `shopkeepers.locale`, which sign-up now sets from a `locale` param (falling back to `Accept-Language`) and `PATCH /shopkeeper_auth` can change (`en` or `ja`, else 422). Other requests follow the `Accept-Language` header (English by default) via the new `SetLocale` concern. Mails to a shopkeeper use their `locale`; an invitation to an email with no account uses the inviter's. Translations are in `config/locales/ja.yml`, `rails.ja.yml` and `devise.ja.yml`, with no extra gems. Hard-coded English in the error pages, `ErrorsController`, the sample item tag and the mail greetings moved to locale keys. `test/i18n/locale_files_test.rb` fails on any untranslated key. Two English messages changed: an unknown `current_platform` now gets the same "Your input data is wrong." as a blank one, and a duplicate member gets "User is already a member of this organization" instead of "has already been taken". Fix `PATCH /shopkeeper_auth` returning 422 unless `email` was sent: devise_token_auth checked the params before `name`, `time_zone` and `locale` were permitted (#150)
Expand All @@ -27,7 +28,7 @@
- API change: members and invitations must have at least one role (422 "Roles can't be blank"). A role-less member made `GET /permissions` raise (#121)
- API change: password reset and confirmation `redirect_url` must be on the API's own host. Other hosts get 422 for resets and fall back to the default page for confirmations (#120)
- Harden admin sign-in: rate limit by IP and email, `reset_session` on sign-in and sign-out, and Madmin now rejects a deleted admin's session (#119)
- AGENTS.md: correct the CORS and test-worker notes, and keep the test count current (#118, #127, #132, #147, #149, #150, #153)
- AGENTS.md: correct the CORS and test-worker notes, and keep the test count current (#118, #127, #132, #147, #149, #150, #153, #154)
- Clear finished Solid Queue jobs every hour via `config/recurring.yml` (#117)
- API change: `item_tags#update` no longer accepts `state`. Completion goes through the `complete`/`idle` events, and an unknown state no longer returns 500 (#116)
- API change: API errors are always JSON `{code, error_message}`, including missing records (404), missing parameters (400), unknown API routes and 500s; before, they were HTML unless the request sent a JSON Content-Type (#115)
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ gem "valid_email2"
gem "importmap-rails"
gem "tailwindcss-rails", "~> 4.0"
gem "resend"
# Error monitoring. Initialised only when SENTRY_DSN is set (config/initializers/sentry.rb)
gem "sentry-ruby", "~> 7.1"
gem "sentry-rails", "~> 7.1"

# Push notifications via APNs (iOS) and FCM (Android). Provider integration
# (apnotic + googleauth + initializer with credentials) lands in a follow-up
Expand Down
9 changes: 9 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,13 @@ GEM
seed-fu (2.3.9)
activerecord (>= 3.1)
activesupport (>= 3.1)
sentry-rails (7.1.0)
railties (>= 5.2.0)
sentry-ruby (~> 7.1.0)
sentry-ruby (7.1.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
logger
signet (0.22.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
Expand Down Expand Up @@ -535,6 +542,8 @@ DEPENDENCIES
rubocop-rails-omakase
ruby-vips
seed-fu (~> 2.3)
sentry-rails (~> 7.1)
sentry-ruby (~> 7.1)
solid_cable
solid_cache
solid_queue
Expand Down
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Want a customized backend generated for you? [nativeapptemplate-agent](https://g
- **[pundit](https://github.com/varvet/pundit)**
- **[acts_as_tenant](https://github.com/ErwinM/acts_as_tenant)**
- **[noticed](https://github.com/excid3/noticed)** + **[action_push_native](https://github.com/basecamp/action_push_native)** (push notifications)
- **[sentry-ruby](https://github.com/getsentry/sentry-ruby)** (optional error monitoring, off unless `SENTRY_DSN` is set)
- **Test** (Minitest)

### Included Features
Expand All @@ -42,6 +43,7 @@ Want a customized backend generated for you? [nativeapptemplate-agent](https://g
- Force Privacy Policy Version Update
- Force Terms of Use Version Update
- Push Notifications (APNs for iOS, FCM for Android) — paid clients only
- Optional Error Monitoring with Sentry (no personal data sent)
- And more!

## Related Repositories
Expand Down Expand Up @@ -104,7 +106,7 @@ This starts up Overmind running the processes defined in `Procfile.dev`. We've c
The API deploys with [Kamal 2](https://kamal-deploy.org) to any Linux server with SSH access: `Dockerfile` builds the image, `config/deploy.yml` describes the server, and images are pushed to GitHub Container Registry (ghcr.io). PostgreSQL runs on the same server as a Kamal accessory, and Solid Queue runs inside Puma, so one small VPS is enough. The server sits behind Cloudflare: the zone's SSL/TLS mode is "Full (strict)", kamal-proxy serves a Cloudflare Origin Certificate, and the server firewall admits port 443 from [Cloudflare's ranges](https://www.cloudflare.com/ips/) only. (Without Cloudflare, switch `proxy` in `config/deploy.yml` to the Let's Encrypt variant shown in its comment.)

1. Edit `config/deploy.yml`: the server IP (`servers` and `accessories.db.host`), `image` and `registry.username` (your GitHub user or org), and `proxy.host` (also `app.domain` in `config/settings.yml`).
2. Export the secrets `.kamal/secrets` reads: `KAMAL_REGISTRY_PASSWORD` (a GitHub token with `write:packages`) and `NATIVEAPPTEMPLATEAPI_POSTGRES_PASSWORD`. `RAILS_MASTER_KEY` comes from `config/credentials/production.key`.
2. Export the secrets `.kamal/secrets` reads: `KAMAL_REGISTRY_PASSWORD` (a GitHub token with `write:packages`) and `NATIVEAPPTEMPLATEAPI_POSTGRES_PASSWORD`. `RAILS_MASTER_KEY` comes from `config/credentials/production.key`. Optionally `NATIVEAPPTEMPLATEAPI_SENTRY_DSN` (see [Error monitoring (Sentry)](#error-monitoring-sentry)).
3. Create an Origin Certificate in the Cloudflare dashboard (SSL/TLS → Origin Server) and save both halves outside the repo. Copying from the dashboard can lose the line breaks, so rewrap while saving:

```bash
Expand All @@ -128,6 +130,51 @@ bin/kamal setup

Later deploys are `bin/kamal deploy`; `bin/kamal console`, `bin/kamal logs` and `bin/kamal dbc` are defined as aliases.

## Error monitoring (Sentry)

The API can report unexpected errors to [Sentry](https://sentry.io) through `sentry-ruby` and `sentry-rails` (`config/initializers/sentry.rb`). It is optional and off by default.

### Turning it on

Sentry is off unless `SENTRY_DSN` is set. Without it the SDK is never initialised and the app behaves exactly as if the gems were not there, which is how development and test run.

In production the DSN is a Kamal secret: `config/deploy.yml` lists `SENTRY_DSN` under `env.secret`, and `.kamal/secrets` reads it from `NATIVEAPPTEMPLATEAPI_SENTRY_DSN` on the deploying machine. To turn it on:

1. Create a Ruby on Rails project in Sentry and copy its DSN (Project Settings → Client Keys).
2. Export it where you export the other deploy secrets: `export NATIVEAPPTEMPLATEAPI_SENTRY_DSN=https://...`. Keep it out of the repo.
3. `bin/kamal deploy`.

To turn it off again, unset the variable and redeploy: the container gets an empty `SENTRY_DSN`. Without Kamal, set `SENTRY_DSN` in the app's environment however your platform does it.

Events are sent only from the `production` environment; any other environment with a DSN initialises the SDK but sends nothing. The release is the deployed git SHA (`KAMAL_VERSION`, set by Kamal in the container).

### What is sent

Sentry's free plan allows 5,000 errors a month, so only real bugs are sent: unhandled exceptions in requests (500s) and in Active Job / Solid Queue jobs (covered by sentry-rails). Not sent:

- Anything the API answers with a 4xx through `rescue_from` in `Api::V1::Shopkeeper::BaseController` (not found, Pundit denials, missing parameters, failed destroys), and the `account_not_found` / `not_account_member` organization errors.
- Exceptions Rails turns into 4xx responses: sentry-rails' defaults (`ActiveRecord::RecordNotFound`, `ActionController::RoutingError`, `ActionController::ParameterMissing`, ...) plus `ActiveRecord::RecordInvalid`, `ActiveRecord::RecordNotDestroyed` and `Pundit::NotAuthorizedError` (`NativeapptemplateSentry::EXPECTED_EXCEPTIONS`; add your own expected exceptions there).
- Errors the code rescues and logs itself, such as a failure to create a new shop's sample item tag.
- Performance traces, profiles and breadcrumbs (all disabled).

### Privacy

- The user on an event is the shopkeeper's ID only, and the event is tagged with the current account's ID (`SetCurrentRequestDetails`). No email, name or IP address.
- `send_default_pii` is off: request bodies, query strings, cookies and the client IP are not collected.
- `before_send` drops the `devise_token_auth` headers (`Authorization`, `access-token`, `client`, `uid`, `expiry`, `token-type`), replaces anything that looks like an email address in exception messages and the URL with `[FILTERED EMAIL]`, and applies `Rails.application.config.filter_parameters` to any request data and extras.
- Never pass an email or a name to `Sentry.set_user`, `Sentry.set_extras` or `Sentry.capture_message`.

`test/config/sentry_test.rb` and `test/integration/sentry_context_test.rb` cover these rules.

### Recommended Sentry settings

Sentry still derives the user's IP address and location from the connection on its side. Turn that off in **Organization Settings → Security & Privacy**:

- Enable **Prevent Storing of IP Addresses**.
- Under **Advanced Data Scrubbing**, add a rule: Method **Remove**, Data Type **Anything**, Source `$user.geo.**`. The IP setting alone does not drop the location Sentry computes from the IP, so this rule is needed as well.

If your users are in the EU, also pick the EU data region when you create the Sentry organization.

## Contributing

Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on reporting issues, proposing changes, and submitting pull requests.
Expand Down
10 changes: 10 additions & 0 deletions app/controllers/concerns/set_current_request_details.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ module SetCurrentRequestDetails
end

set_current_tenant(Current.account)
set_sentry_context
end
end

private

# IDs only: no email or name goes to Sentry (config/initializers/sentry.rb).
# Both calls are no-ops when Sentry is not initialised.
def set_sentry_context
Sentry.set_user(id: current_shopkeeper.id) if current_shopkeeper
Sentry.set_tags(account_id: Current.account.id) if Current.account
end
end
3 changes: 3 additions & 0 deletions config/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ env:
secret:
- RAILS_MASTER_KEY
- POSTGRES_PASSWORD
# Optional. Empty leaves Sentry off (config/initializers/sentry.rb,
# .kamal/secrets, README "Error monitoring (Sentry)").
- SENTRY_DSN
clear:
# Run the Solid Queue supervisor inside Puma, including the recurring tasks in
# config/recurring.yml. No separate job container or cron.
Expand Down
Loading
Loading