Merge pull request #367 from warmbly/feat/sentry-everywhere

Complete Sentry coverage across every service, optional everywhere, behind one Go wrapper
This commit is contained in:
Matthew Meszaros
2026-09-07 05:25:31 -07:00
committed by GitHub
131 changed files with 2893 additions and 761 deletions
+3
View File
@@ -197,6 +197,9 @@ jobs:
context: ${{ matrix.service == 'tracking' && './tracking' || '.' }}
file: ${{ matrix.service == 'tracking' && './tracking/Dockerfile' || format('deploy/docker/{0}.Dockerfile', matrix.service) }}
platforms: ${{ matrix.platform }}
build-args: |
VERSION=dev-${{ github.sha }}
COMMIT=${{ github.sha }}
cache-from: type=gha,scope=${{ matrix.service }}-${{ steps.prep.outputs.pair }}
cache-to: type=gha,mode=max,scope=${{ matrix.service }}-${{ steps.prep.outputs.pair }}
outputs: type=image,name=${{ env.IMAGE_PREFIX }}/${{ matrix.service }},push-by-digest=true,name-canonical=true,push=true
+13
View File
@@ -106,6 +106,16 @@ jobs:
context: ./${{ matrix.service }}
file: ./${{ matrix.service }}/Dockerfile
push: true
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
SENTRY_ORG=${{ vars.SENTRY_ORG }}
SENTRY_PROJECT=${{ matrix.service == 'admin' && vars.SENTRY_PROJECT_ADMIN || vars.SENTRY_PROJECT_WEB }}
# Source maps are uploaded only when all three are configured on this
# repository. A fork has none of them, the plugin is not in the build,
# and the image is byte-identical to one built with no Sentry account.
secrets: |
sentry_auth_token=${{ secrets.SENTRY_AUTH_TOKEN }}
tags: |
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:${{ github.ref_name }}
${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:v${{ needs.validate-tag.outputs.minor }}
@@ -157,6 +167,9 @@ jobs:
context: ${{ matrix.service == 'tracking' && './tracking' || '.' }}
file: ${{ matrix.service == 'tracking' && './tracking/Dockerfile' || format('deploy/docker/{0}.Dockerfile', matrix.service) }}
platforms: ${{ matrix.platform }}
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
cache-from: type=gha,scope=${{ matrix.service }}-${{ steps.prep.outputs.pair }}
cache-to: type=gha,mode=max,scope=${{ matrix.service }}-${{ steps.prep.outputs.pair }}
outputs: type=image,name=${{ env.IMAGE_PREFIX }}/${{ matrix.service }},push-by-digest=true,name-canonical=true,push=true
-1
View File
@@ -11,7 +11,6 @@
/worker
/backend
/seed
/forms
/migrate
/updater
/warmblyctl
+19 -3
View File
@@ -11,9 +11,25 @@ RUN corepack enable && corepack prepare pnpm@11.9.0 --activate
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
# No VITE_* is baked here on purpose: URLs come from the runtime config below,
# so this one image works for any deployment.
RUN pnpm build
# The build identity, the same VERSION/COMMIT the Go images take. This is the
# one VITE_* that must be baked: it tags every error event with the build, and
# it has to match the release the source maps were uploaded under, which a
# container variable set after the bundle was built could not. Every other
# setting comes from the runtime config below, so one image still serves any
# deployment.
ARG VERSION=""
ARG COMMIT=""
# Source-map upload is optional and off unless CI passes all three. A fork or a
# self-host build sets none of them, needs no Sentry account, and ships no
# source maps.
ARG SENTRY_ORG=""
ARG SENTRY_PROJECT=""
RUN --mount=type=secret,id=sentry_auth_token,required=false \
VITE_SENTRY_RELEASE="${VERSION:-$COMMIT}" \
SENTRY_ORG="$SENTRY_ORG" \
SENTRY_PROJECT="$SENTRY_PROJECT" \
SENTRY_AUTH_TOKEN="$(cat /run/secrets/sentry_auth_token 2>/dev/null || true)" \
pnpm build
# Stage 2: serve the built SPA from nginx with a history fallback. The
# entrypoint renders /config.js from container env at startup.
+14 -4
View File
@@ -3,12 +3,22 @@
# any deployment. Runs before nginx starts (nginx /docker-entrypoint.d hook).
set -eu
# Values are written into JavaScript string literals, so a double quote, a
# backslash or a line break in one would end the literal early and take the
# whole config with it, leaving the app with no API_URL at all. Escape rather
# than trust whatever ended up in .env.
js() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | tr -d '\r\n'
}
cat > /usr/share/nginx/html/config.js <<EOF
window.__WARMBLY_ENV__ = {
API_URL: "${WARMBLY_API_URL:-}",
DASHBOARD_URL: "${WARMBLY_DASHBOARD_URL:-}",
ENV_LABEL: "${WARMBLY_ENV_LABEL:-}",
TURNSTILE_KEY: "${WARMBLY_TURNSTILE_KEY:-}"
API_URL: "$(js "${WARMBLY_API_URL:-}")",
DASHBOARD_URL: "$(js "${WARMBLY_DASHBOARD_URL:-}")",
ENV_LABEL: "$(js "${WARMBLY_ENV_LABEL:-}")",
TURNSTILE_KEY: "$(js "${WARMBLY_TURNSTILE_KEY:-}")",
SENTRY_DSN: "$(js "${WARMBLY_SENTRY_DSN:-}")",
SENTRY_ENVIRONMENT: "$(js "${WARMBLY_SENTRY_ENVIRONMENT:-}")"
};
EOF
+2
View File
@@ -33,6 +33,7 @@
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@sentry/react": "^10.22.0",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-query-devtools": "^5.90.2",
@@ -56,6 +57,7 @@
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@sentry/vite-plugin": "^5.4.0",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
+290
View File
@@ -70,6 +70,9 @@ importers:
'@radix-ui/react-tooltip':
specifier: ^1.2.8
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@sentry/react':
specifier: ^10.22.0
version: 10.73.0(react@19.2.6)
'@tailwindcss/vite':
specifier: ^4.1.18
version: 4.3.0(vite@7.3.6(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0))
@@ -134,6 +137,9 @@ importers:
'@eslint/js':
specifier: ^9.36.0
version: 9.39.4
'@sentry/vite-plugin':
specifier: ^5.4.0
version: 5.4.0(rollup@4.60.4)
'@types/node':
specifier: ^24.6.0
version: 24.12.4
@@ -1229,6 +1235,108 @@ packages:
cpu: [x64]
os: [win32]
'@sentry/browser-utils@10.73.0':
resolution: {integrity: sha512-qQygxJZ+RV779+iL1+lrJ4f4sZLgbgW0/JWPNp0YlcEAE62yCsdKbqoTEjB/EugdS4mSjBMX0chZC6rblu2Ycw==}
engines: {node: '>=18'}
'@sentry/browser@10.73.0':
resolution: {integrity: sha512-HqTe1S5RrWLufhX2LaFP3yNoMxfNDroh120bq1zdGHZfFDBMJQ0CDXxHO+L4UJfQ5dWdCCzWbXIAiZuWGa/DFQ==}
engines: {node: '>=18'}
'@sentry/bundler-plugins@10.73.0':
resolution: {integrity: sha512-4X5m2hoqgKO6SxHR7MF9QnvxPNk0hwTJFixDJ/pzQGVM+C34j/rSabouBqd0M+ZdxFeAt/9kA5X0TyeceNo9YQ==}
engines: {node: '>= 18'}
peerDependencies:
rollup: '>=3.2.0'
webpack: '>=5.0.0'
peerDependenciesMeta:
rollup:
optional: true
webpack:
optional: true
'@sentry/cli-darwin@2.58.6':
resolution: {integrity: sha512-udAVvcyfNa0R+95GvPz/+43/N3TC0TYKdkQ7D7jhPSzbcMc7l2fxRNN5yB3UpCA5fWFnW4toeaqwDBhb/Wh3LA==}
engines: {node: '>=10'}
os: [darwin]
'@sentry/cli-linux-arm64@2.58.6':
resolution: {integrity: sha512-q8mEcNNmeXMy5i+jWT30TVpH7LcP4HD21CD5XRSPAd/a912HF6EpK0ybf/1USO14WOhoXbAGi9txwaWabSe33g==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux, freebsd, android]
'@sentry/cli-linux-arm@2.58.6':
resolution: {integrity: sha512-pD0LAt5PcUzAinBwvDqc66x9+2CabHEv486yP0gRjWO7SakbaxmfVq/EXd8VLq/Tzi39LAu422UYK1lpW3MILw==}
engines: {node: '>=10'}
cpu: [arm]
os: [linux, freebsd, android]
'@sentry/cli-linux-i686@2.58.6':
resolution: {integrity: sha512-q8vNJi1eOV/4vxAFWBsEwLHoSYapaZHIf4j76KJGJXFKTkEbsjCOOsKbwUIBTQQhRgV4DFWh3ryfsPS/que4Kg==}
engines: {node: '>=10'}
cpu: [x86, ia32]
os: [linux, freebsd, android]
'@sentry/cli-linux-x64@2.58.6':
resolution: {integrity: sha512-DZu956Mhi3ZRjTBe1WdbGV46ldVbA8d2rgp/fh51GsI25zjBHah4wZnPTSzpc+YqxU6pJpg579B/r3jrIK530Q==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux, freebsd, android]
'@sentry/cli-win32-arm64@2.58.6':
resolution: {integrity: sha512-nj0Ff/kmAB73EPDhR8B4O9r+NUHK5GkPCkGWC+kXVemqAJWL5jcJ5KdxG0l/S0z6RoEoltID8/43/B+TaMlT7A==}
engines: {node: '>=10'}
cpu: [arm64]
os: [win32]
'@sentry/cli-win32-i686@2.58.6':
resolution: {integrity: sha512-WNZiDzPbgsEMQWq4avsQ391v/xWKJDIWWWo9GYl+N/w5qcYKkoDW7wQG7T9FasI6ENn68phChTOAPXXxbfAdOg==}
engines: {node: '>=10'}
cpu: [x86, ia32]
os: [win32]
'@sentry/cli-win32-x64@2.58.6':
resolution: {integrity: sha512-R35WJ17oF4D2eqI1DR2sQQqr0fjRTt5xoP16WrTu91XM2lndRMFsnjh+/GttbxapLCBNlrjzia99MJ0PZHZpgA==}
engines: {node: '>=10'}
cpu: [x64]
os: [win32]
'@sentry/cli@2.58.6':
resolution: {integrity: sha512-baBcNPLLfUi9WuL+Tpri9BFaAdvugZIKelC5X0tt0Zdy+K0K+PCVSrnNmwMWU/HyaF/SEv6b6UHnXIdqanBlcg==}
engines: {node: '>= 10'}
hasBin: true
'@sentry/conventions@0.16.0':
resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==}
engines: {node: '>=14'}
'@sentry/core@10.73.0':
resolution: {integrity: sha512-FLO1UgH19RyasVpofu612WCOgb2nEH0dZy+R72d7p65XU9i0wxlMKm3+sgfwKmiSJp1Qhilaaxs4Jg6BbiM5HA==}
engines: {node: '>=18'}
'@sentry/feedback@10.73.0':
resolution: {integrity: sha512-D6nSngX+e46Mae2/oh2bxBvxNK1z2NERbuMAhB5sx9x4xMBWIyGnYYTECehvEqV9+AqGAgxxhOZoYIG3AmRwww==}
engines: {node: '>=18'}
'@sentry/react@10.73.0':
resolution: {integrity: sha512-wJrzS98ddPvhGS/MKNHZyE8X7ecd4KwKdz7fHino2qkqBBrF4cxWr+q/uLTIk5PYWMkeJ4oKMjHAjOqmzGR4tg==}
engines: {node: '>=18'}
peerDependencies:
react: ^16.14.0 || 17.x || 18.x || 19.x
'@sentry/replay-canvas@10.73.0':
resolution: {integrity: sha512-sxa2lKkHPfF/j5xFpW7gocthWXRqyoHz8KCPy6yGc8plT477nl57iGSKhQDYsx1Ny10TLjs7YkIuPP1GY/Ax2Q==}
engines: {node: '>=18'}
'@sentry/replay@10.73.0':
resolution: {integrity: sha512-nN2wjN/Y0J5BOJV5hqRHUEBfxwUsipp1PKjcDHh6Fpxnrtfldu3Y99E8cQInseo5heFdzEvrOHBBrqWZXOHVKQ==}
engines: {node: '>=18'}
'@sentry/vite-plugin@5.4.0':
resolution: {integrity: sha512-fFJgCxs5hDyAm9BbZJ+LbA+LK2tjX5OoD0v0ARU4StR6KQmGUduoPs69yJ9AfqZ0om3Rlp5JDliiwFcNkasORA==}
engines: {node: '>= 18'}
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -1642,6 +1750,10 @@ packages:
detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dotenv@17.4.2:
resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
engines: {node: '>=12'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -1842,6 +1954,10 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
glob@13.0.6:
resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==}
engines: {node: 18 || 20 || >=22}
globals@14.0.0:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
@@ -2094,6 +2210,10 @@ packages:
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
minipass@7.1.3:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'}
motion-dom@12.40.0:
resolution: {integrity: sha512-HxU3ZaBwNPVQUBQf1xxgq+7JrPNZvjLVxgbpEZL7RrWJnsxOf0/OM+yrHG9ogLQ31Do/r57Oz2gQWPK+6q62mg==}
@@ -2125,6 +2245,15 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-releases@2.0.46:
resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==}
engines: {node: '>=18'}
@@ -2160,6 +2289,10 @@ packages:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-scurry@2.0.2:
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
engines: {node: 18 || 20 || >=22}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
@@ -2178,6 +2311,13 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
progress@2.0.3:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
proxy-from-env@2.1.0:
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
engines: {node: '>=10'}
@@ -2371,6 +2511,9 @@ packages:
resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==}
engines: {node: '>=16'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tr46@6.0.0:
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
engines: {node: '>=20'}
@@ -2525,6 +2668,9 @@ packages:
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
engines: {node: '>=18'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webidl-conversions@8.0.1:
resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
engines: {node: '>=20'}
@@ -2541,6 +2687,9 @@ packages:
resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==}
engines: {node: '>=20'}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -3521,6 +3670,115 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.60.4':
optional: true
'@sentry/browser-utils@10.73.0':
dependencies:
'@sentry/conventions': 0.16.0
'@sentry/core': 10.73.0
'@sentry/browser@10.73.0':
dependencies:
'@sentry/browser-utils': 10.73.0
'@sentry/conventions': 0.16.0
'@sentry/core': 10.73.0
'@sentry/feedback': 10.73.0
'@sentry/replay': 10.73.0
'@sentry/replay-canvas': 10.73.0
'@sentry/bundler-plugins@10.73.0(rollup@4.60.4)':
dependencies:
'@babel/core': 7.29.7
'@sentry/cli': 2.58.6
'@sentry/core': 10.73.0
dotenv: 17.4.2
find-up: 5.0.0
glob: 13.0.6
magic-string: 0.30.21
optionalDependencies:
rollup: 4.60.4
transitivePeerDependencies:
- encoding
- supports-color
'@sentry/cli-darwin@2.58.6':
optional: true
'@sentry/cli-linux-arm64@2.58.6':
optional: true
'@sentry/cli-linux-arm@2.58.6':
optional: true
'@sentry/cli-linux-i686@2.58.6':
optional: true
'@sentry/cli-linux-x64@2.58.6':
optional: true
'@sentry/cli-win32-arm64@2.58.6':
optional: true
'@sentry/cli-win32-i686@2.58.6':
optional: true
'@sentry/cli-win32-x64@2.58.6':
optional: true
'@sentry/cli@2.58.6':
dependencies:
https-proxy-agent: 5.0.1
node-fetch: 2.7.0
progress: 2.0.3
proxy-from-env: 1.1.0
which: 2.0.2
optionalDependencies:
'@sentry/cli-darwin': 2.58.6
'@sentry/cli-linux-arm': 2.58.6
'@sentry/cli-linux-arm64': 2.58.6
'@sentry/cli-linux-i686': 2.58.6
'@sentry/cli-linux-x64': 2.58.6
'@sentry/cli-win32-arm64': 2.58.6
'@sentry/cli-win32-i686': 2.58.6
'@sentry/cli-win32-x64': 2.58.6
transitivePeerDependencies:
- encoding
- supports-color
'@sentry/conventions@0.16.0': {}
'@sentry/core@10.73.0':
dependencies:
'@sentry/conventions': 0.16.0
'@sentry/feedback@10.73.0':
dependencies:
'@sentry/core': 10.73.0
'@sentry/react@10.73.0(react@19.2.6)':
dependencies:
'@sentry/browser': 10.73.0
'@sentry/conventions': 0.16.0
'@sentry/core': 10.73.0
react: 19.2.6
'@sentry/replay-canvas@10.73.0':
dependencies:
'@sentry/core': 10.73.0
'@sentry/replay': 10.73.0
'@sentry/replay@10.73.0':
dependencies:
'@sentry/browser-utils': 10.73.0
'@sentry/core': 10.73.0
'@sentry/vite-plugin@5.4.0(rollup@4.60.4)':
dependencies:
'@sentry/bundler-plugins': 10.73.0(rollup@4.60.4)
transitivePeerDependencies:
- encoding
- rollup
- supports-color
- webpack
'@standard-schema/spec@1.1.0': {}
'@standard-schema/utils@0.3.0': {}
@@ -3949,6 +4207,8 @@ snapshots:
detect-node-es@1.1.0: {}
dotenv@17.4.2: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -4174,6 +4434,12 @@ snapshots:
dependencies:
is-glob: 4.0.3
glob@13.0.6:
dependencies:
minimatch: 10.2.5
minipass: 7.1.3
path-scurry: 2.0.2
globals@14.0.0: {}
globals@16.5.0: {}
@@ -4393,6 +4659,8 @@ snapshots:
dependencies:
brace-expansion: 1.1.15
minipass@7.1.3: {}
motion-dom@12.40.0:
dependencies:
motion-utils: 12.39.0
@@ -4413,6 +4681,10 @@ snapshots:
natural-compare@1.4.0: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-releases@2.0.46: {}
obug@2.1.4: {}
@@ -4446,6 +4718,11 @@ snapshots:
path-key@3.1.1: {}
path-scurry@2.0.2:
dependencies:
lru-cache: 11.5.2
minipass: 7.1.3
pathe@2.0.3: {}
picocolors@1.1.1: {}
@@ -4460,6 +4737,10 @@ snapshots:
prelude-ls@1.2.1: {}
progress@2.0.3: {}
proxy-from-env@1.1.0: {}
proxy-from-env@2.1.0: {}
punycode@2.3.1: {}
@@ -4631,6 +4912,8 @@ snapshots:
dependencies:
tldts: 7.4.11
tr46@0.0.3: {}
tr46@6.0.0:
dependencies:
punycode: 2.3.1
@@ -4737,6 +5020,8 @@ snapshots:
dependencies:
xml-name-validator: 5.0.0
webidl-conversions@3.0.1: {}
webidl-conversions@8.0.1: {}
whatwg-mimetype@4.0.0: {}
@@ -4748,6 +5033,11 @@ snapshots:
tr46: 6.0.0
webidl-conversions: 8.0.1
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which@2.0.2:
dependencies:
isexe: 2.0.0
+4
View File
@@ -4,6 +4,10 @@
# refuse to run them. esbuild is pulled in transitively by vite and
# needs to compile its native binary at install time.
allowBuilds:
# The Sentry Vite plugin shells out to @sentry/cli, so its install script
# (which fetches the binary) has to run or the optional source-map upload
# has nothing to invoke.
'@sentry/cli': true
esbuild: true
# Match the dashboard's overrides so transitive deps don't drift.
+9
View File
@@ -11,6 +11,15 @@ export const DASHBOARD_URL: string = runtimeEnv("DASHBOARD_URL", import.meta.env
export const TURNSTILE_KEY: string = runtimeEnv("TURNSTILE_KEY", import.meta.env.VITE_TURNSTILE_KEY);
// Browser error reporting. The admin panel ships to self-hosters like every
// other image, so the DSN is the operator's and an empty one means the SDK is
// never initialised. See lib/observability.
export const SENTRY_DSN: string = runtimeEnv("SENTRY_DSN", import.meta.env.VITE_SENTRY_DSN);
export const SENTRY_ENVIRONMENT: string = runtimeEnv("SENTRY_ENVIRONMENT", import.meta.env.VITE_SENTRY_ENVIRONMENT, import.meta.env.MODE);
// Build-time on purpose: it has to match the release the source maps were
// uploaded under, which a container variable set afterwards could not.
export const SENTRY_RELEASE: string = import.meta.env.VITE_SENTRY_RELEASE ?? "";
export type EnvLabel = "production" | "staging" | "development";
const RAW_ENV_LABEL = runtimeEnv("ENV_LABEL", import.meta.env.VITE_ENV_LABEL as string | undefined).toLowerCase();
+33
View File
@@ -0,0 +1,33 @@
// Browser error reporting for the operator panel.
//
// Same rule as the dashboard: the admin image ships to self-hosters too, so a
// literal DSN here would make every self-hosted panel report its operator's
// errors and URLs to somebody else. The DSN comes from the container-injected
// runtime config, and an unset DSN means the SDK is never initialised, so
// nothing is ever sent anywhere.
import * as Sentry from "@sentry/react";
import { SENTRY_DSN, SENTRY_ENVIRONMENT, SENTRY_RELEASE } from "./env";
let reporting = false;
// initErrorReporting is called once, before the app renders.
export function initErrorReporting(): void {
if (!SENTRY_DSN) return;
Sentry.init({
dsn: SENTRY_DSN,
sendDefaultPii: true,
environment: SENTRY_ENVIRONMENT,
// Empty is omitted rather than sent: an event tagged with the empty
// release matches no uploaded source map and reads as a real release.
release: SENTRY_RELEASE || undefined,
});
reporting = true;
}
// captureException reports an error the app handled itself. A no-op when no DSN
// is configured.
export function captureException(error: unknown): void {
if (!reporting) return;
Sentry.captureException(error);
}
+5
View File
@@ -15,6 +15,8 @@ import "@fontsource/inter/600.css";
import "@fontsource/poppins/600.css";
import "@fontsource/poppins/700.css";
import { initErrorReporting } from "@/lib/observability";
import { Toaster } from "@/components/ui/sonner";
import { AppShell } from "@/components/layout/AppShell";
import { RequireAdmin } from "@/components/layout/RequireAdmin";
@@ -209,6 +211,9 @@ function AppShellWithKey() {
// we need a passthrough.
export { Outlet };
// Before the first render, so a boot failure is reported too.
initErrorReporting();
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
+35
View File
@@ -2,12 +2,47 @@ import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
import tailwindcss from "@tailwindcss/vite";
import { sentryVitePlugin } from "@sentry/vite-plugin";
// Source-map upload, and nothing else, is what the Sentry plugin does here.
//
// It is never a required build step: a fork, a self-host build or a local
// `pnpm build` sets none of these and the plugin is simply not in the plugin
// list, so nothing needs a Sentry account and nothing is uploaded. CI passes
// the token as a build secret only for the hosted release.
//
// Source maps are emitted only in that case too, so the shipped bundle is
// unchanged for everybody else, and `filesToDeleteAfterUpload` keeps the .map
// files out of the image once they have been sent.
const sentryAuthToken = process.env.SENTRY_AUTH_TOKEN;
const sentryOrg = process.env.SENTRY_ORG;
const sentryProject = process.env.SENTRY_PROJECT;
const uploadSourceMaps = Boolean(sentryAuthToken && sentryOrg && sentryProject);
const sentryPlugins = uploadSourceMaps
? [
sentryVitePlugin({
authToken: sentryAuthToken,
org: sentryOrg,
project: sentryProject,
release: { name: process.env.VITE_SENTRY_RELEASE },
sourcemaps: { filesToDeleteAfterUpload: ["dist/**/*.map"] },
telemetry: false,
}),
]
: [];
export default defineConfig({
plugins: [
react(),
tailwindcss(),
...sentryPlugins,
],
build: {
// Only when they are going to be uploaded: shipping them otherwise
// would hand every visitor the dashboard's original sources.
sourcemap: uploadSourceMaps,
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
+31 -31
View File
@@ -17,7 +17,6 @@ import (
"github.com/MicahParks/keyfunc/v3"
"github.com/aws/aws-sdk-go-v2/aws"
awsconf "github.com/aws/aws-sdk-go-v2/config"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/meszmate/apple-go"
"github.com/warmbly/warmbly/internal/api"
@@ -27,6 +26,7 @@ import (
"github.com/warmbly/warmbly/internal/app/adminoutreach"
"github.com/warmbly/warmbly/internal/app/advanced"
"github.com/warmbly/warmbly/internal/app/unsublink"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/warmbly/warmbly/internal/app/advisor"
@@ -328,7 +328,7 @@ func main() {
if config.TasksProvider() == "gcloud" {
serviceAccount, err = cfg.LoadGoogleServiceAccount(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
@@ -337,7 +337,7 @@ func main() {
if cfg.Env == "dev" {
log.Printf("Warning: Failed to fetch Google OIDC keys: %v", err)
} else {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
}
@@ -345,7 +345,7 @@ func main() {
apiCfg, err := cfg.LoadApiConfig(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
@@ -357,7 +357,7 @@ func main() {
if config.AWSNeeded() {
awscfg, err = awsconf.LoadDefaultConfig(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
}
@@ -369,13 +369,13 @@ func main() {
kms, err := kms.FromEnv(ctx, awscfg, masterKey)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
geoPath, err := cfg.LoadGeoDBPath(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
@@ -394,40 +394,40 @@ func main() {
s3, err := storage.NewFromEnv(ctx, awscfg, "main")
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
s3ForHandler = s3
primaryDBEndpoint, err := cfg.LoadPrimaryDBEndpoint(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
primaryDB, err := db.New(ctx, primaryDBEndpoint)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
// Run database migrations
log.Println("Running database migrations...")
if err := db.RunMigrations(primaryDBEndpoint); err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal("Failed to run migrations: ", err)
}
log.Println("Database migrations completed")
primaryRedis, err := cfg.LoadPrimaryRedisEndpoint(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
cache, err := cache.New(primaryRedis)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
@@ -444,13 +444,13 @@ func main() {
}
pubsubClient, err := pubsub.NewClient(ctx, gcpProjectID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal("Failed to initialize Pub/Sub client: ", err)
}
// Create the realtime topics + "<topic>-sub" subscriptions if missing,
// so the Elixir Broadway consumers always have a subscription to read.
if err := pubsubClient.EnsureRealtimeTopology(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal("Failed to provision Pub/Sub topics/subscriptions: ", err)
}
streamingPublisher = pubsub.NewStreamingPublisher(pubsubClient)
@@ -463,7 +463,7 @@ func main() {
emailCfg, err := cfg.LoadEmailConfig(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
@@ -478,7 +478,7 @@ func main() {
mailTransport, err = notify.NewTransport(ctx, cfg, emailCfg.EmailName, emailCfg.EmailAddress)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
emailNotificationService = mailTransport
@@ -497,7 +497,7 @@ func main() {
authCfg, err := cfg.LoadAuthConfig(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
@@ -529,25 +529,25 @@ func main() {
if config.EventBusProvider() == "kafka" {
kafkaBootstrapServers, err = cfg.LoadKafkaBootstrapServers(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
kafkaSaslConfig, err = cfg.LoadKafkaConfigSasl(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
}
codecImpl, err := codec.FromEnv()
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
bus, err := eventbus.FromEnv(kafkaBootstrapServers, kafkaSaslConfig)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
@@ -580,7 +580,7 @@ func main() {
webauthnRepository := repository.NewWebAuthnRepository(primaryDB)
credEncrypter, cerr := encrypt.FromEnv()
if cerr != nil {
sentry.CaptureException(cerr)
errs.CaptureFatal(cerr)
log.Fatal("Invalid CREDENTIALS_ENCRYPTION_KEY: ", cerr)
}
emailRepostory := repository.NewEmailRepostory(primaryDB, credEncrypter)
@@ -599,7 +599,7 @@ func main() {
instanceSettings = instancesettings.NewService(instancesettings.NewStore(primaryDB.Pool))
bootstrapInstanceSettings(ctx, instanceSettings)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
@@ -754,7 +754,7 @@ func main() {
if config.BillingProvider() == "stripe" {
stripeCfg, err := cfg.LoadStripeConfig(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
stripeService = stripe.NewService(stripeCfg, subscriptionRepository, planRepository, workerAssignmentService, discountService)
@@ -961,7 +961,7 @@ func main() {
RPOrigins: authCfg.WebAuthnRPOrigins,
})
if passkeyErr != nil {
sentry.CaptureException(passkeyErr)
errs.CaptureFatal(passkeyErr)
log.Fatal(passkeyErr)
}
passkeysUsable = passkeysUsableFor(os.Getenv("APP_URL"))
@@ -979,7 +979,7 @@ func main() {
cache,
)
if berr := bootstrapService.Run(ctx); berr != nil {
sentry.CaptureException(berr)
errs.CaptureException(berr)
log.Printf("Warning: bootstrap failed: %v", berr)
}
@@ -1008,7 +1008,7 @@ func main() {
{Kind: "blob", Provider: s3.Name(), Display: s3.Name(), ReadOnly: true},
{Kind: "eventbus", Provider: "kafka", Display: "kafka", ReadOnly: true},
}); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
log.Printf("storage_backends registrar: %v", err)
}
@@ -1313,12 +1313,12 @@ func main() {
if config.TasksProvider() == "gcloud" {
cloudTasksCfg, err := cfg.LoadCloudTasksConfig(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
gclient, err := gtasks.NewClient(ctx, cloudTasksCfg.QueueName, cloudTasksCfg.WebhookURL, serviceAccount, cloudTasksCfg.EmulatorHost)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
tasksClient = gclient
@@ -2111,7 +2111,7 @@ func main() {
AppEnv: os.Getenv("APP_ENV"),
}
sentry.CaptureMessage("Starting the backend on " + addr)
errs.CaptureMessage("Starting the backend on " + addr)
router := api.Run(h, m, oidcH, addr, ginMode, allowedOrigins)
+4 -4
View File
@@ -15,7 +15,6 @@ import (
"github.com/aws/aws-sdk-go-v2/aws"
awsconf "github.com/aws/aws-sdk-go-v2/config"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/app/advanced"
"github.com/warmbly/warmbly/internal/app/cipher"
jobs "github.com/warmbly/warmbly/internal/app/consumer"
@@ -48,6 +47,7 @@ import (
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/notify"
"github.com/warmbly/warmbly/internal/observability"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/encrypt"
"github.com/warmbly/warmbly/internal/pkg/generation"
"github.com/warmbly/warmbly/internal/pkg/geo"
@@ -167,14 +167,14 @@ func main() {
}
pubsubClient, err := pubsub.NewClient(ctx, gcpProjectID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal(err)
}
defer pubsubClient.Close()
// Idempotently ensure the realtime topics + subscriptions exist (safe to
// run from both backend and consumer; AlreadyExists is treated as success).
if err := pubsubClient.EnsureRealtimeTopology(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal("Failed to provision Pub/Sub topics/subscriptions: ", err)
}
streamingPublisher = pubsub.NewStreamingPublisher(pubsubClient)
@@ -187,7 +187,7 @@ func main() {
// Repositories
credEncrypter, err := encrypt.FromEnv()
if err != nil {
sentry.CaptureException(err)
errs.CaptureFatal(err)
log.Fatal("Invalid CREDENTIALS_ENCRYPTION_KEY: ", err)
}
emailRepo := repository.NewEmailRepostory(primaryDB, credEncrypter)
+31
View File
@@ -11,11 +11,22 @@ import (
"os"
"strconv"
"strings"
"time"
"github.com/warmbly/warmbly/internal/formserver"
"github.com/warmbly/warmbly/internal/observability"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func main() {
// Error reporting, before anything that can fail. Optional here as
// everywhere: no SENTRY_DSN means nothing is initialised and nothing is
// sent. A failure to configure it must not stop the service serving forms.
if err := observability.InitSentryEnv("forms"); err != nil {
log.Printf("error reporting not configured: %v", err)
}
defer errs.Flush(2 * time.Second)
backendURL := strings.TrimSpace(os.Getenv("BACKEND_INTERNAL_URL"))
if backendURL == "" {
log.Fatal("BACKEND_INTERNAL_URL is required (the backend's internal API base, e.g. http://localhost:8080)")
@@ -46,6 +57,13 @@ func main() {
InternalToken: token,
StaticDir: staticDir,
SubmitLimit: submitLimit,
// The browser half of error reporting, separate from this process's
// own SENTRY_DSN: form pages are public and their errors belong in a
// frontend project, not the service's. Empty means the page loads no
// reporting SDK, which is the self-host default.
BrowserSentryDSN: strings.TrimSpace(os.Getenv("WARMBLY_SENTRY_DSN")),
Release: observability.Release(),
Environment: appEnv(),
})
if err != nil {
log.Fatal(err)
@@ -62,10 +80,23 @@ func main() {
}
log.Printf("forms service listening on :%s (backend %s)", port, backendURL)
if err := r.Run(":" + port); err != nil {
// The listener dying is the one failure here worth reporting: the
// boot-time checks above are operator configuration, not a bug.
errs.CaptureException(err)
errs.Flush(2 * time.Second)
log.Fatal(err)
}
}
// appEnv is the deployment label, matching what InitSentryEnv reports for this
// process so the browser and the server halves agree.
func appEnv() string {
if env := strings.TrimSpace(os.Getenv("APP_ENV")); env != "" {
return env
}
return "dev"
}
func splitCSV(v string) []string {
var out []string
for _, part := range strings.Split(v, ",") {
+6
View File
@@ -40,6 +40,12 @@ EXPOSE 4000
ENV PHX_SERVER=true
# Build identity, the same VERSION/COMMIT the Go images take. Read at runtime
# by config/runtime.exs and used only to tag error events.
ARG VERSION=""
ARG COMMIT=""
ENV WARMBLY_RELEASE=${VERSION:-${COMMIT}}
# 127.0.0.1, not localhost: busybox wget tries ::1 first but the server binds IPv4.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s \
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:4000/health || exit 1
+12
View File
@@ -406,6 +406,12 @@ services:
# container via host.docker.internal, mirroring the tracking service.
BACKEND_INTERNAL_URL: ${BACKEND_INTERNAL_URL:-http://backend:8080}
INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:-local-dev-internal-token}
APP_ENV: ${APP_ENV:-dev}
# Two DSNs, two audiences: SENTRY_DSN is this Go process, and
# WARMBLY_SENTRY_DSN is stamped into the public form page for the browser
# app. Both unset means neither reports and no host is contacted.
SENTRY_DSN: ${SENTRY_DSN:-}
WARMBLY_SENTRY_DSN: ${WARMBLY_SENTRY_DSN:-}
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
@@ -533,6 +539,9 @@ services:
# Behind a reverse proxy set CHECK_ORIGIN=true once PHX_HOST matches the
# public websocket hostname.
CHECK_ORIGIN: ${CHECK_ORIGIN:-false}
# The environment label events are tagged with, matching every other
# service in this file.
APP_ENV: ${APP_ENV:-dev}
# SENTRY_DSN deliberately arrives via env_file, not the mapping above: the
# Elixir Sentry library reads the variable itself and refuses to start on an
# empty string, so it has to be genuinely absent rather than "".
@@ -579,6 +588,9 @@ services:
WARMBLY_DASHBOARD_URL: ${APP_URL:-http://${PUBLIC_HOST:-localhost}:5173}
WARMBLY_ENV_LABEL: ${ENV_LABEL:-development}
WARMBLY_TURNSTILE_KEY: ${WARMBLY_TURNSTILE_KEY:-1x00000000000000000000AA}
# Unset means the panel initialises no error reporting and contacts no
# Sentry host, exactly like the dashboard above.
WARMBLY_SENTRY_DSN: ${WARMBLY_SENTRY_DSN:-}
depends_on:
backend: { condition: service_healthy }
@@ -377,14 +377,32 @@ Delayed sends run through the local poller, so the backend must be running for s
## Observability
### Error reporting
Every runtime in Warmbly can report its errors, and none of them do unless you say where. A DSN is the operator's choice, not a requirement of the software, so the default install reports nowhere and contacts no host. Each variable is read by one process, so pointing the backend at a project does not make the dashboard report too.
| Variable | Read by | What it does | Default |
|---|---|---|---|
| `SENTRY_DSN` | backend, consumer, worker | Server-side error reporting. Optional in every environment, including `prod` | unset |
| `SENTRY_DSN` | forms service | The forms service's own errors. It reads its own environment, like tracking | unset |
| `SENTRY_DSN` | tracking service | Rust errors and panics. An invalid DSN disables reporting with a log line rather than stopping the service | unset |
| `SENTRY_DSN` | realtime service | An empty string is treated as unset on purpose, because the library rejects `""` hard enough to take the node down | unset |
| `WARMBLY_SENTRY_DSN` | web, admin containers | Browser error reporting, read at container start like the other `WARMBLY_*` values. Unset means the SDK is never initialised and no host is contacted | unset |
| `WARMBLY_SENTRY_DSN` | forms service | Stamped into the public form page for the form app's browser errors, along with the service's `APP_ENV` as their environment. Separate from the forms service's own `SENTRY_DSN`: one is a Go process, the other is a page a stranger loads | unset |
| `WARMBLY_SENTRY_ENVIRONMENT` | web, admin containers | The environment label those browser events carry. Defaults to the build mode. Form pages do not read it: the forms service stamps its own `APP_ENV` into the page instead | unset |
| `APP_ENV` | every server-side service | Doubles as the environment label on the events that service reports. Browser events take theirs from `WARMBLY_SENTRY_ENVIRONMENT` instead, because the container serving the bundle is not the process reporting the error | `dev` |
| `WARMBLY_RELEASE` | tracking, realtime | The build events are tagged with. The published images set it from the release tag; the Go services and the frontends read the same value from their build stamp instead | `dev` |
An unset DSN means nothing leaves the process and no Sentry host is contacted. The dashboard, the admin panel and form pages go further and never load the SDK at all, so there is not even a script; the Go, Rust and Elixir services still record errors, to their own logs. A set DSN can point at Sentry Cloud, a self-hosted Sentry, or any Sentry-compatible server such as GlitchTip; nothing in Warmbly assumes sentry.io.
The `admin` panel and the dashboard also tag events with the build they were served from. That value is baked at image build time, because it has to match the release the source maps were uploaded under, so it cannot be changed by a container variable afterwards.
### Push
| Variable | What it does | Default |
|---|---|---|
| `SENTRY_DSN` | Error reporting. Optional in every environment, including `prod` | unset |
| `WARMBLY_SENTRY_DSN` | Browser error reporting for the dashboard container, read at container start like the other `WARMBLY_*` values. Unset means the dashboard initialises no reporting SDK and contacts no Sentry host | unset |
| `APNS_KEY` or `APNS_KEY_PATH`, `APNS_KEY_ID`, `APNS_TEAM_ID`, `APNS_TOPIC` | Mobile push on backend and consumer. Partial configuration disables push with a warning, never a crash | unset |
Error reporting is off by default everywhere. A DSN is the operator's choice, and each of these is read by one process, so pointing the backend at a project does not make the dashboard report too. Both accept Sentry Cloud, a self-hosted Sentry or any Sentry-compatible server.
## Updates
| Variable | What it does | Default | Restart needed |
@@ -410,6 +428,8 @@ The public face of hosted forms (`cmd/forms`): it serves the React (TanStack) fo
| `BACKEND_INTERNAL_URL` | Where the service resolves forms and forwards submissions, the same variable the tracking service uses. **Required**: the service exits at boot without it | none |
| `INTERNAL_API_TOKEN` | Bearer token for those calls, matching the backend's. **Required**: the service exits at boot on an empty value | none |
| `FORM_IP_RATE_LIMIT` | Public form submissions allowed per source IP per 10 minutes, per forms-service instance | `30` |
| `SENTRY_DSN` | The service's own error reporting. Unset means none | unset |
| `WARMBLY_SENTRY_DSN` | Stamped into the form page so the browser app reports too. Unset means the page loads no reporting SDK at all | unset |
| `TRUSTED_PROXIES` | CIDRs whose `X-Forwarded-For` the service believes, same convention as the backend. Empty trusts nothing and uses the socket peer; set it behind a reverse proxy or the submit limiter throttles the proxy's address instead of the visitor's | empty |
## Tracking service
@@ -432,7 +452,9 @@ The Rust open and click service. It reads its own environment, so these have to
| `KAFKA_BOOTSTRAP_SERVERS`, `KAFKA_SASL_USERNAME`, `KAFKA_SASL_PASSWORD` | Broker transport when `EVENTBUS_PROVIDER=kafka` | unset |
| `SCHEMA_REGISTRY_URL`, `SCHEMA_REGISTRY_KEY`, `SCHEMA_REGISTRY_SECRET` | Registry for the Avro codec | unset |
| `AWS_CONFIG_ENABLED` | `true` falls back to AWS SSM and Secrets Manager for any value missing from the environment | `false` |
| `APP_ENV` | Environment label used in logs | `dev` |
| `APP_ENV` | Environment label used in logs, and on reported errors | `dev` |
| `SENTRY_DSN` | Error and panic reporting. Unset means none; an invalid value logs and disables it rather than stopping the service | unset |
| `WARMBLY_RELEASE` | The build reported errors are tagged with | `dev` |
## Realtime service
@@ -457,6 +479,7 @@ The Elixir websocket service. Its runtime configuration is read only when the re
| `RATE_LIMIT_WS_JOIN` | Channel joins per minute, counted per `phx_join` on an open socket | `30` |
| `RATE_LIMIT_WS_EVENT` | Client events per minute, which is what bounds presence updates | `60` |
| `SENTRY_DSN` | Error reporting. An empty string is treated as unset on purpose, because the library rejects `""` hard enough to take the node down | unset |
| `WARMBLY_RELEASE` | The build reported errors are tagged with | `dev` |
<Callout type="warn" title="CHECK_ORIGIN is false by default">
The shipped default accepts a websocket upgrade from **any** origin. A token is still required to join a channel, so an attacker needs a valid JWT either way, but on a deployment reachable from the internet set `PHX_HOST` to the public websocket hostname and `CHECK_ORIGIN=true` so only your own dashboard can open a socket.
@@ -213,10 +213,15 @@ An instance reports errors nowhere unless you point it somewhere. Every service
| Service | Variable |
|---|---|
| Backend, consumer, worker | `SENTRY_DSN` |
| Dashboard container | `WARMBLY_SENTRY_DSN` |
| Realtime | `SENTRY_DSN` |
| Forms service | `SENTRY_DSN` |
| Tracking service | `SENTRY_DSN` |
| Realtime service | `SENTRY_DSN` |
| Dashboard and admin containers | `WARMBLY_SENTRY_DSN` |
| Public form pages | `WARMBLY_SENTRY_DSN` on the forms service |
Set one and that service reports to whatever Sentry Cloud project, self-hosted Sentry or Sentry-compatible server you name. Leave it unset, which is the default the installer writes, and the SDK is never initialised: there is no host to contact and nothing to opt out of.
Set one and that service reports to whatever Sentry Cloud project, self-hosted Sentry or Sentry-compatible server you name. Leave it unset, which is the default the installer writes, and nothing leaves the process: there is no host to contact and nothing to opt out of. Errors still reach that service's own log, as they always did. The dashboard, the admin panel and form pages go further and load no reporting code at all, so there is not even a script to block.
The `warmbly` CLI runs on your own machine and reports nowhere, ever. It has no DSN to set.
## See also
@@ -109,6 +109,27 @@ The volumes hold the per-organization data keys. Every sealed mailbox credential
`redis-cli FLUSHDB` also destroys `bootstrap:setup_token`, every pending auth session and every login attempt counter. On an unclaimed instance that throws away the only way in. Delete the specific key, or wait out the window.
</Callout>
## Collecting errors somewhere you can read them
`make logs` shows you what a service printed. It does not tell you that a handler threw at 3am, in which build, for which organization. Every runtime in Warmbly can send that to Sentry instead, and none of them do until you set a DSN.
Point each service at a project by setting one variable on it:
```bash
# backend, consumer, worker, forms, tracking, realtime
SENTRY_DSN=https://<key>@<host>/<project>
# the dashboard, the admin panel, and public form pages (browser errors)
WARMBLY_SENTRY_DSN=https://<key>@<host>/<project>
```
Under the installer's compose file both go in `.env`: every service inherits it through `env_file`, so one line and a `docker compose up -d` covers the whole instance.
Every event is tagged with the service that raised it, the environment and the build, so one project can hold the whole instance and still be filterable. Server-side services take the environment from `APP_ENV`. The dashboard and admin panel take theirs from `WARMBLY_SENTRY_ENVIRONMENT`, since the container serving the bundle is not the process reporting the error. Hosted form pages are the exception among the browser apps: their environment is stamped into the page by the forms service, so it comes from that service's `APP_ENV`. The frontends are separate projects' worth of events in practice; give the browser ones their own project if you want the noise apart.
The server behind the DSN is your choice. Sentry Cloud's free Developer plan covers 5k errors a month, and Warmbly assumes nothing about the host, so a self-hosted Sentry or a Sentry-compatible server such as GlitchTip works with the same variable.
Leaving every DSN unset is a supported configuration, not a degraded one: no host is contacted and errors go to the log as before. See [data control](/development/data-control/#error-reporting).
## Still stuck
```bash
+6
View File
@@ -6,6 +6,12 @@
<meta name="robots" content="noindex" />
<!-- The Go shell stamps the render token here; empty in `vite dev`. -->
<meta name="wf-token" content="" />
<!-- Browser error reporting, stamped by the Go shell from the forms
service's WARMBLY_SENTRY_DSN. Empty (the default, and always in
`vite dev`) means the SDK chunk is never even fetched. -->
<meta name="wf-sentry-dsn" content="" />
<meta name="wf-release" content="" />
<meta name="wf-environment" content="" />
<title>Form</title>
</head>
<body>
+1
View File
@@ -12,6 +12,7 @@
"preview": "vite preview"
},
"dependencies": {
"@sentry/browser": "^10.73.0",
"@tanstack/react-form": "^1.23.8",
"@tanstack/react-query": "^5.90.5",
"@tanstack/react-router": "^1.132.0",
+65
View File
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@sentry/browser':
specifier: ^10.73.0
version: 10.73.0
'@tanstack/react-form':
specifier: ^1.23.8
version: 1.33.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
@@ -524,6 +527,34 @@ packages:
cpu: [x64]
os: [win32]
'@sentry/browser-utils@10.73.0':
resolution: {integrity: sha512-qQygxJZ+RV779+iL1+lrJ4f4sZLgbgW0/JWPNp0YlcEAE62yCsdKbqoTEjB/EugdS4mSjBMX0chZC6rblu2Ycw==}
engines: {node: '>=18'}
'@sentry/browser@10.73.0':
resolution: {integrity: sha512-HqTe1S5RrWLufhX2LaFP3yNoMxfNDroh120bq1zdGHZfFDBMJQ0CDXxHO+L4UJfQ5dWdCCzWbXIAiZuWGa/DFQ==}
engines: {node: '>=18'}
'@sentry/conventions@0.16.0':
resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==}
engines: {node: '>=14'}
'@sentry/core@10.73.0':
resolution: {integrity: sha512-FLO1UgH19RyasVpofu612WCOgb2nEH0dZy+R72d7p65XU9i0wxlMKm3+sgfwKmiSJp1Qhilaaxs4Jg6BbiM5HA==}
engines: {node: '>=18'}
'@sentry/feedback@10.73.0':
resolution: {integrity: sha512-D6nSngX+e46Mae2/oh2bxBvxNK1z2NERbuMAhB5sx9x4xMBWIyGnYYTECehvEqV9+AqGAgxxhOZoYIG3AmRwww==}
engines: {node: '>=18'}
'@sentry/replay-canvas@10.73.0':
resolution: {integrity: sha512-sxa2lKkHPfF/j5xFpW7gocthWXRqyoHz8KCPy6yGc8plT477nl57iGSKhQDYsx1Ny10TLjs7YkIuPP1GY/Ax2Q==}
engines: {node: '>=18'}
'@sentry/replay@10.73.0':
resolution: {integrity: sha512-nN2wjN/Y0J5BOJV5hqRHUEBfxwUsipp1PKjcDHh6Fpxnrtfldu3Y99E8cQInseo5heFdzEvrOHBBrqWZXOHVKQ==}
engines: {node: '>=18'}
'@tanstack/devtools-event-client@0.4.4':
resolution: {integrity: sha512-6T5Yop/793YI+H+5J8Hsyj4kCih9sl4t3ElLgKioW5hk3ocn+ZdSJ94tT7vL7uabxSugWYBZlOTMPzEw2puvQw==}
engines: {node: '>=18'}
@@ -1557,6 +1588,40 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.63.1':
optional: true
'@sentry/browser-utils@10.73.0':
dependencies:
'@sentry/conventions': 0.16.0
'@sentry/core': 10.73.0
'@sentry/browser@10.73.0':
dependencies:
'@sentry/browser-utils': 10.73.0
'@sentry/conventions': 0.16.0
'@sentry/core': 10.73.0
'@sentry/feedback': 10.73.0
'@sentry/replay': 10.73.0
'@sentry/replay-canvas': 10.73.0
'@sentry/conventions@0.16.0': {}
'@sentry/core@10.73.0':
dependencies:
'@sentry/conventions': 0.16.0
'@sentry/feedback@10.73.0':
dependencies:
'@sentry/core': 10.73.0
'@sentry/replay-canvas@10.73.0':
dependencies:
'@sentry/core': 10.73.0
'@sentry/replay': 10.73.0
'@sentry/replay@10.73.0':
dependencies:
'@sentry/browser-utils': 10.73.0
'@sentry/core': 10.73.0
'@tanstack/devtools-event-client@0.4.4': {}
'@tanstack/form-core@1.33.5':
+3
View File
@@ -8,6 +8,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createRootRoute, createRoute, createRouter, Outlet, RouterProvider } from "@tanstack/react-router";
import { FormPage } from "./FormPage";
import { initErrorReporting } from "./observability";
import { NotFound } from "./NotFound";
import "./styles.css";
@@ -32,6 +33,8 @@ declare module "@tanstack/react-router" {
const queryClient = new QueryClient();
initErrorReporting();
createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
+34
View File
@@ -0,0 +1,34 @@
// Browser error reporting for the hosted form page.
//
// Public pages have to stay light, so unlike the dashboard this loads the SDK
// as its own chunk and only when the shell stamped a DSN: with none, which is
// every self-host and every install that has not configured one, nothing is
// fetched and nothing is sent. The cost is that an error thrown in the first
// few milliseconds is missed, which is the right trade on a page whose whole
// job is to render one form for a stranger.
function meta(name: string): string {
return document.querySelector<HTMLMetaElement>(`meta[name="${name}"]`)?.content?.trim() ?? "";
}
export function initErrorReporting(): void {
const dsn = meta("wf-sentry-dsn");
if (!dsn) return;
void import("@sentry/browser").then((Sentry) => {
Sentry.init({
dsn,
release: meta("wf-release") || undefined,
environment: meta("wf-environment") || undefined,
// Named so form-page errors are separable from the dashboard's in
// a shared project, the same way the Go services set ServerName.
initialScope: { tags: { service: "forms" } },
// A form page carries a stranger's answers. Default PII (their IP,
// their headers) is not ours to collect, and the dashboard's
// reasons for sending it do not apply here.
sendDefaultPii: false,
});
}).catch(() => {
// A blocked or failed SDK load must never stop the form rendering.
});
}
+2 -2
View File
@@ -15,9 +15,9 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/api/middleware"
"github.com/warmbly/warmbly/internal/errx"
@@ -80,7 +80,7 @@ func (h *Handler) deleteObjectDetached(ctx context.Context, key string) {
cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 15*time.Second)
defer cancel()
if err := h.Storage.Delete(cleanup, key); err != nil {
sentry.CaptureException(fmt.Errorf("attachment %s: cleanup after refused reservation: %w", key, err))
errs.CaptureException(fmt.Errorf("attachment %s: cleanup after refused reservation: %w", key, err))
}
}
+2 -2
View File
@@ -4,10 +4,10 @@ import (
"fmt"
"net/http"
"github.com/getsentry/sentry-go"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// RateLimitMiddleware checks rate limits for a given category
@@ -35,7 +35,7 @@ func (h *Handler) RateLimitMiddleware(category models.RateLimitCategory) gin.Han
status, err := h.RateLimitService.CheckAndRecord(c.Request.Context(), userID, category)
if err != nil {
// Log error but allow request (fail open)
sentry.CaptureException(err)
errs.CaptureException(err)
c.Next()
return
}
+58 -58
View File
@@ -4,10 +4,10 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/repository"
)
@@ -114,7 +114,7 @@ func (s *adminService) logAction(ctx context.Context, adminID uuid.UUID, action,
CreatedAt: time.Now(),
}
if err := s.repo.CreateAuditLog(ctx, log); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
@@ -139,7 +139,7 @@ func (s *adminService) LogAdminAction(ctx context.Context, adminID uuid.UUID, ac
func (s *adminService) SearchMailboxes(ctx context.Context, search *models.AdminMailboxSearch) (*models.AdminMailboxesResult, *errx.Error) {
result, err := s.repo.SearchMailboxesForAdmin(ctx, search)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to search mailboxes")
}
return result, nil
@@ -148,7 +148,7 @@ func (s *adminService) SearchMailboxes(ctx context.Context, search *models.Admin
func (s *adminService) SearchUsers(ctx context.Context, search *models.AdminUserSearch) (*models.AdminUsersResult, *errx.Error) {
result, err := s.repo.SearchUsers(ctx, search)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to search users")
}
return result, nil
@@ -157,7 +157,7 @@ func (s *adminService) SearchUsers(ctx context.Context, search *models.AdminUser
func (s *adminService) GetUserDetail(ctx context.Context, userID uuid.UUID) (*models.AdminUserDetail, *errx.Error) {
user, err := s.repo.GetUserDetail(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get user detail")
}
if user == nil {
@@ -169,7 +169,7 @@ func (s *adminService) GetUserDetail(ctx context.Context, userID uuid.UUID) (*mo
func (s *adminService) GetUserPreview(ctx context.Context, userID uuid.UUID) (*models.AdminUserPreview, *errx.Error) {
preview, err := s.repo.GetUserPreview(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get user preview")
}
if preview == nil {
@@ -186,7 +186,7 @@ func (s *adminService) BanUser(ctx context.Context, adminID, userID uuid.UUID, r
// Check if user exists
user, err := s.repo.GetUserDetail(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to get user")
}
if user == nil {
@@ -210,7 +210,7 @@ func (s *adminService) BanUser(ctx context.Context, adminID, userID uuid.UUID, r
}
if err := s.repo.BanUser(ctx, userID, adminID, reason, uint32(scope)); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to ban user")
}
@@ -222,7 +222,7 @@ func (s *adminService) UnbanUser(ctx context.Context, adminID, userID uuid.UUID,
// Check if user exists
user, err := s.repo.GetUserDetail(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to get user")
}
if user == nil {
@@ -235,7 +235,7 @@ func (s *adminService) UnbanUser(ctx context.Context, adminID, userID uuid.UUID,
}
if err := s.repo.UnbanUser(ctx, userID, adminID, reason); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to unban user")
}
@@ -246,7 +246,7 @@ func (s *adminService) UnbanUser(ctx context.Context, adminID, userID uuid.UUID,
func (s *adminService) GetUserBans(ctx context.Context, userID uuid.UUID) ([]models.UserBan, *errx.Error) {
bans, err := s.repo.GetUserBans(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get user bans")
}
return bans, nil
@@ -260,7 +260,7 @@ func (s *adminService) GetUserCampaigns(ctx context.Context, userID uuid.UUID, c
}
result, err := s.repo.SearchCampaigns(ctx, search)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get user campaigns")
}
return result, nil
@@ -269,7 +269,7 @@ func (s *adminService) GetUserCampaigns(ctx context.Context, userID uuid.UUID, c
func (s *adminService) GetUserEmails(ctx context.Context, userID uuid.UUID, cursor *uuid.UUID, limit int) ([]models.AdminWorkerEmail, *models.Pagination, *errx.Error) {
emails, pagination, err := s.repo.GetUserEmails(ctx, userID, cursor, limit)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, nil, errx.New(errx.Internal, "failed to get user emails")
}
return emails, pagination, nil
@@ -278,7 +278,7 @@ func (s *adminService) GetUserEmails(ctx context.Context, userID uuid.UUID, curs
func (s *adminService) GetUserRateLimits(ctx context.Context, userID uuid.UUID) (*models.AdminUserRateLimits, *errx.Error) {
limits, err := s.repo.GetUserRateLimits(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get rate limits")
}
// No row means the enforcement path falls back to the product defaults.
@@ -290,7 +290,7 @@ func (s *adminService) GetUserRateLimits(ctx context.Context, userID uuid.UUID)
func (s *adminService) UpdateUserRateLimits(ctx context.Context, adminID, userID uuid.UUID, update *models.UpdateUserRateLimitsRequest, ipAddress, userAgent string) (*models.AdminUserRateLimits, *errx.Error) {
if err := s.repo.UpdateUserRateLimits(ctx, userID, adminID, update); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to update rate limits")
}
@@ -303,7 +303,7 @@ func (s *adminService) UpdateUserRateLimits(ctx context.Context, adminID, userID
func (s *adminService) ListWorkers(ctx context.Context, cursor *uuid.UUID, limit int) (*models.AdminWorkersResult, *errx.Error) {
result, err := s.repo.ListWorkers(ctx, cursor, limit)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list workers")
}
return result, nil
@@ -312,7 +312,7 @@ func (s *adminService) ListWorkers(ctx context.Context, cursor *uuid.UUID, limit
func (s *adminService) GetWorkerDetail(ctx context.Context, workerID uuid.UUID) (*models.AdminWorkerDetail, *errx.Error) {
worker, err := s.repo.GetWorkerDetail(ctx, workerID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get worker detail")
}
if worker == nil {
@@ -323,7 +323,7 @@ func (s *adminService) GetWorkerDetail(ctx context.Context, workerID uuid.UUID)
func (s *adminService) UpdateWorker(ctx context.Context, adminID, workerID uuid.UUID, update *models.AdminUpdateWorker, ipAddress, userAgent string) *errx.Error {
if err := s.repo.UpdateWorker(ctx, workerID, update); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to update worker")
}
@@ -334,7 +334,7 @@ func (s *adminService) UpdateWorker(ctx context.Context, adminID, workerID uuid.
func (s *adminService) GetWorkerEmails(ctx context.Context, workerID uuid.UUID, cursor *uuid.UUID, limit int) ([]models.AdminWorkerEmail, *models.Pagination, *errx.Error) {
emails, pagination, err := s.repo.GetWorkerEmails(ctx, workerID, cursor, limit)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, nil, errx.New(errx.Internal, "failed to get worker emails")
}
return emails, pagination, nil
@@ -343,7 +343,7 @@ func (s *adminService) GetWorkerEmails(ctx context.Context, workerID uuid.UUID,
func (s *adminService) GetWorkerStats(ctx context.Context, workerID uuid.UUID) (*models.WorkerStats, *errx.Error) {
stats, err := s.repo.GetWorkerStats(ctx, workerID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get worker stats")
}
return stats, nil
@@ -351,7 +351,7 @@ func (s *adminService) GetWorkerStats(ctx context.Context, workerID uuid.UUID) (
func (s *adminService) ReassignEmails(ctx context.Context, adminID uuid.UUID, emailIDs []uuid.UUID, newWorkerID uuid.UUID, ipAddress, userAgent string) *errx.Error {
if err := s.repo.ReassignEmails(ctx, emailIDs, newWorkerID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to reassign emails")
}
@@ -364,7 +364,7 @@ func (s *adminService) ReassignEmails(ctx context.Context, adminID uuid.UUID, em
func (s *adminService) ListWarmupPools(ctx context.Context) ([]models.WarmupPoolInfo, *errx.Error) {
pools, err := s.repo.ListWarmupPools(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list warmup pools")
}
return pools, nil
@@ -373,7 +373,7 @@ func (s *adminService) ListWarmupPools(ctx context.Context) ([]models.WarmupPool
func (s *adminService) GetPoolParticipants(ctx context.Context, poolType string, cursor *uuid.UUID, limit int) (*models.WarmupPoolParticipantsResult, *errx.Error) {
result, err := s.repo.GetPoolParticipants(ctx, poolType, cursor, limit)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get pool participants")
}
return result, nil
@@ -382,7 +382,7 @@ func (s *adminService) GetPoolParticipants(ctx context.Context, poolType string,
func (s *adminService) ListBlockedAccounts(ctx context.Context, cursor *uuid.UUID, limit int) (*models.AdminBlockedAccountsResult, *errx.Error) {
result, err := s.repo.ListBlockedAccounts(ctx, cursor, limit)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list blocked accounts")
}
return result, nil
@@ -390,7 +390,7 @@ func (s *adminService) ListBlockedAccounts(ctx context.Context, cursor *uuid.UUI
func (s *adminService) BlockAccount(ctx context.Context, adminID, accountID uuid.UUID, reason string, ipAddress, userAgent string) *errx.Error {
if err := s.repo.BlockAccount(ctx, accountID, adminID, reason); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to block account")
}
@@ -400,7 +400,7 @@ func (s *adminService) BlockAccount(ctx context.Context, adminID, accountID uuid
func (s *adminService) UnblockAccount(ctx context.Context, adminID, accountID uuid.UUID, ipAddress, userAgent string) *errx.Error {
if err := s.repo.UnblockAccount(ctx, accountID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to unblock account")
}
@@ -413,7 +413,7 @@ func (s *adminService) UnblockAccount(ctx context.Context, adminID, accountID uu
func (s *adminService) ListAppeals(ctx context.Context, status string, cursor *uuid.UUID, limit int) (*models.WarmupAppealsResult, *errx.Error) {
result, err := s.repo.ListAppeals(ctx, status, cursor, limit)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list appeals")
}
return result, nil
@@ -422,7 +422,7 @@ func (s *adminService) ListAppeals(ctx context.Context, status string, cursor *u
func (s *adminService) GetAppeal(ctx context.Context, appealID uuid.UUID) (*models.WarmupAppeal, *errx.Error) {
appeal, err := s.repo.GetAppeal(ctx, appealID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get appeal")
}
if appeal == nil {
@@ -434,7 +434,7 @@ func (s *adminService) GetAppeal(ctx context.Context, appealID uuid.UUID) (*mode
func (s *adminService) ReviewAppeal(ctx context.Context, adminID, appealID uuid.UUID, approved bool, notes string, ipAddress, userAgent string) *errx.Error {
appeal, err := s.repo.GetAppeal(ctx, appealID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to get appeal")
}
if appeal == nil {
@@ -446,7 +446,7 @@ func (s *adminService) ReviewAppeal(ctx context.Context, adminID, appealID uuid.
}
if err := s.repo.ReviewAppeal(ctx, appealID, adminID, approved, notes); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to review appeal")
}
@@ -463,7 +463,7 @@ func (s *adminService) ReviewAppeal(ctx context.Context, adminID, appealID uuid.
func (s *adminService) SearchCampaigns(ctx context.Context, search *models.AdminCampaignSearch) (*models.AdminCampaignsResult, *errx.Error) {
result, err := s.repo.SearchCampaigns(ctx, search)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to search campaigns")
}
return result, nil
@@ -472,7 +472,7 @@ func (s *adminService) SearchCampaigns(ctx context.Context, search *models.Admin
func (s *adminService) GetCampaignDetail(ctx context.Context, campaignID uuid.UUID) (*models.AdminCampaignDetail, *errx.Error) {
campaign, err := s.repo.GetCampaignDetail(ctx, campaignID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get campaign detail")
}
if campaign == nil {
@@ -484,7 +484,7 @@ func (s *adminService) GetCampaignDetail(ctx context.Context, campaignID uuid.UU
func (s *adminService) StopCampaign(ctx context.Context, adminID, campaignID uuid.UUID, reason, ipAddress, userAgent string) *errx.Error {
campaign, err := s.repo.GetCampaignDetail(ctx, campaignID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to get campaign")
}
if campaign == nil {
@@ -495,7 +495,7 @@ func (s *adminService) StopCampaign(ctx context.Context, adminID, campaignID uui
// never will, and either can become true between here and the UPDATE.
stopped, err := s.repo.StopCampaign(ctx, campaignID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to stop campaign")
}
if !stopped {
@@ -510,7 +510,7 @@ func (s *adminService) StopCampaign(ctx context.Context, adminID, campaignID uui
Message: "Campaign force-stopped by platform staff: " + reason,
Metadata: map[string]interface{}{"reason": reason},
}); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
@@ -526,7 +526,7 @@ func (s *adminService) StopCampaign(ctx context.Context, adminID, campaignID uui
func (s *adminService) GetPlatformOverview(ctx context.Context) (*models.PlatformOverview, *errx.Error) {
overview, err := s.repo.GetPlatformOverview(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get platform overview")
}
return overview, nil
@@ -535,7 +535,7 @@ func (s *adminService) GetPlatformOverview(ctx context.Context) (*models.Platfor
func (s *adminService) GetAnalyticsTrends(ctx context.Context) (*models.AnalyticsTrends, *errx.Error) {
trends, err := s.repo.GetAnalyticsTrends(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get analytics trends")
}
return trends, nil
@@ -544,7 +544,7 @@ func (s *adminService) GetAnalyticsTrends(ctx context.Context) (*models.Analytic
func (s *adminService) GetDailyEmailStats(ctx context.Context, startDate, endDate time.Time) ([]models.DailyEmailStats, *errx.Error) {
stats, err := s.repo.GetDailyEmailStats(ctx, startDate, endDate)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get daily email stats")
}
return stats, nil
@@ -553,7 +553,7 @@ func (s *adminService) GetDailyEmailStats(ctx context.Context, startDate, endDat
func (s *adminService) GetHourlyEmailStats(ctx context.Context, date time.Time) ([]models.HourlyEmailStats, *errx.Error) {
stats, err := s.repo.GetHourlyEmailStats(ctx, date)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get hourly email stats")
}
return stats, nil
@@ -562,7 +562,7 @@ func (s *adminService) GetHourlyEmailStats(ctx context.Context, date time.Time)
func (s *adminService) GetWorkerLoadStats(ctx context.Context) ([]models.WorkerLoadStats, *errx.Error) {
stats, err := s.repo.GetWorkerLoadStats(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get worker load stats")
}
return stats, nil
@@ -571,7 +571,7 @@ func (s *adminService) GetWorkerLoadStats(ctx context.Context) ([]models.WorkerL
func (s *adminService) GetEmailDistribution(ctx context.Context) ([]models.EmailDistribution, *errx.Error) {
dist, err := s.repo.GetEmailDistribution(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get email distribution")
}
return dist, nil
@@ -580,7 +580,7 @@ func (s *adminService) GetEmailDistribution(ctx context.Context) ([]models.Email
func (s *adminService) GetUserGrowthStats(ctx context.Context, startDate, endDate time.Time) ([]models.UserGrowthStats, *errx.Error) {
stats, err := s.repo.GetUserGrowthStats(ctx, startDate, endDate)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get user growth stats")
}
return stats, nil
@@ -591,7 +591,7 @@ func (s *adminService) GetUserGrowthStats(ctx context.Context, startDate, endDat
func (s *adminService) ListPlans(ctx context.Context, includePrivate bool) ([]models.Plan, *errx.Error) {
plans, err := s.repo.ListPlans(ctx, includePrivate)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list plans")
}
return plans, nil
@@ -600,7 +600,7 @@ func (s *adminService) ListPlans(ctx context.Context, includePrivate bool) ([]mo
func (s *adminService) SearchPlansForAdmin(ctx context.Context, search *models.AdminPlanSearch) (*models.AdminPlansResult, *errx.Error) {
result, err := s.repo.SearchPlansForAdmin(ctx, search)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to search plans")
}
return result, nil
@@ -614,7 +614,7 @@ func (s *adminService) resolveDuration(ctx context.Context, d models.Duration) (
}
id, err := s.repo.DurationIDByTitle(ctx, string(d))
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to resolve plan duration")
}
if id == nil {
@@ -649,7 +649,7 @@ func (s *adminService) CreatePlan(ctx context.Context, adminID uuid.UUID, req *m
}
if err := s.repo.CreatePlan(ctx, plan, *durationID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to create plan")
}
@@ -660,7 +660,7 @@ func (s *adminService) CreatePlan(ctx context.Context, adminID uuid.UUID, req *m
func (s *adminService) GetPlan(ctx context.Context, planID uuid.UUID) (*models.Plan, *errx.Error) {
plan, err := s.repo.GetPlan(ctx, planID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get plan")
}
if plan == nil {
@@ -672,7 +672,7 @@ func (s *adminService) GetPlan(ctx context.Context, planID uuid.UUID) (*models.P
func (s *adminService) UpdatePlan(ctx context.Context, adminID, planID uuid.UUID, req *models.UpdatePlanRequest, ipAddress, userAgent string) (*models.Plan, *errx.Error) {
plan, err := s.repo.GetPlan(ctx, planID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get plan")
}
if plan == nil {
@@ -732,7 +732,7 @@ func (s *adminService) UpdatePlan(ctx context.Context, adminID, planID uuid.UUID
}
if err := s.repo.UpdatePlan(ctx, plan, *durationID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to update plan")
}
@@ -744,7 +744,7 @@ func (s *adminService) DeletePlan(ctx context.Context, adminID, planID uuid.UUID
// Check if plan is in use
inUse, err := s.repo.IsPlanInUse(ctx, planID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to check plan usage")
}
if inUse {
@@ -752,7 +752,7 @@ func (s *adminService) DeletePlan(ctx context.Context, adminID, planID uuid.UUID
}
if err := s.repo.DeletePlan(ctx, planID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to delete plan")
}
@@ -765,7 +765,7 @@ func (s *adminService) DeletePlan(ctx context.Context, adminID, planID uuid.UUID
func (s *adminService) ListEnterpriseInquiries(ctx context.Context, search *models.AdminEnterpriseInquirySearch) (*models.AdminEnterpriseInquiriesResult, *errx.Error) {
result, err := s.repo.ListEnterpriseInquiries(ctx, search)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list enterprise inquiries")
}
return result, nil
@@ -774,7 +774,7 @@ func (s *adminService) ListEnterpriseInquiries(ctx context.Context, search *mode
func (s *adminService) GetEnterpriseInquiry(ctx context.Context, id uuid.UUID) (*models.AdminEnterpriseInquiry, *errx.Error) {
inquiry, err := s.repo.GetEnterpriseInquiry(ctx, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get enterprise inquiry")
}
if inquiry == nil {
@@ -785,7 +785,7 @@ func (s *adminService) GetEnterpriseInquiry(ctx context.Context, id uuid.UUID) (
func (s *adminService) UpdateEnterpriseInquiry(ctx context.Context, adminID, inquiryID uuid.UUID, update *models.UpdateEnterpriseInquiryRequest, ipAddress, userAgent string) *errx.Error {
if err := s.repo.UpdateEnterpriseInquiry(ctx, inquiryID, update); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to update enterprise inquiry")
}
@@ -798,7 +798,7 @@ func (s *adminService) UpdateEnterpriseInquiry(ctx context.Context, adminID, inq
func (s *adminService) ListAdmins(ctx context.Context, cursor *uuid.UUID, limit int) (*models.AdminsResult, *errx.Error) {
result, err := s.repo.ListAdmins(ctx, cursor, limit)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list admins")
}
return result, nil
@@ -811,7 +811,7 @@ func (s *adminService) GrantAdminPermissions(ctx context.Context, adminID, targe
}
if err := s.repo.UpdateUserAdminPermissions(ctx, targetUserID, uint32(permissions), adminID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to grant admin permissions")
}
@@ -826,7 +826,7 @@ func (s *adminService) RevokeAdminPermissions(ctx context.Context, adminID, targ
}
if err := s.repo.UpdateUserAdminPermissions(ctx, targetUserID, 0, adminID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to revoke admin permissions")
}
@@ -839,7 +839,7 @@ func (s *adminService) RevokeAdminPermissions(ctx context.Context, adminID, targ
func (s *adminService) SearchAuditLogs(ctx context.Context, search *models.AdminAuditLogSearch) (*models.AdminAuditLogsResult, *errx.Error) {
result, err := s.repo.SearchAuditLogs(ctx, search)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to search audit logs")
}
return result, nil
+8 -8
View File
@@ -9,11 +9,11 @@ package adminoutreach
import (
"context"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/notify"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/repository"
)
@@ -65,12 +65,12 @@ func (s *service) Send(ctx context.Context, adminID uuid.UUID, req *models.SendA
}
if err := s.repo.Insert(ctx, m); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to record outreach")
}
if err := s.mailer.SendOutreach(ctx, []string{to}, req.ReplyTo, req.Subject, req.Body); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
_ = s.repo.MarkFailed(ctx, m.ID, err.Error())
errStr := err.Error()
m.Status = models.AdminOutreachStatusFailed
@@ -81,7 +81,7 @@ func (s *service) Send(ctx context.Context, adminID uuid.UUID, req *models.SendA
if err := s.repo.MarkSent(ctx, m.ID); err != nil {
// Mail went out; audit row stuck in queued. Log and return
// success so the admin isn't confused into re-sending.
sentry.CaptureException(err)
errs.CaptureException(err)
}
m.Status = models.AdminOutreachStatusSent
return m, nil
@@ -90,7 +90,7 @@ func (s *service) Send(ctx context.Context, adminID uuid.UUID, req *models.SendA
func (s *service) Search(ctx context.Context, search *models.AdminOutreachSearch) (*models.AdminOutreachResult, *errx.Error) {
result, err := s.repo.Search(ctx, search)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load outreach log")
}
return result, nil
@@ -117,7 +117,7 @@ func (s *service) resolveRecipient(ctx context.Context, req *models.SendAdminOut
if req.ToUserID != nil {
u, err := s.userRepo.GetUser(ctx, *req.ToUserID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.New(errx.Internal, "failed to load user")
}
if u == nil {
@@ -128,7 +128,7 @@ func (s *service) resolveRecipient(ctx context.Context, req *models.SendAdminOut
// to_org_id → owner's email
org, err := s.orgRepo.GetByID(ctx, *req.ToOrgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.New(errx.Internal, "failed to load organization")
}
if org == nil {
@@ -136,7 +136,7 @@ func (s *service) resolveRecipient(ctx context.Context, req *models.SendAdminOut
}
u, err := s.userRepo.GetUser(ctx, org.OwnerUserID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.New(errx.Internal, "failed to load owner")
}
if u == nil {
+4 -4
View File
@@ -4,11 +4,11 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/infrastructure/pubsub"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/repository"
)
@@ -90,11 +90,11 @@ func (s *auditService) LogAction(ctx context.Context, orgID, actorID uuid.UUID,
go func() {
defer func() {
if r := recover(); r != nil {
sentry.CurrentHub().Recover(r)
errs.Recover(r)
}
}()
if err := s.repo.Log(context.Background(), log); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return
}
// Notify the org's dashboard to refetch the activity log live. Carries
@@ -138,7 +138,7 @@ func (s *auditService) Search(ctx context.Context, params *models.AuditLogSearch
result, err := s.repo.Search(ctx, params)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
+16 -16
View File
@@ -7,11 +7,11 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/crypt"
)
@@ -47,12 +47,12 @@ func getResetPasswordSessionKey(sessionID uuid.UUID) string {
func (s *authService) saveLoginSession(ctx context.Context, sessionID uuid.UUID, session *models.LoginSession, expiresAt time.Time) *errx.Error {
data, err := json.Marshal(session)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if err := s.cache.Set(ctx, getLoginSessionKey(sessionID), data, time.Until(expiresAt)).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
@@ -65,13 +65,13 @@ func (s *authService) getLoginSession(ctx context.Context, sessionID uuid.UUID)
if errors.Is(err, redis.Nil) {
return nil, nil
}
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
var session models.LoginSession
if err := json.Unmarshal(data, &session); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -81,12 +81,12 @@ func (s *authService) getLoginSession(ctx context.Context, sessionID uuid.UUID)
func (s *authService) saveRegistrationSession(ctx context.Context, sessionID uuid.UUID, session *models.RegistrationSession, expiresAt time.Time) *errx.Error {
data, err := json.Marshal(session)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if err := s.cache.Set(ctx, getRegistrationSessionKey(sessionID), data, time.Until(expiresAt)).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
@@ -99,13 +99,13 @@ func (s *authService) getRegistrationSession(ctx context.Context, sessionID uuid
if errors.Is(err, redis.Nil) {
return nil, nil
}
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
var session models.RegistrationSession
if err := json.Unmarshal(data, &session); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -117,13 +117,13 @@ func (s *authService) canSendEmail(ctx context.Context, flow, email string) *err
count, err := s.cache.Incr(ctx, key).Result()
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if count == 1 {
if err := s.cache.Expire(ctx, key, AuthEmailTTL).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
}
@@ -140,13 +140,13 @@ func (s *authService) passwordResetLimit(ctx context.Context, email string) *err
count, err := s.cache.Incr(ctx, key).Result()
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if count == 1 {
if err := s.cache.Expire(ctx, key, PasswordResetLimitTTL).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
}
@@ -164,7 +164,7 @@ func (s *authService) passwordResetLimit(ctx context.Context, email string) *err
// token and a mail that promised 4 hours.
func (s *authService) saveResetPasswordSession(ctx context.Context, sessionID uuid.UUID, nonce string) *errx.Error {
if err := s.cache.SetEx(ctx, getResetPasswordSessionKey(sessionID), nonce, PasswordResetTTL).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
@@ -178,7 +178,7 @@ func (s *authService) getResetPasswordSession(ctx context.Context, sessionID uui
if errors.Is(err, redis.Nil) {
return "", errx.ErrToken
}
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.InternalError()
}
@@ -220,7 +220,7 @@ func deviceFingerprint(userAgent string) string {
func (s *authService) deletePasswordResetSession(ctx context.Context, sessionID uuid.UUID) *errx.Error {
val, err := s.cache.Del(ctx, getResetPasswordSessionKey(sessionID)).Result()
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
+8 -8
View File
@@ -5,11 +5,11 @@ import (
"errors"
"net/mail"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/app/token"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/idtoken"
)
@@ -90,7 +90,7 @@ func (s *authService) resolveFederatedUser(ctx context.Context, provider, issuer
if s.identities != nil && issuer != "" && subject != "" {
existing, ierr := s.identities.FindUserByIdentity(ctx, issuer, subject)
if ierr != nil {
sentry.CaptureException(ierr)
errs.CaptureException(ierr)
return uuid.Nil, errx.InternalError()
}
if existing != uuid.Nil {
@@ -101,7 +101,7 @@ func (s *authService) resolveFederatedUser(ctx context.Context, provider, issuer
u, uerr := s.userRepository.GetUserByEmail(ctx, email.Address)
if uerr != nil && !errors.Is(uerr, errx.ErrUser) {
sentry.CaptureException(uerr)
errs.CaptureException(uerr)
return uuid.Nil, errx.InternalError()
}
@@ -115,13 +115,13 @@ func (s *authService) resolveFederatedUser(ctx context.Context, provider, issuer
var cerr error
u, cerr = s.createExternalUser(ctx, email, firstName, lastName)
if cerr != nil {
sentry.CaptureException(cerr)
errs.CaptureException(cerr)
return uuid.Nil, errx.InternalError()
}
} else if s.identities != nil && issuer != "" {
linked, herr := s.identities.HasIdentityForIssuer(ctx, u.ID, issuer)
if herr != nil {
sentry.CaptureException(herr)
errs.CaptureException(herr)
return uuid.Nil, errx.InternalError()
}
if linked {
@@ -138,7 +138,7 @@ func (s *authService) resolveFederatedUser(ctx context.Context, provider, issuer
}); lerr != nil {
// A unique-index violation means another account already owns this
// identity. Refuse rather than sign anyone in.
sentry.CaptureException(lerr)
errs.CaptureException(lerr)
return uuid.Nil, errx.New(errx.Forbidden, "that identity is already linked to another account")
}
}
@@ -183,14 +183,14 @@ func (s *authService) createExternalUser(ctx context.Context, email *mail.Addres
var orgErr *errx.Error
org, orgErr = s.organizationService.Create(ctx, u.ID, orgName)
if orgErr != nil {
sentry.CaptureException(orgErr)
errs.CaptureException(orgErr)
// Don't fail the sign-in if org creation fails.
}
}
if s.trialService != nil && org != nil {
if terr := s.trialService.StartFreeTrialWithOrg(ctx, u.ID, org.ID); terr != nil {
sentry.CaptureException(terr)
errs.CaptureException(terr)
// Don't fail the sign-in if trial creation fails.
}
}
+3 -3
View File
@@ -4,10 +4,10 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/argon2"
)
@@ -19,7 +19,7 @@ func (s *authService) GenerateLoginSession(ctx context.Context, userID uuid.UUID
codeHash, err := argon2.Hash(code)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.InternalError()
}
@@ -34,7 +34,7 @@ func (s *authService) GenerateLoginSession(ctx context.Context, userID uuid.UUID
sessionToken, err := s.tokenService.GenerateToken(userID, sessID, "", "", issuedAt, expiresAt)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.InternalError()
}
+9 -9
View File
@@ -5,13 +5,13 @@ import (
"github.com/warmbly/warmbly/internal/app/authrisk"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/app/token"
"github.com/warmbly/warmbly/internal/config"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/notify/templates"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/argon2"
"github.com/warmbly/warmbly/internal/pkg/crypt"
)
@@ -22,7 +22,7 @@ func (s *authService) LoginStart(ctx context.Context, data *AuthData, ipaddr, us
}
if xerr := s.captcha.Verify(ctx, data.Turnstile, ipaddr); xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, xerr
}
@@ -62,30 +62,30 @@ func (s *authService) LoginStart(ctx context.Context, data *AuthData, ipaddr, us
sessionID := uuid.New()
nonce, xerr := crypt.Nonce()
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
code, xerr := crypt.VerificationCode()
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
text, xerr := templates.GenerateLoginCodeHTML(code)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
if xerr := s.sendAuthEmail(ctx, data.Email, "Your Login Code", text); xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.ErrMailUndeliverable
}
codeHash, xerr := argon2.Hash(code)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
@@ -97,7 +97,7 @@ func (s *authService) LoginStart(ctx context.Context, data *AuthData, ipaddr, us
sessionToken, xerr := s.tokenService.GenerateToken(uid, sessionID, "", nonce, issuedAt, expiresAt)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
@@ -154,7 +154,7 @@ func (s *authService) LoginConfirm(ctx context.Context, data *ConfirmData, sessi
v, xerr := argon2.Verify(data.Code, sess.CodeHash)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
+7 -7
View File
@@ -5,9 +5,9 @@ import (
"net/mail"
"strings"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/app/orgrisk"
"github.com/warmbly/warmbly/internal/config"
@@ -43,7 +43,7 @@ func (s *authService) createAccount(ctx context.Context, address, passwordHash,
u, xerr := s.userRepository.CreateUser(ctx, email, passwordHash)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
@@ -79,7 +79,7 @@ func (s *authService) createAccount(ctx context.Context, address, passwordHash,
var orgErr *errx.Error
org, orgErr = s.organizationService.Create(ctx, u.ID, orgName)
if orgErr != nil {
sentry.CaptureException(orgErr)
errs.CaptureException(orgErr)
// Don't fail registration if org creation fails
}
}
@@ -95,7 +95,7 @@ func (s *authService) createAccount(ctx context.Context, address, passwordHash,
// Start 2-week free trial for new user (linked to organization)
if s.trialService != nil && org != nil {
if err := s.trialService.StartFreeTrialWithOrg(ctx, u.ID, org.ID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
// Don't fail registration if trial creation fails
}
}
@@ -104,7 +104,7 @@ func (s *authService) createAccount(ctx context.Context, address, passwordHash,
// Best-effort: a bad or self-referral code never fails registration.
if s.referral != nil && org != nil && referralCode != "" {
if xerr := s.referral.AttributeSignup(ctx, referralCode, org.ID, u.ID); xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
}
}
@@ -158,7 +158,7 @@ func (s *authService) signupAllowed(ctx context.Context, address, invite string)
if s.policy.Registration != config.RegistrationClosed {
empty, err := s.userRepository.IsEmpty(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if empty {
@@ -236,7 +236,7 @@ func (s *authService) federatedSignupAllowed(ctx context.Context, address string
if s.policy.Registration != config.RegistrationClosed {
empty, err := s.userRepository.IsEmpty(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if empty {
+10 -10
View File
@@ -4,11 +4,11 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/notify/templates"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/argon2"
"github.com/warmbly/warmbly/internal/pkg/crypt"
)
@@ -24,7 +24,7 @@ func (s *authService) RegistrationStart(ctx context.Context, data *AuthData, ori
}
if xerr := s.captcha.Verify(ctx, data.Turnstile, ipaddr); xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, xerr
}
@@ -34,7 +34,7 @@ func (s *authService) RegistrationStart(ctx context.Context, data *AuthData, ori
passwordHash, xerr := argon2.Hash(data.Password)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
@@ -58,30 +58,30 @@ func (s *authService) RegistrationStart(ctx context.Context, data *AuthData, ori
sessionID := uuid.New()
nonce, xerr := crypt.Nonce()
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
code, xerr := crypt.VerificationCode()
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
text, xerr := templates.GenerateRegistrationCodeHTML(code)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
if xerr := s.sendAuthEmail(ctx, data.Email, "Your Verification Code", text); xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.ErrMailUndeliverable
}
codeHash, xerr := argon2.Hash(code)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
@@ -99,7 +99,7 @@ func (s *authService) RegistrationStart(ctx context.Context, data *AuthData, ori
sessionToken, xerr := s.tokenService.GenerateToken(uuid.Nil, sessionID, data.Email, nonce, issuedAt, expiresAt)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
@@ -131,7 +131,7 @@ func (s *authService) RegistrationConfirm(ctx context.Context, data *ConfirmData
v, xerr := argon2.Verify(data.Code, sess.CodeHash)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
+12 -12
View File
@@ -4,18 +4,18 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/config"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/notify/templates"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/argon2"
"github.com/warmbly/warmbly/internal/pkg/crypt"
)
func (s *authService) ResetPasswordStart(ctx context.Context, data *ResetPasswordStart, ipaddr string) *errx.Error {
if err := s.captcha.Verify(ctx, data.Turnstile, ipaddr); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
@@ -41,7 +41,7 @@ func (s *authService) ResetPasswordStart(ctx context.Context, data *ResetPasswor
sessionID := uuid.New()
nonce, err := crypt.Nonce()
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
@@ -50,7 +50,7 @@ func (s *authService) ResetPasswordStart(ctx context.Context, data *ResetPasswor
token, err := s.tokenService.GenerateToken(user.ID, sessionID, data.Email, nonce, issuedAt, expiresAt)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
@@ -62,12 +62,12 @@ func (s *authService) ResetPasswordStart(ctx context.Context, data *ResetPasswor
text, err := templates.GenerateResetPasswordHTML(u.FirstName, url)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if err := s.sendAuthEmail(ctx, u.Email, "Password Reset Confirmation", text); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.ErrMailUndeliverable
}
@@ -76,7 +76,7 @@ func (s *authService) ResetPasswordStart(ctx context.Context, data *ResetPasswor
func (s *authService) ResetPasswordConfirm(ctx context.Context, data *ResetPasswordConfirm, session, ipaddr string) *errx.Error {
if err := s.captcha.Verify(ctx, data.Turnstile, ipaddr); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
@@ -108,7 +108,7 @@ func (s *authService) ResetPasswordConfirm(ctx context.Context, data *ResetPassw
passwordHash, hashErr := argon2.Hash(data.Password)
if hashErr != nil {
sentry.CaptureException(hashErr)
errs.CaptureException(hashErr)
return errx.InternalError()
}
@@ -121,7 +121,7 @@ func (s *authService) ResetPasswordConfirm(ctx context.Context, data *ResetPassw
// none, so all are revoked) so a reset always fully cuts off prior access.
if s.tokenService != nil {
if err := s.tokenService.RevokeOtherSessions(ctx, sess.UserID, uuid.Nil); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
// Non-fatal: the password is already reset.
}
}
@@ -143,7 +143,7 @@ func (s *authService) ChangePassword(ctx context.Context, userID, currentSession
ok, verr := argon2.Verify(data.CurrentPassword, hash)
if verr != nil {
sentry.CaptureException(verr)
errs.CaptureException(verr)
return errx.InternalError()
}
if !ok {
@@ -159,7 +159,7 @@ func (s *authService) ChangePassword(ctx context.Context, userID, currentSession
newHash, hashErr := argon2.Hash(data.NewPassword)
if hashErr != nil {
sentry.CaptureException(hashErr)
errs.CaptureException(hashErr)
return errx.InternalError()
}
if err := s.authRepository.ResetPassword(ctx, userID, newHash); err != nil {
@@ -172,7 +172,7 @@ func (s *authService) ChangePassword(ctx context.Context, userID, currentSession
// they just performed.
if s.tokenService != nil && currentSessionID != uuid.Nil {
if err := s.tokenService.RevokeOtherSessions(ctx, userID, currentSessionID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
// Non-fatal: the password is already changed.
}
}
+13 -13
View File
@@ -9,10 +9,10 @@ import (
"net/mail"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/app/token"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/idtoken"
)
@@ -140,16 +140,16 @@ func (s *authService) FederatedProviderLabels() map[string]string {
func (s *authService) mintHandoff(ctx context.Context, result *models.LoginResult, binding string) (string, *errx.Error) {
code, err := randomHex(32)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.InternalError()
}
payload, err := json.Marshal(ssoHandoff{Binding: binding, Result: *result})
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.InternalError()
}
if err := s.cache.SetEx(ctx, ssoHandoffKey(code), payload, ssoHandoffTTL).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.InternalError()
}
return code, nil
@@ -173,7 +173,7 @@ func (s *authService) SSOExchange(ctx context.Context, code, binding string) (*m
}
var handoff ssoHandoff
if err := json.Unmarshal(raw, &handoff); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
if subtle.ConstantTimeCompare([]byte(handoff.Binding), []byte(binding)) != 1 {
@@ -191,32 +191,32 @@ func (s *authService) SSOBegin(ctx context.Context, provider string) (*SSORedire
state, err := randomHex(32)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
nonce, err := randomHex(32)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
verifier, err := randomHex(32)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
binding, err := randomHex(32)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
payload, err := json.Marshal(ssoFlow{Provider: provider, Verifier: verifier, Nonce: nonce, Binding: binding})
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
if err := s.cache.SetEx(ctx, ssoStateKey(state), payload, ssoStateTTL).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -242,7 +242,7 @@ func (s *authService) SSOCallbackComplete(ctx context.Context, in SSOCallback) (
var flow ssoFlow
if err := json.Unmarshal(raw, &flow); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.InternalError()
}
// A state minted for one provider presented at another's callback is a
@@ -255,7 +255,7 @@ func (s *authService) SSOCallbackComplete(ctx context.Context, in SSOCallback) (
if err != nil {
// Reported, not buried: these are configuration problems more often
// than attacks.
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.ErrExternalCode
}
+6 -6
View File
@@ -26,7 +26,6 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/app/organization"
"github.com/warmbly/warmbly/internal/app/trial"
"github.com/warmbly/warmbly/internal/app/user"
@@ -34,6 +33,7 @@ import (
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/infrastructure/cache"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/argon2"
"github.com/warmbly/warmbly/internal/pkg/crypt"
"github.com/warmbly/warmbly/internal/repository"
@@ -269,7 +269,7 @@ func (s *Service) Claim(ctx context.Context, token, address, password, firstName
// token: a stale link out of an old log must never mint a second owner.
empty, eerr := s.users.IsEmpty(ctx)
if eerr != nil {
sentry.CaptureException(eerr)
errs.CaptureException(eerr)
return nil, errx.InternalError()
}
if !empty {
@@ -295,13 +295,13 @@ func (s *Service) Claim(ctx context.Context, token, address, password, firstName
hash, herr := argon2.Hash(password)
if herr != nil {
sentry.CaptureException(herr)
errs.CaptureException(herr)
return nil, errx.InternalError()
}
u, uerr := s.users.CreateUser(ctx, parsed, hash)
if uerr != nil {
sentry.CaptureException(uerr)
errs.CaptureException(uerr)
return nil, errx.InternalError()
}
if firstName != "" {
@@ -319,7 +319,7 @@ func (s *Service) Claim(ctx context.Context, token, address, password, firstName
}
org, orgErr := s.orgSvc.Create(ctx, u.ID, orgName)
if orgErr != nil {
sentry.CaptureException(orgErr)
errs.CaptureException(orgErr)
return nil, errx.InternalError()
}
if s.trialSvc != nil {
@@ -327,7 +327,7 @@ func (s *Service) Claim(ctx context.Context, token, address, password, firstName
}
if err := s.adminRepo.GrantBootstrapAdmin(ctx, u.ID, uint32(models.AllAdminPermissions)); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
+11 -11
View File
@@ -11,7 +11,6 @@ import (
"time"
"unicode/utf8"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/app/dailythrottle"
"github.com/warmbly/warmbly/internal/app/listgate"
@@ -21,6 +20,7 @@ import (
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/infrastructure/pubsub"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/trackdns"
"github.com/warmbly/warmbly/internal/repository"
"github.com/warmbly/warmbly/internal/scheduler"
@@ -167,7 +167,7 @@ func (s *campaignService) Delete(ctx context.Context, orgID uuid.UUID, campaignI
if s.attachmentRepo != nil {
var err error
if attachments, err = s.attachmentRepo.ListByCampaign(ctx, cID); err != nil {
sentry.CaptureException(fmt.Errorf("campaign %s delete: list attachments: %w", cID, err))
errs.CaptureException(fmt.Errorf("campaign %s delete: list attachments: %w", cID, err))
}
}
@@ -181,7 +181,7 @@ func (s *campaignService) Delete(ctx context.Context, orgID uuid.UUID, campaignI
if s.storage != nil {
for _, att := range attachments {
if err := s.storage.Delete(ctx, att.S3Key); err != nil {
sentry.CaptureException(fmt.Errorf("campaign %s delete: object %s: %w", cID, att.S3Key, err))
errs.CaptureException(fmt.Errorf("campaign %s delete: object %s: %w", cID, att.S3Key, err))
}
}
}
@@ -333,14 +333,14 @@ func (s *campaignService) copyAttachments(ctx context.Context, orgID, src, dst u
for _, att := range sources {
body, err := s.storage.Get(ctx, att.S3Key)
if err != nil {
sentry.CaptureException(fmt.Errorf("campaign %s duplicate: read %s: %w", src, att.S3Key, err))
errs.CaptureException(fmt.Errorf("campaign %s duplicate: read %s: %w", src, att.S3Key, err))
continue
}
key := models.AttachmentObjectKey(dst, att.Filename)
err = s.storage.Put(ctx, key, body, att.MimeType)
body.Close()
if err != nil {
sentry.CaptureException(fmt.Errorf("campaign %s duplicate: write %s: %w", src, key, err))
errs.CaptureException(fmt.Errorf("campaign %s duplicate: write %s: %w", src, key, err))
continue
}
att.S3Key = key
@@ -353,7 +353,7 @@ func (s *campaignService) copyAttachments(ctx context.Context, orgID, src, dst u
defer cancel()
for _, att := range copied {
if err := s.storage.Delete(cleanup, att.S3Key); err != nil {
sentry.CaptureException(fmt.Errorf("campaign %s duplicate undo: object %s: %w", src, att.S3Key, err))
errs.CaptureException(fmt.Errorf("campaign %s duplicate undo: object %s: %w", src, att.S3Key, err))
}
}
}, nil
@@ -664,7 +664,7 @@ func (s *campaignService) KeepRunning(ctx context.Context, orgID, campaignID uui
if errors.Is(err, errx.ErrResourceNotFound) {
return errx.ErrNotFound
}
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if !transitioned {
@@ -775,7 +775,7 @@ func (s *campaignService) enqueueCampaignWakeup(ctx context.Context, campaignID
_ = s.campaignRepository.UpdateStatusWithLock(ctx, campaignID, "completed")
return errx.New(errx.BadRequest, "campaign is past its end date; extend or clear the end date to keep sending")
default:
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
}
@@ -795,7 +795,7 @@ func (s *campaignService) enqueueCampaignWakeup(ctx context.Context, campaignID
created, err := s.taskRepo.CreateTaskWithLock(ctx, task, campaignTask)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if !created {
@@ -807,11 +807,11 @@ func (s *campaignService) enqueueCampaignWakeup(ctx context.Context, campaignID
if err != nil {
_ = s.taskRepo.DeleteTask(ctx, taskID)
_ = s.campaignRepository.StopCampaign(ctx, campaignID)
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.ServiceUnavailable, "could not schedule campaign right now")
}
if err := s.taskRepo.UpdateTaskScheduledAt(ctx, taskID, nextTime, cloudTaskName); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
+2 -2
View File
@@ -3,8 +3,8 @@ package cipher
import (
"context"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/observability/errs"
)
type Cipher struct {
@@ -44,7 +44,7 @@ func (s *cipherService) Cipher(ctx context.Context, orgID uuid.UUID) (*Cipher, e
}
if err := s.saveDecryptedKey(ctx, orgID, key); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
return &Cipher{
+5 -6
View File
@@ -1,14 +1,13 @@
package jobs
import (
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func CaptureError(userID, emailID uuid.UUID, err error) {
sentry.WithScope(func(scope *sentry.Scope) {
scope.SetTag("user_id", userID.String())
scope.SetTag("email_id", emailID.String())
sentry.CaptureException(err)
})
errs.CaptureException(err,
errs.Tag("user_id", userID.String()),
errs.Tag("email_id", emailID.String()),
)
}
+2 -2
View File
@@ -16,10 +16,10 @@ import (
"fmt"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/infrastructure/cache"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// Resource enumerates the actions the throttle bounds. Keeping the
@@ -66,7 +66,7 @@ func (s *service) CheckAndIncrement(ctx context.Context, scope uuid.UUID, res Re
count, err := s.cache.Incr(ctx, key).Result()
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil // fail-open
}
// On the very first hit the TTL is unset; set a 25h floor so the
+24 -24
View File
@@ -15,8 +15,8 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
@@ -90,7 +90,7 @@ func (s *service) ScheduleOrganizationDeletion(ctx context.Context, orgID, reque
org, err := s.orgRepo.GetByID(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load organization")
}
if org == nil {
@@ -127,7 +127,7 @@ func (s *service) ScheduleOrganizationDeletion(ctx context.Context, orgID, reque
if errors.Is(err, repository.ErrPendingDeletionExists) {
return nil, errx.New(errx.Conflict, "organization is already scheduled for deletion")
}
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to schedule organization deletion")
}
@@ -140,7 +140,7 @@ func (s *service) ScheduleOrganizationDeletion(ctx context.Context, orgID, reque
func (s *service) CancelOrganizationDeletion(ctx context.Context, orgID, requesterUserID uuid.UUID, req *models.CancelDeletionRequest) *errx.Error {
org, err := s.orgRepo.GetByID(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to load organization")
}
if org == nil {
@@ -152,7 +152,7 @@ func (s *service) CancelOrganizationDeletion(ctx context.Context, orgID, request
d, err := s.repo.GetActive(ctx, models.DeletionResourceOrganization, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to load pending deletion")
}
if d == nil {
@@ -165,7 +165,7 @@ func (s *service) CancelOrganizationDeletion(ctx context.Context, orgID, request
}
if err := s.repo.Cancel(ctx, d.ID, requesterUserID, reason); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to cancel deletion")
}
@@ -176,7 +176,7 @@ func (s *service) CancelOrganizationDeletion(ctx context.Context, orgID, request
func (s *service) GetOrganizationStatus(ctx context.Context, orgID uuid.UUID) (*models.DangerZoneStatus, *errx.Error) {
org, err := s.orgRepo.GetByID(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load organization")
}
if org == nil {
@@ -193,7 +193,7 @@ func (s *service) GetOrganizationStatus(ctx context.Context, orgID uuid.UUID) (*
d, err := s.repo.GetActive(ctx, models.DeletionResourceOrganization, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load pending deletion")
}
status.PendingDeletion = d
@@ -209,7 +209,7 @@ func (s *service) ScheduleUserDeletion(ctx context.Context, userID uuid.UUID, re
user, err := s.userRepo.GetUser(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load user")
}
if user == nil {
@@ -241,7 +241,7 @@ func (s *service) ScheduleUserDeletion(ctx context.Context, userID uuid.UUID, re
if errors.Is(err, repository.ErrPendingDeletionExists) {
return nil, errx.New(errx.Conflict, "account is already scheduled for deletion")
}
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to schedule account deletion")
}
@@ -254,7 +254,7 @@ func (s *service) ScheduleUserDeletion(ctx context.Context, userID uuid.UUID, re
func (s *service) CancelUserDeletion(ctx context.Context, userID uuid.UUID, req *models.CancelDeletionRequest) *errx.Error {
user, err := s.userRepo.GetUser(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to load user")
}
if user == nil {
@@ -263,7 +263,7 @@ func (s *service) CancelUserDeletion(ctx context.Context, userID uuid.UUID, req
d, err := s.repo.GetActive(ctx, models.DeletionResourceUser, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to load pending deletion")
}
if d == nil {
@@ -276,7 +276,7 @@ func (s *service) CancelUserDeletion(ctx context.Context, userID uuid.UUID, req
}
if err := s.repo.Cancel(ctx, d.ID, userID, reason); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to cancel deletion")
}
@@ -287,7 +287,7 @@ func (s *service) CancelUserDeletion(ctx context.Context, userID uuid.UUID, req
func (s *service) GetUserStatus(ctx context.Context, userID uuid.UUID) (*models.DangerZoneStatus, *errx.Error) {
user, err := s.userRepo.GetUser(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load user")
}
if user == nil {
@@ -304,7 +304,7 @@ func (s *service) GetUserStatus(ctx context.Context, userID uuid.UUID) (*models.
d, err := s.repo.GetActive(ctx, models.DeletionResourceUser, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load pending deletion")
}
status.PendingDeletion = d
@@ -334,7 +334,7 @@ func (s *service) ExecuteDuePendingDeletions(ctx context.Context) (int, int, err
// one transitions pending -> executing.
claimed, err := s.repo.MarkExecuting(ctx, d.ID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
failed++
continue
}
@@ -343,14 +343,14 @@ func (s *service) ExecuteDuePendingDeletions(ctx context.Context) (int, int, err
}
if err := s.runHardDelete(ctx, &d); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
_ = s.repo.MarkFailed(ctx, d.ID, err.Error())
failed++
continue
}
if err := s.repo.MarkCompleted(ctx, d.ID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
failed++
continue
}
@@ -396,7 +396,7 @@ func (s *service) DispatchReminders(ctx context.Context) error {
d := batch[i]
s.sendReminderEmail(ctx, &d, t.bit)
if err := s.repo.SetNotifBit(ctx, d.ID, t.bit); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
}
@@ -447,7 +447,7 @@ func (s *service) sendUserScheduledEmail(ctx context.Context, user *models.User,
return
}
if err := s.notifier.Send(ctx, []string{user.Email}, nil, nil, subject, body); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
@@ -461,7 +461,7 @@ func (s *service) sendUserCancelledEmail(ctx context.Context, user *models.User,
return
}
if err := s.notifier.Send(ctx, []string{user.Email}, nil, nil, subject, body); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
@@ -550,7 +550,7 @@ func (s *service) sendCompletionEmail(ctx context.Context, d *models.ScheduledDe
return
}
if err := s.notifier.Send(ctx, []string{requester.Email}, nil, nil, subject, body); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
@@ -600,7 +600,7 @@ func (s *service) orgRecipients(ctx context.Context, org *models.Organization) [
members, err := s.orgRepo.GetMembers(ctx, org.ID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return out
}
for i := range members {
@@ -624,7 +624,7 @@ func (s *service) orgRecipients(ctx context.Context, org *models.Organization) [
func (s *service) sendToEach(ctx context.Context, recipients []string, subject, body string) {
for _, to := range recipients {
if err := s.notifier.Send(ctx, []string{to}, nil, nil, subject, body); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
}
+18 -18
View File
@@ -6,10 +6,10 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/repository"
)
@@ -83,7 +83,7 @@ func NormalizeCode(code string) string {
func (s *service) List(ctx context.Context, search *models.AdminDiscountSearch) (*models.AdminDiscountsResult, *errx.Error) {
result, err := s.codeRepo.List(ctx, search)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list discount codes")
}
return result, nil
@@ -92,7 +92,7 @@ func (s *service) List(ctx context.Context, search *models.AdminDiscountSearch)
func (s *service) Get(ctx context.Context, id uuid.UUID) (*models.DiscountCode, *errx.Error) {
code, err := s.codeRepo.GetByID(ctx, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get discount code")
}
if code == nil {
@@ -156,7 +156,7 @@ func (s *service) Create(ctx context.Context, adminID uuid.UUID, req *models.Cre
existing, err := s.codeRepo.GetByCode(ctx, code)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to check existing code")
}
if existing != nil {
@@ -164,7 +164,7 @@ func (s *service) Create(ctx context.Context, adminID uuid.UUID, req *models.Cre
}
if err := s.codeRepo.Create(ctx, dc); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to create discount code")
}
@@ -175,7 +175,7 @@ func (s *service) Create(ctx context.Context, adminID uuid.UUID, req *models.Cre
func (s *service) Update(ctx context.Context, adminID, id uuid.UUID, req *models.UpdateDiscountCodeRequest, ipAddress, userAgent string) (*models.DiscountCode, *errx.Error) {
dc, err := s.codeRepo.GetByID(ctx, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get discount code")
}
if dc == nil {
@@ -239,7 +239,7 @@ func (s *service) Update(ctx context.Context, adminID, id uuid.UUID, req *models
}
if err := s.codeRepo.Update(ctx, dc); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to update discount code")
}
@@ -250,14 +250,14 @@ func (s *service) Update(ctx context.Context, adminID, id uuid.UUID, req *models
func (s *service) Delete(ctx context.Context, adminID, id uuid.UUID, ipAddress, userAgent string) *errx.Error {
dc, err := s.codeRepo.GetByID(ctx, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to get discount code")
}
if dc == nil {
return errx.ErrNotFound
}
if err := s.codeRepo.Delete(ctx, id); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to delete discount code")
}
s.log(ctx, adminID, "delete_discount_code", id, map[string]any{"code": dc.Code}, ipAddress, userAgent)
@@ -267,7 +267,7 @@ func (s *service) Delete(ctx context.Context, adminID, id uuid.UUID, ipAddress,
func (s *service) ListRedemptions(ctx context.Context, codeID uuid.UUID, offset, limit int) (*models.AdminDiscountRedemptionsResult, *errx.Error) {
result, err := s.redRepo.ListByCode(ctx, codeID, offset, limit)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list redemptions")
}
return result, nil
@@ -276,7 +276,7 @@ func (s *service) ListRedemptions(ctx context.Context, codeID uuid.UUID, offset,
func (s *service) ListOrganizationRedemptions(ctx context.Context, orgID uuid.UUID, limit int) ([]models.DiscountRedemption, *errx.Error) {
rows, err := s.redRepo.ListByOrganization(ctx, orgID, limit)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list redemptions")
}
return rows, nil
@@ -342,7 +342,7 @@ func (s *service) resolve(ctx context.Context, orgID uuid.UUID, code string, pla
dc, err := s.codeRepo.GetByCode(ctx, normalized)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, "", errx.New(errx.Internal, "failed to validate discount code")
}
if dc == nil {
@@ -380,7 +380,7 @@ func (s *service) resolve(ctx context.Context, orgID uuid.UUID, code string, pla
orgCount, err := s.redRepo.CountActiveByCodeAndOrg(ctx, dc.ID, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, "", errx.New(errx.Internal, "failed to validate discount code")
}
if dc.PerAccountLimit > 0 && orgCount >= dc.PerAccountLimit {
@@ -424,7 +424,7 @@ func (s *service) reserve(ctx context.Context, code *models.DiscountCode, orgID
case errors.Is(err, repository.ErrDiscountAlreadyRedeemed):
return uuid.Nil, errx.New(errx.BadRequest, "You've already used this discount code.")
case err != nil:
sentry.CaptureException(err)
errs.CaptureException(err)
return uuid.Nil, errx.New(errx.Internal, "failed to record discount redemption")
}
return red.ID, nil
@@ -432,7 +432,7 @@ func (s *service) reserve(ctx context.Context, code *models.DiscountCode, orgID
func (s *service) AttachRedemptionStripe(ctx context.Context, redemptionID uuid.UUID, sessionID, couponID *string) *errx.Error {
if err := s.redRepo.AttachStripeRefs(ctx, redemptionID, sessionID, couponID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to attach redemption references")
}
return nil
@@ -440,7 +440,7 @@ func (s *service) AttachRedemptionStripe(ctx context.Context, redemptionID uuid.
func (s *service) CancelRedemptionByID(ctx context.Context, redemptionID uuid.UUID) *errx.Error {
if err := s.redRepo.CancelByID(ctx, redemptionID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to cancel redemption")
}
return nil
@@ -448,7 +448,7 @@ func (s *service) CancelRedemptionByID(ctx context.Context, redemptionID uuid.UU
func (s *service) MarkRedemptionApplied(ctx context.Context, sessionID string, subscriptionID *uuid.UUID) *errx.Error {
if err := s.redRepo.MarkAppliedBySession(ctx, sessionID, subscriptionID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to mark redemption applied")
}
return nil
@@ -456,7 +456,7 @@ func (s *service) MarkRedemptionApplied(ctx context.Context, sessionID string, s
func (s *service) CancelRedemption(ctx context.Context, sessionID string) *errx.Error {
if err := s.redRepo.CancelBySession(ctx, sessionID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to cancel redemption")
}
return nil
+6 -6
View File
@@ -6,10 +6,10 @@ import (
"errors"
"time"
"github.com/getsentry/sentry-go"
"github.com/redis/go-redis/v9"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// OnboardingStateTTL bounds the time a user has to complete an OAuth round trip.
@@ -25,11 +25,11 @@ func (s *emailService) saveOnboardingState(ctx context.Context, state string, da
}
raw, err := json.Marshal(data)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if err := s.r.Set(ctx, onboardingStateKey(state), raw, OnboardingStateTTL).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
return nil
@@ -44,16 +44,16 @@ func (s *emailService) takeOnboardingState(ctx context.Context, state string) (*
if errors.Is(err, redis.Nil) {
return nil, errx.ErrEmailOnboardState
}
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
// Single-use: remove immediately to prevent replay even on later errors.
if err := s.r.Del(ctx, onboardingStateKey(state)).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
var out models.EmailOnboardingState
if err := json.Unmarshal(raw, &out); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
return &out, nil
+4 -4
View File
@@ -9,11 +9,11 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/infrastructure/pubsub"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/crypt"
"golang.org/x/oauth2"
)
@@ -34,7 +34,7 @@ func (s *emailService) OAuthStart(ctx context.Context, userID string, orgID *uui
state, err := crypt.Nonce()
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -228,7 +228,7 @@ func (s *emailService) OnboardSMTPIMAP(ctx context.Context, userID string, orgID
// the scheduler will pick the account up on its next pass.
if orgID != nil {
if _, err := s.workerAssignment.AssignWorkerToEmail(ctx, acc.ID, *orgID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
@@ -256,7 +256,7 @@ func (s *emailService) dispatchAccountConnected(ctx context.Context, orgID *uuid
"created_at": acc.CreatedAt,
}
if _, err := s.webhookService.Dispatch(ctx, *orgID, models.WebhookEventEmailAccountConnected, payload); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
+2 -2
View File
@@ -10,11 +10,11 @@ import (
"context"
"strings"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/crypt"
"golang.org/x/oauth2"
)
@@ -54,7 +54,7 @@ func (s *emailService) OAuthReauth(ctx context.Context, userID string, orgID *uu
state, err := crypt.Nonce()
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
+6 -6
View File
@@ -5,10 +5,10 @@ import (
"errors"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// ValidateCredentials seals a copy of the credentials with the org DEK and asks
@@ -24,21 +24,21 @@ func (s *emailService) ValidateCredentials(ctx context.Context, orgID uuid.UUID,
cipher, err := s.cipherService.Cipher(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
sealedIMAP := *credentials.IMAP
sealedIMAP.Password, err = cipher.Encrypt(ctx, credentials.IMAP.Password)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
sealedSMTP := *credentials.SMTP
sealedSMTP.Password, err = cipher.Encrypt(ctx, credentials.SMTP.Password)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
@@ -47,7 +47,7 @@ func (s *emailService) ValidateCredentials(ctx context.Context, orgID uuid.UUID,
ProcessID: processID,
Credentials: &models.SmtpImap{SMTP: &sealedSMTP, IMAP: &sealedIMAP},
}); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
@@ -63,7 +63,7 @@ func (s *emailService) ValidateCredentials(ctx context.Context, orgID uuid.UUID,
if errors.Is(err, context.DeadlineExceeded) {
return errx.ErrEmailValidation
}
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
+71 -71
View File
@@ -8,12 +8,12 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/app/dailythrottle"
"github.com/warmbly/warmbly/internal/config"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/crypt"
"github.com/warmbly/warmbly/internal/repository"
)
@@ -246,7 +246,7 @@ func (s *organizationService) Create(ctx context.Context, userID uuid.UUID, name
// Check organization limit
user, userErr := s.userRepo.GetUser(ctx, userID)
if userErr != nil {
sentry.CaptureException(userErr)
errs.CaptureException(userErr)
return nil, errx.New(errx.Internal, "failed to get user")
}
if user == nil {
@@ -255,7 +255,7 @@ func (s *organizationService) Create(ctx context.Context, userID uuid.UUID, name
ownedCount, countErr := s.orgRepo.GetUserOwnedOrganizationCount(ctx, userID)
if countErr != nil {
sentry.CaptureException(countErr)
errs.CaptureException(countErr)
return nil, errx.New(errx.Internal, "failed to get organization count")
}
if ownedCount >= user.MaxOrganizations {
@@ -275,7 +275,7 @@ func (s *organizationService) Create(ctx context.Context, userID uuid.UUID, name
org.Slug = &slug
if err := s.orgRepo.Create(ctx, org); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to create organization")
}
@@ -292,7 +292,7 @@ func (s *organizationService) Create(ctx context.Context, userID uuid.UUID, name
}
if err := s.orgRepo.AddMember(ctx, member); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
// Rollback org creation
_ = s.orgRepo.Delete(ctx, org.ID)
return nil, errx.New(errx.Internal, "failed to add owner member")
@@ -310,7 +310,7 @@ func (s *organizationService) Create(ctx context.Context, userID uuid.UUID, name
Color: seed.Color,
Permissions: seed.Permissions,
}); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
@@ -331,7 +331,7 @@ func (s *organizationService) Create(ctx context.Context, userID uuid.UUID, name
func (s *organizationService) Get(ctx context.Context, orgID uuid.UUID) (*models.Organization, *errx.Error) {
org, err := s.orgRepo.GetByID(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get organization")
}
if org == nil {
@@ -344,7 +344,7 @@ func (s *organizationService) Get(ctx context.Context, orgID uuid.UUID) (*models
func (s *organizationService) GetBySlug(ctx context.Context, slug string) (*models.Organization, *errx.Error) {
org, err := s.orgRepo.GetBySlug(ctx, slug)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get organization")
}
if org == nil {
@@ -357,7 +357,7 @@ func (s *organizationService) GetBySlug(ctx context.Context, slug string) (*mode
func (s *organizationService) Update(ctx context.Context, orgID uuid.UUID, req *models.UpdateOrganizationRequest) (*models.Organization, *errx.Error) {
org, err := s.orgRepo.GetByID(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get organization")
}
if org == nil {
@@ -400,7 +400,7 @@ func (s *organizationService) Update(ctx context.Context, orgID uuid.UUID, req *
org.UpdatedAt = time.Now()
if err := s.orgRepo.Update(ctx, org); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to update organization")
}
@@ -410,7 +410,7 @@ func (s *organizationService) Update(ctx context.Context, orgID uuid.UUID, req *
// Delete deletes an organization
func (s *organizationService) Delete(ctx context.Context, orgID uuid.UUID) *errx.Error {
if err := s.orgRepo.Delete(ctx, orgID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to delete organization")
}
return nil
@@ -420,7 +420,7 @@ func (s *organizationService) Delete(ctx context.Context, orgID uuid.UUID) *errx
func (s *organizationService) GetUserOrganizations(ctx context.Context, userID uuid.UUID) ([]models.OrganizationMember, *errx.Error) {
members, err := s.orgRepo.GetUserOrganizations(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get user organizations")
}
return members, nil
@@ -430,7 +430,7 @@ func (s *organizationService) GetUserOrganizations(ctx context.Context, userID u
func (s *organizationService) GetUserDefaultOrganization(ctx context.Context, userID uuid.UUID) (*models.Organization, *errx.Error) {
org, err := s.orgRepo.GetUserDefaultOrganization(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get default organization")
}
return org, nil
@@ -440,11 +440,11 @@ func (s *organizationService) GetUserDefaultOrganization(ctx context.Context, us
func (s *organizationService) GetMembers(ctx context.Context, orgID uuid.UUID) ([]models.OrganizationMember, *errx.Error) {
members, err := s.orgRepo.GetMembers(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get members")
}
if err := s.orgRepo.HydrateMemberRoles(ctx, orgID, members); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
return members, nil
}
@@ -453,7 +453,7 @@ func (s *organizationService) GetMembers(ctx context.Context, orgID uuid.UUID) (
func (s *organizationService) GetMembership(ctx context.Context, orgID, userID uuid.UUID) (*models.OrganizationMember, *errx.Error) {
member, err := s.orgRepo.GetMember(ctx, orgID, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get membership")
}
return member, nil
@@ -499,7 +499,7 @@ func (s *organizationService) InviteMember(ctx context.Context, orgID uuid.UUID,
// Generate invitation token
token, tokErr := generateInvitationToken()
if tokErr != nil {
sentry.CaptureException(tokErr)
errs.CaptureException(tokErr)
return nil, errx.New(errx.Internal, "failed to generate invitation token")
}
@@ -517,11 +517,11 @@ func (s *organizationService) InviteMember(ctx context.Context, orgID uuid.UUID,
}
if err := s.orgRepo.CreateInvitation(ctx, inv); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to create invitation")
}
if err := s.orgRepo.SetInvitationRoles(ctx, inv.ID, roleIDs); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to attach roles")
}
inv.Roles = toMemberRoles(roles)
@@ -541,7 +541,7 @@ func (s *organizationService) resolveRoleSet(ctx context.Context, orgID uuid.UUI
for _, id := range ids {
role, err := s.orgRepo.GetRoleByID(ctx, orgID, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, nil, 0, errx.New(errx.Internal, "failed to load role")
}
if role == nil {
@@ -565,7 +565,7 @@ func toMemberRoles(roles []models.OrganizationRole) []models.MemberRole {
func (s *organizationService) AcceptInvitation(ctx context.Context, token string, userID uuid.UUID, email string) (*models.OrganizationMember, *errx.Error) {
inv, err := s.orgRepo.GetInvitationByToken(ctx, token)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get invitation")
}
if inv == nil {
@@ -579,7 +579,7 @@ func (s *organizationService) AcceptInvitation(ctx context.Context, token string
func (s *organizationService) AcceptInvitationByID(ctx context.Context, invitationID, userID uuid.UUID, email string) (*models.OrganizationMember, *errx.Error) {
inv, err := s.orgRepo.GetInvitationByID(ctx, invitationID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get invitation")
}
if inv == nil {
@@ -592,7 +592,7 @@ func (s *organizationService) AcceptInvitationByID(ctx context.Context, invitati
func (s *organizationService) PreviewInvitation(ctx context.Context, token string) (*models.InvitationPreview, *errx.Error) {
inv, err := s.orgRepo.GetInvitationByToken(ctx, token)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get invitation")
}
if inv == nil {
@@ -629,7 +629,7 @@ func (s *organizationService) GetInvitationToken(ctx context.Context, orgID, inv
inv, err := s.orgRepo.GetInvitationByID(ctx, invitationID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.New(errx.Internal, "failed to get invitation")
}
if inv == nil || inv.OrganizationID != orgID {
@@ -665,7 +665,7 @@ func (s *organizationService) acceptResolved(ctx context.Context, inv *models.Or
// deleted in the meantime are dropped; at least one must survive.
invRoleIDs, ierr := s.orgRepo.GetInvitationRoles(ctx, inv.ID)
if ierr != nil {
sentry.CaptureException(ierr)
errs.CaptureException(ierr)
return nil, errx.New(errx.Internal, "failed to load invitation roles")
}
var liveRoleIDs []uuid.UUID
@@ -673,7 +673,7 @@ func (s *organizationService) acceptResolved(ctx context.Context, inv *models.Or
for _, id := range invRoleIDs {
role, rerr := s.orgRepo.GetRoleByID(ctx, inv.OrganizationID, id)
if rerr != nil {
sentry.CaptureException(rerr)
errs.CaptureException(rerr)
return nil, errx.New(errx.Internal, "failed to load role")
}
if role == nil {
@@ -704,7 +704,7 @@ func (s *organizationService) acceptResolved(ctx context.Context, inv *models.Or
AcceptedAt: &now,
}
if err := s.orgRepo.AddMemberWithRoles(ctx, member, liveRoleIDs); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to add member")
}
@@ -721,7 +721,7 @@ func (s *organizationService) acceptResolved(ctx context.Context, inv *models.Or
func (s *organizationService) UpdateMemberRole(ctx context.Context, orgID, actorID, memberUserID uuid.UUID, req *models.UpdateMemberRequest) (*models.OrganizationMember, *errx.Error) {
member, err := s.orgRepo.GetMember(ctx, orgID, memberUserID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get member")
}
if member == nil {
@@ -747,13 +747,13 @@ func (s *organizationService) UpdateMemberRole(ctx context.Context, orgID, actor
}
if err := s.orgRepo.SetMemberRoles(ctx, orgID, memberUserID, roleIDs); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to update roles")
}
updated, gerr := s.orgRepo.GetMember(ctx, orgID, memberUserID)
if gerr != nil {
sentry.CaptureException(gerr)
errs.CaptureException(gerr)
return nil, errx.New(errx.Internal, "failed to load member")
}
if updated != nil {
@@ -766,7 +766,7 @@ func (s *organizationService) UpdateMemberRole(ctx context.Context, orgID, actor
func (s *organizationService) RemoveMember(ctx context.Context, orgID, memberUserID uuid.UUID) *errx.Error {
member, err := s.orgRepo.GetMember(ctx, orgID, memberUserID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to get member")
}
if member == nil {
@@ -779,7 +779,7 @@ func (s *organizationService) RemoveMember(ctx context.Context, orgID, memberUse
}
if err := s.orgRepo.RemoveMember(ctx, orgID, memberUserID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to remove member")
}
@@ -790,11 +790,11 @@ func (s *organizationService) RemoveMember(ctx context.Context, orgID, memberUse
func (s *organizationService) GetPendingInvitations(ctx context.Context, orgID uuid.UUID) ([]models.OrganizationInvitation, *errx.Error) {
invitations, err := s.orgRepo.GetPendingInvitations(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get invitations")
}
if err := s.orgRepo.HydrateInvitationRoles(ctx, invitations); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
return invitations, nil
}
@@ -803,7 +803,7 @@ func (s *organizationService) GetPendingInvitations(ctx context.Context, orgID u
func (s *organizationService) GetUserPendingInvitations(ctx context.Context, email string) ([]models.OrganizationInvitation, *errx.Error) {
invitations, err := s.orgRepo.GetUserPendingInvitations(ctx, email)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get invitations")
}
return invitations, nil
@@ -812,7 +812,7 @@ func (s *organizationService) GetUserPendingInvitations(ctx context.Context, ema
// CancelInvitation cancels a pending invitation
func (s *organizationService) CancelInvitation(ctx context.Context, invitationID uuid.UUID) *errx.Error {
if err := s.orgRepo.DeleteInvitation(ctx, invitationID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to cancel invitation")
}
return nil
@@ -823,7 +823,7 @@ func (s *organizationService) TransferOwnership(ctx context.Context, orgID, newO
// Verify new owner is a member
member, err := s.orgRepo.GetMember(ctx, orgID, newOwnerUserID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to verify membership")
}
if member == nil {
@@ -831,7 +831,7 @@ func (s *organizationService) TransferOwnership(ctx context.Context, orgID, newO
}
if err := s.orgRepo.TransferOwnership(ctx, orgID, newOwnerUserID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to transfer ownership")
}
@@ -842,7 +842,7 @@ func (s *organizationService) TransferOwnership(ctx context.Context, orgID, newO
func (s *organizationService) HasPermission(ctx context.Context, orgID, userID uuid.UUID, perm models.OrganizationPermission) (bool, *errx.Error) {
member, err := s.orgRepo.GetMember(ctx, orgID, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return false, errx.New(errx.Internal, "failed to check permission")
}
if member == nil {
@@ -877,7 +877,7 @@ func (s *organizationService) CanAddMember(ctx context.Context, orgID uuid.UUID)
count, xerr := s.orgRepo.GetMemberCount(ctx, orgID)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return false, errx.New(errx.Internal, "failed to get member count")
}
@@ -923,7 +923,7 @@ func (s *organizationService) CanAddCampaign(ctx context.Context, orgID uuid.UUI
func (s *organizationService) MailboxAllowance(ctx context.Context, orgID uuid.UUID) (*models.MailboxAllowance, *errx.Error) {
count, err := s.orgRepo.GetEmailAccountCount(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get email account count")
}
a := &models.MailboxAllowance{Used: count, SendsPerMailbox: config.FairUseSendsPerMailbox}
@@ -936,7 +936,7 @@ func (s *organizationService) MailboxAllowance(ctx context.Context, orgID uuid.U
sub, serr := s.subRepo.GetByOrganizationID(ctx, orgID)
if serr != nil {
sentry.CaptureException(serr)
errs.CaptureException(serr)
return nil, errx.New(errx.Internal, "failed to get subscription")
}
a.Paid = sub != nil && sub.HasPaidSubscription()
@@ -983,7 +983,7 @@ func (s *organizationService) MailboxAllowance(ctx context.Context, orgID uuid.U
if a.Allowance != nil {
rows, rerr := s.orgRepo.ListLimitRequestsForOrg(ctx, orgID)
if rerr != nil {
sentry.CaptureException(rerr)
errs.CaptureException(rerr)
}
for i := range rows {
if rows[i].Field == "max_email_accounts" && rows[i].Status == models.LimitRequestStatusPending {
@@ -999,7 +999,7 @@ func (s *organizationService) MailboxAllowance(ctx context.Context, orgID uuid.U
func (s *organizationService) GetCampaignCounts(ctx context.Context, orgID uuid.UUID) (total int, active int, err *errx.Error) {
t, a, xerr := s.orgRepo.GetCampaignCounts(ctx, orgID)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return 0, 0, errx.New(errx.Internal, "failed to get campaign counts")
}
return t, a, nil
@@ -1009,7 +1009,7 @@ func (s *organizationService) GetCampaignCounts(ctx context.Context, orgID uuid.
func (s *organizationService) GetOrganizationLimits(ctx context.Context, orgID uuid.UUID) (*models.OrganizationLimits, *errx.Error) {
sub, err := s.subRepo.GetByOrganizationID(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get subscription")
}
if sub == nil || sub.Plan == nil {
@@ -1029,25 +1029,25 @@ func (s *organizationService) GetOrganizationLimits(ctx context.Context, orgID u
func (s *organizationService) GetOrganizationCounts(ctx context.Context, orgID uuid.UUID) (*models.OrganizationCounts, *errx.Error) {
total, active, err := s.orgRepo.GetCampaignCounts(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get campaign counts")
}
members, err := s.orgRepo.GetMemberCount(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get member count")
}
emails, err := s.orgRepo.GetEmailAccountCount(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get email account count")
}
contacts, err := s.orgRepo.GetContactCount(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to get contact count")
}
@@ -1070,7 +1070,7 @@ func (s *organizationService) CreateEnterpriseInquiry(ctx context.Context, inqui
inquiry.CreatedAt = time.Now()
if err := s.orgRepo.CreateEnterpriseInquiry(ctx, inquiry); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to create enterprise inquiry")
}
@@ -1125,7 +1125,7 @@ func (s *organizationService) GetUserAdminPermissions(ctx context.Context, userI
func (s *organizationService) SearchOrganizationsForAdmin(ctx context.Context, search *models.AdminOrgSearch) (*models.AdminOrgsResult, *errx.Error) {
result, err := s.orgRepo.SearchOrganizationsForAdmin(ctx, search)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to search organizations")
}
return result, nil
@@ -1138,7 +1138,7 @@ func (s *organizationService) SearchOrganizationsForAdmin(ctx context.Context, s
func (s *organizationService) GetOrganizationAdminDetail(ctx context.Context, orgID uuid.UUID) (*models.AdminOrgDetail, *errx.Error) {
detail, err := s.orgRepo.GetOrganizationAdminDetail(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load organization")
}
if detail == nil {
@@ -1166,7 +1166,7 @@ func (s *organizationService) GetOrganizationAdminDetail(ctx context.Context, or
func (s *organizationService) GetOrganizationMembersForAdmin(ctx context.Context, orgID uuid.UUID) ([]models.AdminOrgMember, *errx.Error) {
members, err := s.orgRepo.GetOrganizationMembersForAdmin(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load members")
}
return members, nil
@@ -1177,7 +1177,7 @@ func (s *organizationService) GetOrganizationMembersForAdmin(ctx context.Context
func (s *organizationService) GetLimitOverrides(ctx context.Context, orgID uuid.UUID) (*models.OrganizationLimitOverrides, *errx.Error) {
o, err := s.orgRepo.GetOrganizationLimitOverrides(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load limit overrides")
}
return o, nil
@@ -1190,7 +1190,7 @@ func (s *organizationService) GetLimitOverrides(ctx context.Context, orgID uuid.
func (s *organizationService) SetLimitOverrides(ctx context.Context, orgID uuid.UUID, req *models.UpdateOrgOverridesRequest, grantedBy uuid.UUID) (*models.OrganizationLimitOverrides, *errx.Error) {
o, err := s.orgRepo.UpsertOrganizationLimitOverrides(ctx, orgID, req, grantedBy)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to write limit overrides")
}
return o, nil
@@ -1374,7 +1374,7 @@ func (s *organizationService) SubmitLimitIncreaseRequest(ctx context.Context, or
if strings.Contains(err.Error(), "uq_limit_requests_one_pending_per_field") {
return nil, errx.New(errx.Conflict, "a pending request already exists for this field")
}
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to submit request")
}
@@ -1397,7 +1397,7 @@ func (s *organizationService) SubmitLimitIncreaseRequest(ctx context.Context, or
func (s *organizationService) ListLimitRequestsForOrg(ctx context.Context, orgID uuid.UUID) ([]models.LimitIncreaseRequest, *errx.Error) {
rows, err := s.orgRepo.ListLimitRequestsForOrg(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load requests")
}
return rows, nil
@@ -1409,7 +1409,7 @@ func (s *organizationService) ListLimitRequestsForOrg(ctx context.Context, orgID
func (s *organizationService) CancelLimitRequest(ctx context.Context, id, userID uuid.UUID) *errx.Error {
lr, err := s.orgRepo.GetLimitRequest(ctx, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to load request")
}
if lr == nil {
@@ -1422,7 +1422,7 @@ func (s *organizationService) CancelLimitRequest(ctx context.Context, id, userID
return errx.New(errx.BadRequest, "only pending requests can be cancelled")
}
if err := s.orgRepo.UpdateLimitRequestStatus(ctx, id, models.LimitRequestStatusCancelled, userID, ""); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to cancel request")
}
return nil
@@ -1448,7 +1448,7 @@ func (s *organizationService) AdminListLimitRequests(ctx context.Context, search
result, err := s.orgRepo.ListLimitRequestsForAdmin(ctx, search)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load limit requests")
}
return result, nil
@@ -1460,7 +1460,7 @@ func (s *organizationService) AdminListLimitRequests(ctx context.Context, search
func (s *organizationService) ApproveLimitRequest(ctx context.Context, id, reviewerID uuid.UUID, notes string) (*models.LimitIncreaseRequest, *errx.Error) {
lr, err := s.orgRepo.GetLimitRequest(ctx, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load request")
}
if lr == nil {
@@ -1486,7 +1486,7 @@ func (s *organizationService) ApproveLimitRequest(ctx context.Context, id, revie
}
if err := s.orgRepo.UpdateLimitRequestStatus(ctx, id, models.LimitRequestStatusApproved, reviewerID, notes); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to mark request approved")
}
lr.Status = models.LimitRequestStatusApproved
@@ -1498,7 +1498,7 @@ func (s *organizationService) ApproveLimitRequest(ctx context.Context, id, revie
func (s *organizationService) RejectLimitRequest(ctx context.Context, id, reviewerID uuid.UUID, notes string) (*models.LimitIncreaseRequest, *errx.Error) {
lr, err := s.orgRepo.GetLimitRequest(ctx, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load request")
}
if lr == nil {
@@ -1508,7 +1508,7 @@ func (s *organizationService) RejectLimitRequest(ctx context.Context, id, review
return nil, errx.New(errx.BadRequest, "only pending requests can be rejected")
}
if err := s.orgRepo.UpdateLimitRequestStatus(ctx, id, models.LimitRequestStatusRejected, reviewerID, notes); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to mark request rejected")
}
lr.Status = models.LimitRequestStatusRejected
@@ -1536,7 +1536,7 @@ func (s *organizationService) validateRolePermissions(ctx context.Context, orgID
func (s *organizationService) validateActorHoldsPermissions(ctx context.Context, orgID, actorID uuid.UUID, perms models.OrganizationPermission) *errx.Error {
actor, err := s.orgRepo.GetMember(ctx, orgID, actorID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to load member")
}
if actor == nil {
@@ -1562,7 +1562,7 @@ func validateRoleName(name string) (string, *errx.Error) {
func (s *organizationService) ListRoles(ctx context.Context, orgID uuid.UUID) ([]models.OrganizationRole, *errx.Error) {
roles, err := s.orgRepo.ListRoles(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list roles")
}
if roles == nil {
@@ -1583,7 +1583,7 @@ func (s *organizationService) CreateRole(ctx context.Context, orgID, actorID uui
count, err := s.orgRepo.CountRoles(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to count roles")
}
if count >= MaxCustomRolesPerOrg {
@@ -1613,7 +1613,7 @@ func (s *organizationService) CreateRole(ctx context.Context, orgID, actorID uui
func (s *organizationService) UpdateRole(ctx context.Context, orgID, actorID, roleID uuid.UUID, req *models.UpdateOrganizationRoleRequest) (*models.OrganizationRole, *errx.Error) {
role, err := s.orgRepo.GetRoleByID(ctx, orgID, roleID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load role")
}
if role == nil {
@@ -1648,7 +1648,7 @@ func (s *organizationService) UpdateRole(ctx context.Context, orgID, actorID, ro
// Write-through: assigned members pick up the new name + permissions
// atomically (their effective access changes live via the audit spine).
if err := s.orgRepo.UpdateRole(ctx, role); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to update role")
}
return role, nil
@@ -1661,7 +1661,7 @@ func (s *organizationService) DeleteRole(ctx context.Context, orgID, actorID, ro
// the Admin role).
role, rerr := s.orgRepo.GetRoleByID(ctx, orgID, roleID)
if rerr != nil {
sentry.CaptureException(rerr)
errs.CaptureException(rerr)
return errx.New(errx.Internal, "failed to load role")
}
if role == nil {
@@ -1671,7 +1671,7 @@ func (s *organizationService) DeleteRole(ctx context.Context, orgID, actorID, ro
return xerr
}
if err := s.orgRepo.DeleteRole(ctx, orgID, roleID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to delete role")
}
return nil
+21 -21
View File
@@ -14,8 +14,8 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
@@ -63,7 +63,7 @@ func (s *service) RequestExport(
active, err := s.repo.HasActiveExport(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
if active {
@@ -77,7 +77,7 @@ func (s *service) RequestExport(
IncludeSecrets: req.IncludeSecrets,
}
if err := s.repo.CreateExportJob(ctx, job); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -87,7 +87,7 @@ func (s *service) RequestExport(
runCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), staleTransferDeadline)
defer cancel()
if err := s.runExport(runCtx, job, passphrase); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
_ = s.repo.FailExportJob(context.WithoutCancel(ctx), job.ID, err.Error())
}
}()
@@ -145,7 +145,7 @@ func (s *service) runExport(ctx context.Context, job *models.OrgExportJob, passp
func (s *service) GetExport(ctx context.Context, orgID, id uuid.UUID) (*models.OrgExportJob, *errx.Error) {
job, err := s.repo.GetExportJob(ctx, orgID, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
if job == nil {
@@ -157,7 +157,7 @@ func (s *service) GetExport(ctx context.Context, orgID, id uuid.UUID) (*models.O
func (s *service) ListExports(ctx context.Context, orgID uuid.UUID) ([]models.OrgExportJob, *errx.Error) {
jobs, err := s.repo.ListExportJobs(ctx, orgID, jobHistoryLimit)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
return jobs, nil
@@ -181,7 +181,7 @@ func (s *service) OpenExport(ctx context.Context, orgID, id uuid.UUID) (io.ReadC
body, err := s.blobs.Get(ctx, *job.ArchiveKey)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, nil, errx.New(errx.NotFound, "This archive could not be read from storage.")
}
return body, job, nil
@@ -190,12 +190,12 @@ func (s *service) OpenExport(ctx context.Context, orgID, id uuid.UUID) (io.ReadC
func (s *service) DeleteExport(ctx context.Context, orgID, id uuid.UUID) *errx.Error {
key, err := s.repo.DeleteExportJob(ctx, orgID, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if key != "" && s.blobs != nil {
if err := s.blobs.Delete(ctx, key); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
return nil
@@ -254,7 +254,7 @@ func (s *service) Preflight(
}
known, rerr := s.repo.ResolveUsersByEmail(ctx, emails)
if rerr != nil {
sentry.CaptureException(rerr)
errs.CaptureException(rerr)
return nil, errx.InternalError()
}
for _, m := range manifest.Members {
@@ -271,7 +271,7 @@ func (s *service) Preflight(
}
destCols, err := s.repo.TableColumns(ctx, mt.Name)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
if len(destCols) == 0 {
@@ -346,7 +346,7 @@ func (s *service) RequestImport(
active, err := s.repo.HasActiveImport(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
if active {
@@ -386,14 +386,14 @@ func (s *service) RequestImport(
if s.blobs != nil {
key := uploadObjectKey(orgID, uuid.New())
if err := s.blobs.Put(ctx, key, io.NewSectionReader(archive, 0, size), "application/zip"); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "The archive could not be stored for import.")
}
job.ArchiveKey = &key
}
if err := s.repo.CreateImportJob(ctx, job); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -425,7 +425,7 @@ func (s *service) RequestImport(
_ = s.repo.UpdateImportProgress(runCtx, job.ID, percent, stage)
})
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
_ = s.repo.FailImportJob(runCtx, job.ID, err.Error())
return
}
@@ -435,7 +435,7 @@ func (s *service) RequestImport(
// not linger past the point it could still be useful.
if job.ArchiveKey != nil && s.blobs != nil {
if derr := s.blobs.Delete(runCtx, *job.ArchiveKey); derr != nil {
sentry.CaptureException(derr)
errs.CaptureException(derr)
}
}
}()
@@ -446,7 +446,7 @@ func (s *service) RequestImport(
func (s *service) GetImport(ctx context.Context, orgID, id uuid.UUID) (*models.OrgImportJob, *errx.Error) {
job, err := s.repo.GetImportJob(ctx, orgID, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
if job == nil {
@@ -458,7 +458,7 @@ func (s *service) GetImport(ctx context.Context, orgID, id uuid.UUID) (*models.O
func (s *service) ListImports(ctx context.Context, orgID uuid.UUID) ([]models.OrgImportJob, *errx.Error) {
jobs, err := s.repo.ListImportJobs(ctx, orgID, jobHistoryLimit)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
return jobs, nil
@@ -478,18 +478,18 @@ func (s *service) PurgeExpiredExports(ctx context.Context) (int, error) {
job := &expired[i]
if job.ArchiveKey != nil && s.blobs != nil {
if derr := s.blobs.Delete(ctx, *job.ArchiveKey); derr != nil {
sentry.CaptureException(derr)
errs.CaptureException(derr)
}
}
if err := s.repo.MarkExportExpired(ctx, job.ID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
continue
}
purged++
}
if err := s.repo.FailStaleTransfers(ctx, time.Now().Add(-staleTransferDeadline)); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
return purged, nil
}
+5 -5
View File
@@ -5,11 +5,11 @@ import (
"encoding/json"
"errors"
"github.com/getsentry/sentry-go"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// Challenge/session data lives in Redis between a Begin and Finish call,
@@ -28,12 +28,12 @@ func loginKey(sessionID uuid.UUID) string {
func (s *service) saveSession(ctx context.Context, key string, data *webauthn.SessionData) *errx.Error {
raw, err := json.Marshal(data)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if err := s.cache.Set(ctx, key, raw, CeremonyTTL).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
@@ -49,13 +49,13 @@ func (s *service) takeSession(ctx context.Context, key string) (*webauthn.Sessio
if errors.Is(err, redis.Nil) {
return nil, errx.ErrPasskeySession
}
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
var data webauthn.SessionData
if err := json.Unmarshal(raw, &data); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
+2 -2
View File
@@ -5,11 +5,11 @@ import (
"strings"
"unicode/utf8"
"github.com/getsentry/sentry-go"
"github.com/go-webauthn/webauthn/protocol"
"github.com/go-webauthn/webauthn/webauthn"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func (s *service) BeginRegistration(ctx context.Context, userID uuid.UUID) (*protocol.CredentialCreation, *errx.Error) {
@@ -27,7 +27,7 @@ func (s *service) BeginRegistration(ctx context.Context, userID uuid.UUID) (*pro
webauthn.WithExclusions(wuser.excludeList()),
)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.ErrPasskey
}
+17 -17
View File
@@ -19,10 +19,10 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/repository"
)
@@ -144,7 +144,7 @@ func genCode() string {
func (s *service) EnsureCode(ctx context.Context, ownerUserID, ownerOrgID uuid.UUID) (*models.ReferralCode, *errx.Error) {
existing, err := s.repo.GetCodeByOwner(ctx, ownerUserID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load referral code")
}
if existing != nil {
@@ -159,7 +159,7 @@ func (s *service) EnsureCode(ctx context.Context, ownerUserID, ownerOrgID uuid.U
taken, err := s.repo.GetCodeByCode(ctx, code)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to mint referral code")
}
if taken != nil {
@@ -167,7 +167,7 @@ func (s *service) EnsureCode(ctx context.Context, ownerUserID, ownerOrgID uuid.U
}
clash, err := s.discountRepo.GetByCode(ctx, code)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to mint referral code")
}
if clash != nil {
@@ -203,7 +203,7 @@ func (s *service) EnsureCode(ctx context.Context, ownerUserID, ownerOrgID uuid.U
if again, gerr := s.repo.GetCodeByOwner(ctx, ownerUserID); gerr == nil && again != nil {
return again, nil
}
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to mint referral code")
}
return rc, nil
@@ -223,12 +223,12 @@ func (s *service) Summary(ctx context.Context, ownerUserID, ownerOrgID uuid.UUID
ledger, err := s.repo.GetLedger(ctx, ownerOrgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load referral earnings")
}
total, pending, qualified, rewarded, err := s.repo.AttributionStats(ctx, ownerOrgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to load referral stats")
}
@@ -254,7 +254,7 @@ func (s *service) Summary(ctx context.Context, ownerUserID, ownerOrgID uuid.UUID
func (s *service) ListAttributions(ctx context.Context, referrerOrgID uuid.UUID, limit, offset int) ([]models.ReferralAttribution, *errx.Error) {
rows, err := s.repo.ListAttributionsByReferrer(ctx, referrerOrgID, limit, offset)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list referrals")
}
return rows, nil
@@ -263,7 +263,7 @@ func (s *service) ListAttributions(ctx context.Context, referrerOrgID uuid.UUID,
func (s *service) ListEarnings(ctx context.Context, orgID uuid.UUID, limit, offset int) ([]models.ReferralEarningsTransaction, *errx.Error) {
rows, err := s.repo.ListEarnings(ctx, orgID, limit, offset)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.New(errx.Internal, "failed to list referral earnings")
}
return rows, nil
@@ -278,7 +278,7 @@ func (s *service) AttributeSignup(ctx context.Context, code string, inviteeOrgID
}
rc, err := s.repo.GetCodeByCode(ctx, code)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil // never block signup on a lookup failure
}
if rc == nil {
@@ -331,14 +331,14 @@ func (s *service) QualifyOnConversion(ctx context.Context, inviteeOrgID uuid.UUI
return
}
if err := s.repo.MarkAttributionQualified(ctx, attr.ID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
func (s *service) RewardOnFirstInvoice(ctx context.Context, inviteeOrgID, planID uuid.UUID, eventID string) *errx.Error {
attr, err := s.repo.GetAttributionByInviteeOrg(ctx, inviteeOrgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to load referral attribution")
}
if attr == nil {
@@ -358,7 +358,7 @@ func (s *service) RewardOnFirstInvoice(ctx context.Context, inviteeOrgID, planID
// surfaced (not silently treated as under-cap) but doesn't block the reward.
since := time.Now().Add(-clawbackWindow)
if n, cerr := s.repo.CountRewardedByReferrerSince(ctx, attr.ReferrerOrgID, since); cerr != nil {
sentry.CaptureException(cerr)
errs.CaptureException(cerr)
} else if n >= MonthlyRewardCap {
_ = s.repo.MarkAttributionVoid(ctx, attr.ID, "monthly_cap")
return nil
@@ -379,7 +379,7 @@ func (s *service) RewardOnFirstInvoice(ctx context.Context, inviteeOrgID, planID
// replayed event, so nothing moved.
applied, err := s.repo.ApplyReferralReward(ctx, attr, reward, DefaultCurrency, eventID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.New(errx.Internal, "failed to record referral reward")
}
if !applied {
@@ -412,7 +412,7 @@ func (s *service) ClawbackForInvitee(ctx context.Context, inviteeOrgID uuid.UUID
// reverse the same reward.
applied, err := s.repo.ApplyReferralClawback(ctx, attr, attr.RewardCents, attr.RewardCurrency, eventID+":clawback", reason)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return
}
if !applied {
@@ -449,11 +449,11 @@ func (s *service) SyncStripeBalance(ctx context.Context, orgID uuid.UUID) {
// so a balance that returns to a prior value still yields a distinct key.
key := fmt.Sprintf("refbal:%s:%d:%d", orgID, ledger.LifetimeEarnedCents, ledger.BalanceCents)
if _, xerr := s.balancer.ApplyCustomerCredit(ctx, sub.StripeCustomerID, delta, ledger.Currency, key); xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return
}
if err := s.repo.SetStripePushed(ctx, orgID, ledger.BalanceCents); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
+3 -3
View File
@@ -4,8 +4,8 @@ import (
"context"
"fmt"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
@@ -46,7 +46,7 @@ func (s *sequenceService) Delete(ctx context.Context, userID, campaignID, sequen
for _, key := range keys {
if err := s.storage.Delete(ctx, key); err != nil {
sentry.CaptureException(fmt.Errorf("sequence %s delete: object %s: %w", sequenceID, key, err))
errs.CaptureException(fmt.Errorf("sequence %s delete: object %s: %w", sequenceID, key, err))
}
}
return nil
@@ -69,7 +69,7 @@ func (s *sequenceService) stepObjectKeys(ctx context.Context, campaignID, sequen
}
atts, err := s.attachmentRepo.ListForStep(ctx, cID, sID)
if err != nil {
sentry.CaptureException(fmt.Errorf("sequence %s delete: list attachments: %w", sequenceID, err))
errs.CaptureException(fmt.Errorf("sequence %s delete: list attachments: %w", sequenceID, err))
return nil
}
keys := make([]string, 0, len(atts))
+2 -2
View File
@@ -4,9 +4,9 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func getTokenKey(id uuid.UUID) string {
@@ -15,7 +15,7 @@ func getTokenKey(id uuid.UUID) string {
func (s *socketService) saveToken(ctx context.Context, id uuid.UUID, nonce string, expiresAt time.Time) *errx.Error {
if err := s.cache.SetEx(ctx, getTokenKey(id), nonce, time.Until(expiresAt)).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
return nil
+3 -3
View File
@@ -4,9 +4,9 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/crypt"
)
@@ -16,13 +16,13 @@ func (s *socketService) GenerateWebsocketToken(ctx context.Context, userID uuid.
id := uuid.New()
nonce, err := crypt.Nonce()
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.InternalError()
}
wsToken, err := s.tokenService.GenerateToken(userID, id, "", nonce, issuedAt, expiresAt)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.InternalError()
}
+12 -12
View File
@@ -9,7 +9,7 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/google/uuid"
"github.com/stripe/stripe-go/v76"
@@ -170,7 +170,7 @@ func (s *stripeService) CreateCustomer(ctx context.Context, userID uuid.UUID, em
cust, err := customer.New(params)
if err != nil {
sentry.CaptureException(fmt.Errorf("stripe customer creation failed: %w", err))
errs.CaptureException(fmt.Errorf("stripe customer creation failed: %w", err))
return "", errx.New(errx.Internal, "failed to create billing account")
}
@@ -204,7 +204,7 @@ func (s *stripeService) ApplyCustomerCredit(ctx context.Context, customerID stri
}
txn, err := balancetxn.New(params)
if err != nil {
sentry.CaptureException(fmt.Errorf("stripe customer balance txn failed: %w", err))
errs.CaptureException(fmt.Errorf("stripe customer balance txn failed: %w", err))
return "", errx.New(errx.Internal, "failed to apply referral credit")
}
return txn.ID, nil
@@ -310,7 +310,7 @@ func (s *stripeService) CreateCheckoutSession(ctx context.Context, userID uuid.U
if reservedID != nil {
_ = s.discountService.CancelRedemptionByID(ctx, *reservedID)
}
sentry.CaptureException(fmt.Errorf("stripe checkout session failed: %w", err))
errs.CaptureException(fmt.Errorf("stripe checkout session failed: %w", err))
return nil, errx.New(errx.Internal, "failed to create checkout session")
}
@@ -319,7 +319,7 @@ func (s *stripeService) CreateCheckoutSession(ctx context.Context, userID uuid.U
// checkout.session.expired.
if reservedID != nil {
if xerr := s.discountService.AttachRedemptionStripe(ctx, *reservedID, &sess.ID, couponID); xerr != nil {
sentry.CaptureException(fmt.Errorf("attach discount redemption refs failed: %s", xerr.Message))
errs.CaptureException(fmt.Errorf("attach discount redemption refs failed: %s", xerr.Message))
}
}
@@ -359,7 +359,7 @@ func (s *stripeService) mintCoupon(dc *models.DiscountCode) (string, *errx.Error
c, err := coupon.New(params)
if err != nil {
sentry.CaptureException(fmt.Errorf("stripe coupon creation failed: %w", err))
errs.CaptureException(fmt.Errorf("stripe coupon creation failed: %w", err))
return "", errx.New(errx.Internal, "failed to apply discount")
}
return c.ID, nil
@@ -410,7 +410,7 @@ func (s *stripeService) CreateCreditCheckoutSession(ctx context.Context, userID,
sess, serr := session.New(params)
if serr != nil {
sentry.CaptureException(fmt.Errorf("stripe credit checkout session failed: %w", serr))
errs.CaptureException(fmt.Errorf("stripe credit checkout session failed: %w", serr))
return nil, errx.New(errx.Internal, "failed to create checkout session")
}
return sess, nil
@@ -499,7 +499,7 @@ func (s *stripeService) CreatePortalSession(ctx context.Context, customerID, ret
sess, err := portalsession.New(params)
if err != nil {
sentry.CaptureException(fmt.Errorf("stripe portal session failed: %w", err))
errs.CaptureException(fmt.Errorf("stripe portal session failed: %w", err))
return "", errx.New(errx.Internal, "failed to create billing portal session")
}
@@ -521,7 +521,7 @@ func (s *stripeService) CancelSubscription(ctx context.Context, subscriptionID s
_, err := subscription.Update(subscriptionID, params)
if err != nil {
sentry.CaptureException(fmt.Errorf("stripe subscription cancel failed: %w", err))
errs.CaptureException(fmt.Errorf("stripe subscription cancel failed: %w", err))
return errx.New(errx.Internal, "failed to update subscription")
}
@@ -630,7 +630,7 @@ func (s *stripeService) ChangePlan(ctx context.Context, orgID uuid.UUID, newPlan
// a direct plan change). Best-effort: the discount is already live.
if reservedID != nil {
if xerr := s.discountService.AttachRedemptionStripe(ctx, *reservedID, nil, couponID); xerr != nil {
sentry.CaptureException(fmt.Errorf("attach discount redemption refs failed: %s", xerr.Message))
errs.CaptureException(fmt.Errorf("attach discount redemption refs failed: %s", xerr.Message))
}
}
@@ -867,7 +867,7 @@ func (s *stripeService) handleCheckoutCompleted(ctx context.Context, event *stri
subID = &sub.ID
}
if xerr := s.discountService.MarkRedemptionApplied(ctx, checkoutSession.ID, subID); xerr != nil {
sentry.CaptureException(fmt.Errorf("mark discount redemption applied failed: %s", xerr.Message))
errs.CaptureException(fmt.Errorf("mark discount redemption applied failed: %s", xerr.Message))
}
}
@@ -1130,7 +1130,7 @@ func (s *stripeService) handleInvoicePaid(ctx context.Context, event *stripe.Eve
(inv.BillingReason == stripe.InvoiceBillingReasonSubscriptionCreate ||
inv.BillingReason == stripe.InvoiceBillingReasonSubscriptionCycle) {
if err := s.credits.ResetMonthlyAllowance(ctx, sub.OrganizationID, plan.MonthlyCredits, event.ID); err != nil {
sentry.CaptureException(fmt.Errorf("monthly credit reset failed for org %s: %w", sub.OrganizationID, err))
errs.CaptureException(fmt.Errorf("monthly credit reset failed for org %s: %w", sub.OrganizationID, err))
} else if s.audit != nil {
s.audit.LogAction(ctx, sub.OrganizationID, sub.UserID, models.AuditActionUpdate, models.AuditEntityCreditGrant, nil, "", "", nil, map[string]string{
"reason": "monthly_reset",
+5 -5
View File
@@ -6,11 +6,11 @@ import (
"errors"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func getSessionKey(id uuid.UUID) string {
@@ -20,7 +20,7 @@ func getSessionKey(id uuid.UUID) string {
func (s *tokenService) saveSession(ctx context.Context, session *models.Session, ttl time.Duration) *errx.Error {
data, err := json.Marshal(session)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
@@ -37,13 +37,13 @@ func (s *tokenService) getSession(ctx context.Context, sessionID uuid.UUID) (*mo
if errors.Is(err, redis.Nil) {
return nil, nil
}
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
var session models.Session
if err := json.Unmarshal(data, &session); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -52,7 +52,7 @@ func (s *tokenService) getSession(ctx context.Context, sessionID uuid.UUID) (*mo
func (s *tokenService) deleteSession(ctx context.Context, sessionID uuid.UUID) *errx.Error {
if err := s.cache.Del(ctx, getSessionKey(sessionID)).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
+7 -7
View File
@@ -5,13 +5,13 @@ import (
"net/netip"
"time"
"github.com/getsentry/sentry-go"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/mileusna/useragent"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/infrastructure/db"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/crypt"
)
@@ -80,13 +80,13 @@ func (s *tokenService) GenerateSessionWithOrg(ctx context.Context, userID uuid.U
ip, err := netip.ParseAddr(ipaddr)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
ipinfo, err := s.geo.Lookup(ip)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -140,14 +140,14 @@ func (s *tokenService) GenerateSessionWithOrg(ctx context.Context, userID uuid.U
accessTokenExpiresAt := issuedAt.Add(AccessTokenLifeTime)
accessNonce, err := crypt.Nonce()
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
session.AccessNonce = accessNonce
accessToken, err := s.GenerateToken(userID, session.ID, email, accessNonce, issuedAt, accessTokenExpiresAt)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -155,14 +155,14 @@ func (s *tokenService) GenerateSessionWithOrg(ctx context.Context, userID uuid.U
session.ExpiresAt = &refreshTokenExpiresAt
refreshNonce, err := crypt.Nonce()
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
session.RefreshNonce = refreshNonce
refreshToken, err := s.GenerateToken(userID, session.ID, email, refreshNonce, issuedAt, refreshTokenExpiresAt)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
+7 -7
View File
@@ -4,9 +4,9 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/crypt"
)
@@ -39,31 +39,31 @@ func (s *tokenService) RefreshToken(ctx context.Context, refreshToken string) (*
accessTokenExpiresAt := issuedAt.Add(AccessTokenLifeTime)
accessNonce, xerr := crypt.Nonce()
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
newAccessToken, xerr := s.GenerateToken(sess.UserID, sess.ID, "", accessNonce, issuedAt, accessTokenExpiresAt)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
refreshTokenExpiresAt := issuedAt.Add(RefreshTokenLifeTime)
refreshNonce, xerr := crypt.Nonce()
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
newRefreshToken, xerr := s.GenerateToken(sess.UserID, sess.ID, "", refreshNonce, issuedAt, refreshTokenExpiresAt)
if xerr != nil {
sentry.CaptureException(xerr)
errs.CaptureException(xerr)
return nil, errx.InternalError()
}
if err := s.tokenRepository.RefreshToken(ctx, sess.ID, t.Nonce, accessNonce, refreshNonce, issuedAt); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -75,7 +75,7 @@ func (s *tokenService) RefreshToken(ctx context.Context, refreshToken string) (*
// Dropping the cache forces the next GetSession to re-read from the
// updated Postgres row.
if err := s.deleteSession(ctx, sess.ID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
// Don't fail the refresh — the worst case if the delete somehow
// failed is the user retries; we already returned the new tokens.
}
+3 -3
View File
@@ -4,10 +4,10 @@ import (
"context"
"strings"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/mailhtml"
)
@@ -27,7 +27,7 @@ func (s *uniboxService) GetByID(
{
msg, owner, err := s.uniboxRepository.GetByIDForOrg(ctx, orgID, id)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
ownerID = owner
@@ -67,7 +67,7 @@ func (s *uniboxService) GetByID(
// still has its preview text. Returning 500 made the whole message
// unopenable instead of showing what we have.
if !fixtureMessage {
sentry.CaptureException(err)
errs.CaptureException(err)
}
resp.BodyPlain = snippet
resp.BodyTruncated = !fixtureMessage
+3 -3
View File
@@ -3,10 +3,10 @@ package unibox
import (
"context"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/generation"
)
@@ -20,7 +20,7 @@ const GroundingLimitMax = 20
func (s *uniboxService) ThreadGrounding(ctx context.Context, orgID uuid.UUID, threadID string, limit int) ([]models.MessageGrounding, *errx.Error) {
out, err := s.uniboxRepository.GroundingByThread(ctx, orgID, threadID, clampGroundingLimit(limit))
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
return out, nil
@@ -31,7 +31,7 @@ func (s *uniboxService) ThreadGrounding(ctx context.Context, orgID uuid.UUID, th
func (s *uniboxService) AddressGrounding(ctx context.Context, orgID uuid.UUID, address string, limit int) ([]models.MessageGrounding, *errx.Error) {
out, err := s.uniboxRepository.GroundingByAddress(ctx, orgID, address, clampGroundingLimit(limit))
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
return out, nil
+2 -2
View File
@@ -4,10 +4,10 @@ import (
"context"
"strconv"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func (s *uniboxService) Incoming(
@@ -36,7 +36,7 @@ func (s *uniboxService) Incoming(
}
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
+3 -3
View File
@@ -3,10 +3,10 @@ package unibox
import (
"context"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// SetThreadLabels replaces the conversation's label set with the given
@@ -18,7 +18,7 @@ func (s *uniboxService) SetThreadLabels(ctx context.Context, userID uuid.UUID, t
}
labels, err := s.uniboxRepository.SetThreadLabels(ctx, userID, threadID, categoryIDs)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
return labels, nil
@@ -31,7 +31,7 @@ func (s *uniboxService) ListThreadLabels(ctx context.Context, userID uuid.UUID,
}
labels, err := s.uniboxRepository.ListThreadLabels(ctx, userID, threadID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
return labels, nil
+3 -3
View File
@@ -3,11 +3,11 @@ package unibox
import (
"context"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/config"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// Overview rolls up the counts the dashboard's scope rail and top
@@ -16,7 +16,7 @@ import (
func (s *uniboxService) Overview(ctx context.Context, orgID, userID uuid.UUID) (*models.UniboxOverview, *errx.Error) {
o, err := s.uniboxRepository.Overview(ctx, orgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
if len(o.Mailboxes) > OverviewMaxMailboxes {
@@ -34,7 +34,7 @@ func (s *uniboxService) Overview(ctx context.Context, orgID, userID uuid.UUID) (
if n, err := s.taskRepo.CountScheduledForUser(ctx, userID); err == nil {
o.ScheduledPending = n
} else {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
// Static-for-now cap. Surfacing it in the overview lets the
+5 -5
View File
@@ -4,9 +4,9 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"github.com/warmbly/warmbly/internal/observability/errs"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
@@ -35,7 +35,7 @@ const snippetMaxLen = 240
func (s *uniboxService) ListScheduled(ctx context.Context, userID uuid.UUID) ([]models.UniboxScheduledItem, *errx.Error) {
rows, err := s.taskRepo.ListScheduledForUser(ctx, userID, ScheduledListMax)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -68,7 +68,7 @@ func (s *uniboxService) ListScheduledByThread(ctx context.Context, userID uuid.U
}
rows, err := s.taskRepo.ListScheduledForUserByThread(ctx, userID, threadID, ScheduledThreadListMax)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -113,7 +113,7 @@ func (s *uniboxService) ListScheduledByThread(ctx context.Context, userID uuid.U
func (s *uniboxService) CancelScheduled(ctx context.Context, userID, taskID uuid.UUID) *errx.Error {
cloudTaskName, cancelled, err := s.taskRepo.CancelScheduledByUser(ctx, taskID, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
if !cancelled {
@@ -137,7 +137,7 @@ func (s *uniboxService) CancelScheduled(ctx context.Context, userID, taskID uuid
// but doesn't change the response — the DB is the source
// of truth and the handler safety-net catches stragglers.
if st, ok := status.FromError(derr); !ok || st.Code() != codes.NotFound {
sentry.CaptureException(derr)
errs.CaptureException(derr)
log.Warn().
Err(derr).
Str("task_id", taskID.String()).
+3 -3
View File
@@ -3,10 +3,10 @@ package unibox
import (
"context"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// Search searches emails with filters
@@ -31,7 +31,7 @@ func (s *uniboxService) Search(
// sender filter is handled inside Search via params.Sender.
resp, err := s.uniboxRepository.Search(ctx, orgID, userID, params)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -46,7 +46,7 @@ func (s *uniboxService) GetUnseenCount(
) (int64, *errx.Error) {
count, err := s.uniboxRepository.GetUnseenCount(ctx, orgID, emailAccountID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return 0, errx.InternalError()
}
+4 -4
View File
@@ -3,15 +3,15 @@ package unibox
import (
"context"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func (s *uniboxService) MarkSeen(ctx context.Context, userID, emailID uuid.UUID, seen bool) *errx.Error {
if err := s.uniboxRepository.MarkSeen(ctx, userID, emailID, seen); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
@@ -33,14 +33,14 @@ func (s *uniboxService) MarkSeenBulk(ctx context.Context, orgID uuid.UUID, data
return nil, errx.ErrUniboxFolder
}
if err := s.uniboxRepository.MarkSeenByFolder(ctx, orgID, data.Folder, data.Seen); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
return data, nil
}
if err := s.uniboxRepository.MarkSeenBulk(ctx, orgID, data.EmailIDs, data.Seen); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
+4 -4
View File
@@ -4,10 +4,10 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func (s *uniboxService) Snooze(ctx context.Context, userID uuid.UUID, threadID string, until time.Time) (*models.UniboxSnooze, *errx.Error) {
@@ -27,7 +27,7 @@ func (s *uniboxService) Snooze(ctx context.Context, userID uuid.UUID, threadID s
row, err := s.uniboxRepository.UpsertSnooze(ctx, userID, threadID, until.UTC())
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
return row, nil
@@ -38,7 +38,7 @@ func (s *uniboxService) Unsnooze(ctx context.Context, userID uuid.UUID, threadID
return errx.New(errx.BadRequest, "thread_id is required")
}
if err := s.uniboxRepository.DeleteSnooze(ctx, userID, threadID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
return nil
@@ -47,7 +47,7 @@ func (s *uniboxService) Unsnooze(ctx context.Context, userID uuid.UUID, threadID
func (s *uniboxService) ListSnoozes(ctx context.Context, userID uuid.UUID) ([]models.UniboxSnooze, *errx.Error) {
rows, err := s.uniboxRepository.ListSnoozes(ctx, userID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
return rows, nil
+3 -3
View File
@@ -4,10 +4,10 @@ import (
"context"
"strconv"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// GetByThread returns every message in a thread. limit/cursor are
@@ -32,7 +32,7 @@ func (s *uniboxService) GetByThread(
resp, err := s.uniboxRepository.GetByThread(ctx, orgID, emailID, threadID, l, cursor)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
@@ -48,7 +48,7 @@ func (s *uniboxService) LatestMessageIDInThread(
) (string, *errx.Error) {
messageID, err := s.uniboxRepository.LatestMessageIDInThread(ctx, orgID, threadID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", errx.InternalError()
}
return messageID, nil
+5 -5
View File
@@ -5,11 +5,11 @@ import (
"encoding/json"
"errors"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func getUserKey(id uuid.UUID) string {
@@ -19,13 +19,13 @@ func getUserKey(id uuid.UUID) string {
func (s *userService) SaveUser(ctx context.Context, user *models.User) *errx.Error {
raw, err := json.Marshal(user)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
key := getUserKey(user.ID)
if err := s.cache.SetEx(ctx, key, raw, UserTTL).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return errx.InternalError()
}
@@ -38,13 +38,13 @@ func (s *userService) getUser(ctx context.Context, userID uuid.UUID) (*models.Us
if errors.Is(err, redis.Nil) {
return nil, nil
}
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
var user models.User
if err := json.Unmarshal(data, &user); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, errx.InternalError()
}
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/pkg/generation"
@@ -159,7 +159,7 @@ func (s *service) PollBatches(ctx context.Context) error {
for i := range jobs {
job := &jobs[i]
if err := s.pollBatchJob(ctx, job); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
log.Warn().Err(err).Str("job_id", job.ID.String()).Str("batch_id", job.BatchID).
Msg("warmup batch generation: poll failed")
}
@@ -307,7 +307,7 @@ func (s *service) ingestBatch(ctx context.Context, job *models.WarmupGenerationJ
}
if err := s.repo.InsertConversation(ctx, record); err != nil {
job.FailedCount++
sentry.CaptureException(err)
errs.CaptureException(err)
continue
}
job.GeneratedCount++
@@ -4,9 +4,9 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/email"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func (w *WorkerService) HandleEmailValidation(ctx context.Context, data models.EventWorkerEmailValidation) error {
@@ -15,19 +15,19 @@ func (w *WorkerService) HandleEmailValidation(ctx context.Context, data models.E
cipher, err := w.CipherService.Cipher(ctx, data.OrgID)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil
}
data.Credentials.IMAP.Password, err = cipher.Decrypt(ctx, data.Credentials.IMAP.Password)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil
}
data.Credentials.SMTP.Password, err = cipher.Decrypt(ctx, data.Credentials.SMTP.Password)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil
}
@@ -40,7 +40,7 @@ func (w *WorkerService) HandleEmailValidation(ctx context.Context, data models.E
ok := false
defer func() {
if r := recover(); r != nil {
sentry.CurrentHub().Recover(r)
errs.Recover(r)
}
results <- ok
}()
@@ -65,7 +65,7 @@ func (w *WorkerService) HandleEmailValidation(ctx context.Context, data models.E
}
if err := w.Cache.Publish(ctx, "email_validation:"+data.ProcessID.String(), msg).Err(); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil
}
+5 -6
View File
@@ -4,17 +4,16 @@ import (
"errors"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func (w *WMail) CaptureError(err error) {
sentry.WithScope(func(scope *sentry.Scope) {
scope.SetTag("user_id", w.UserID.String())
scope.SetTag("email_id", w.ID.String())
sentry.CaptureException(err)
})
errs.CaptureException(err,
errs.Tag("user_id", w.UserID.String()),
errs.Tag("email_id", w.ID.String()),
)
// If the error is a critical mail error (auth, disabled, rate limit), publish
// an event so the consumer can mark the account inactive and stop syncing.
+5 -5
View File
@@ -7,7 +7,6 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/app/cipher"
"github.com/warmbly/warmbly/internal/infrastructure/codec"
@@ -15,6 +14,7 @@ import (
"github.com/warmbly/warmbly/internal/infrastructure/kafka"
"github.com/warmbly/warmbly/internal/infrastructure/storage"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/pkg/emsg"
"github.com/warmbly/warmbly/internal/repository"
)
@@ -328,22 +328,22 @@ func (p *publisher) publish(topic, key string, event interface{}) error {
}
if p.codec == nil {
sentry.CaptureException(fmt.Errorf("codec not configured, topic: %s", topic))
errs.CaptureException(fmt.Errorf("codec not configured, topic: %s", topic))
return fmt.Errorf("codec not configured")
}
if p.bus == nil {
sentry.CaptureException(fmt.Errorf("event bus not configured, topic: %s", topic))
errs.CaptureException(fmt.Errorf("event bus not configured, topic: %s", topic))
return fmt.Errorf("event bus not configured")
}
ctx := context.Background()
data, err := p.codec.Serialize(ctx, topic, event)
if err != nil {
sentry.CaptureException(fmt.Errorf("failed to serialize event: %w", err))
errs.CaptureException(fmt.Errorf("failed to serialize event: %w", err))
return err
}
if err := p.bus.Publish(ctx, topic, key, data); err != nil {
sentry.CaptureException(fmt.Errorf("failed to publish event: %w", err))
errs.CaptureException(fmt.Errorf("failed to publish event: %w", err))
return err
}
return nil
+27
View File
@@ -14,6 +14,7 @@ import (
"encoding/json"
"errors"
"fmt"
"html"
"net/http"
"os"
"path/filepath"
@@ -45,6 +46,15 @@ type Config struct {
// endpoints, the service's only dependency.
BackendURL string
InternalToken string
// BrowserSentryDSN is stamped into the page shell so the form app can
// report browser errors. Empty, which is the default, means the app never
// loads the SDK.
BrowserSentryDSN string
// Release tags those browser events with the build serving them.
Release string
// Environment is the deployment label those events carry, so staging and
// production form pages are separable in one project.
Environment string
// StaticDir is the built forms app (forms/dist): index.html is the page
// shell, assets/ the hashed bundles.
StaticDir string
@@ -77,6 +87,12 @@ func New(cfg Config) (*Server, error) {
if err != nil {
return nil, fmt.Errorf("formserver: cannot read %s/index.html (run `pnpm build` in forms/): %w", cfg.StaticDir, err)
}
// The browser DSN and the release are the same for every request, so they
// are stamped once here rather than per shell serve. An empty DSN leaves
// the placeholder empty and the page loads no reporting SDK at all.
shell = stampMeta(shell, "wf-sentry-dsn", cfg.BrowserSentryDSN)
shell = stampMeta(shell, "wf-release", cfg.Release)
shell = stampMeta(shell, "wf-environment", cfg.Environment)
limit := cfg.SubmitLimit
if limit <= 0 {
limit = submitDefaultLimit
@@ -147,6 +163,17 @@ func (s *Server) fetchForm(c *gin.Context) (*formwire.PublicForm, error) {
// stamps the real render token into it.
const wfTokenPlaceholder = `<meta name="wf-token" content="" />`
// stampMeta fills in one of the empty meta tags forms/index.html ships. An
// empty value is left alone, and so is a shell built before the tag existed.
func stampMeta(shell []byte, name, value string) []byte {
if value == "" {
return shell
}
placeholder := []byte(`<meta name="` + name + `" content="" />`)
stamped := []byte(`<meta name="` + name + `" content="` + html.EscapeString(value) + `" />`)
return bytes.Replace(shell, placeholder, stamped, 1)
}
// ServeFormShell serves the app shell for a published form. Public and
// unauthenticated: the unguessable public id is the capability. The form is
// resolved before serving so unknown ids 404 like any dead link, the
+30
View File
@@ -402,3 +402,33 @@ func TestFormServerSubmitRateLimit(t *testing.T) {
t.Fatalf("third submit status %d, want 429", w.Code)
}
}
// The browser DSN is stamped into the shell once at construction, so a page a
// stranger loads either carries the operator's DSN or carries an empty tag and
// loads no reporting SDK at all. There is no third state.
func TestFormShellStampsBrowserSentryDSN(t *testing.T) {
shell := []byte(`<!doctype html><html><head>` +
`<meta name="wf-sentry-dsn" content="" />` +
`<meta name="wf-release" content="" />` +
`</head><body></body></html>`)
unset := string(stampMeta(stampMeta(shell, "wf-sentry-dsn", ""), "wf-release", ""))
if !strings.Contains(unset, `<meta name="wf-sentry-dsn" content="" />`) {
t.Fatalf("empty DSN should leave the placeholder untouched, got %s", unset)
}
set := string(stampMeta(stampMeta(shell, "wf-sentry-dsn", `https://k@example.invalid/1`), "wf-release", "v1.2.3"))
if !strings.Contains(set, `<meta name="wf-sentry-dsn" content="https://k@example.invalid/1" />`) {
t.Fatalf("DSN was not stamped, got %s", set)
}
if !strings.Contains(set, `<meta name="wf-release" content="v1.2.3" />`) {
t.Fatalf("release was not stamped, got %s", set)
}
// A DSN is operator-supplied, so it must not be able to close the tag and
// inject markup into a page served to the public.
escaped := string(stampMeta(shell, "wf-sentry-dsn", `" /><script>alert(1)</script><meta x="`))
if strings.Contains(escaped, "<script>") {
t.Fatalf("stamped value was not escaped: %s", escaped)
}
}
+43 -13
View File
@@ -1,27 +1,57 @@
package db
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/getsentry/sentry-go"
"github.com/jackc/pgx/v5/pgconn"
"github.com/warmbly/warmbly/internal/observability/errs"
)
func CaptureError(err error, query string, params []any, operation string) {
if err == nil {
return
}
wrappedErr := fmt.Errorf("%s failed: %w (query: %s, params: %v)", operation, err, query, params)
sentry.CaptureException(wrappedErr)
sentry.WithScope(func(scope *sentry.Scope) {
scope.SetTag("db.operation", operation)
scope.SetTag("db.query", query) // Sanitize sensitive params in prod
if pgErr, ok := err.(*pgconn.PgError); ok {
scope.SetExtra("pg.code", pgErr.Code) // e.g., "23505" for unique violation
scope.SetExtra("pg.detail", pgErr.Detail)
scope.SetExtra("pg.hint", pgErr.Hint)
}
shape := paramShape(params)
wrappedErr := fmt.Errorf("%s failed: %w (query: %s, params: %s)", operation, err, query, shape)
scope.SetExtra("db.params", params) // Redact if sensitive
})
opts := []errs.Option{
errs.Tag("db.operation", operation),
errs.Tag("db.query", query),
errs.Extra("db.params", shape),
}
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
opts = append(opts,
errs.Extra("pg.code", pgErr.Code), // e.g., "23505" for unique violation
errs.Extra("pg.detail", pgErr.Detail),
errs.Extra("pg.hint", pgErr.Hint),
)
}
errs.CaptureException(wrappedErr, opts...)
}
// paramShape describes the query's arguments without their values.
//
// The values are the row: email addresses, password hashes, tokens, message
// bodies. The query text next to them already says which column each one is,
// so reporting them would put the most sensitive data in the system into an
// error tracker. How many there were and of what type is enough to tell one
// call site from another, which is all the report is for.
func paramShape(params []any) string {
if len(params) == 0 {
return "none"
}
kinds := make([]string, 0, len(params))
for _, p := range params {
if p == nil {
kinds = append(kinds, "nil")
continue
}
kinds = append(kinds, fmt.Sprintf("%T", p))
}
return strconv.Itoa(len(params)) + " (" + strings.Join(kinds, ", ") + ")"
}
+6 -6
View File
@@ -7,8 +7,8 @@ import (
"time"
"cloud.google.com/go/pubsub"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/observability/errs"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
@@ -197,7 +197,7 @@ func (p *StreamingPublisher) PublishTaskStatus(ctx context.Context, userID strin
}
if err := p.client.Publish(ctx, TopicTaskStatus, event, attrs); err != nil {
sentry.CaptureException(fmt.Errorf("failed to publish task status: %w", err))
errs.CaptureException(fmt.Errorf("failed to publish task status: %w", err))
}
}
@@ -227,7 +227,7 @@ func (p *StreamingPublisher) PublishEmailError(ctx context.Context, userID strin
}
if err := p.client.Publish(ctx, TopicEmailError, event, attrs); err != nil {
sentry.CaptureException(fmt.Errorf("failed to publish email error: %w", err))
errs.CaptureException(fmt.Errorf("failed to publish email error: %w", err))
}
}
@@ -256,7 +256,7 @@ func (p *StreamingPublisher) PublishEmailWarning(ctx context.Context, userID str
}
if err := p.client.Publish(ctx, TopicEmailWarning, event, attrs); err != nil {
sentry.CaptureException(fmt.Errorf("failed to publish email warning: %w", err))
errs.CaptureException(fmt.Errorf("failed to publish email warning: %w", err))
}
}
@@ -281,7 +281,7 @@ func (p *StreamingPublisher) PublishCampaignProgress(ctx context.Context, userID
}
if err := p.client.Publish(ctx, TopicCampaignUpdate, event, attrs); err != nil {
sentry.CaptureException(fmt.Errorf("failed to publish campaign progress: %w", err))
errs.CaptureException(fmt.Errorf("failed to publish campaign progress: %w", err))
}
}
@@ -307,6 +307,6 @@ func (p *StreamingPublisher) PublishWarmupStats(ctx context.Context, userID stri
}
if err := p.client.Publish(ctx, TopicWarmupUpdate, event, attrs); err != nil {
sentry.CaptureException(fmt.Errorf("failed to publish warmup stats: %w", err))
errs.CaptureException(fmt.Errorf("failed to publish warmup stats: %w", err))
}
}
+4 -4
View File
@@ -4,8 +4,8 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/config"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/repository"
)
@@ -42,7 +42,7 @@ func (j *AuditRetentionJob) Run(ctx context.Context) error {
cutoff := time.Now().AddDate(0, 0, -days)
if _, err := j.repo.PruneOlderThan(ctx, cutoff); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
return nil
@@ -70,14 +70,14 @@ func (s *AuditRetentionScheduler) Start(ctx context.Context) {
defer ticker.Stop()
if err := s.job.Run(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
for {
select {
case <-ticker.C:
if err := s.job.Run(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
case <-s.stopCh:
return
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/app/dangerzone"
)
@@ -26,10 +26,10 @@ func NewDangerZoneJob(svc dangerzone.Service) *DangerZoneJob {
// got marked failed.
func (j *DangerZoneJob) Run(ctx context.Context) {
if _, _, err := j.svc.ExecuteDuePendingDeletions(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
if err := j.svc.DispatchReminders(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
emailverifyapp "github.com/warmbly/warmbly/internal/app/emailverify"
)
@@ -39,7 +39,7 @@ func (j *EmailVerificationJob) Run(ctx context.Context) error {
for {
n, err := j.svc.VerifyPending(ctx, j.batchSize)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
if n < j.batchSize || ctx.Err() != nil {
@@ -84,7 +84,7 @@ func (s *EmailVerificationScheduler) Start(ctx context.Context) {
return
}
if err := s.job.Run(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
}
+3 -3
View File
@@ -4,8 +4,8 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/rs/zerolog/log"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/app/guardrail"
"github.com/warmbly/warmbly/internal/repository"
@@ -35,7 +35,7 @@ func (j *GuardrailJob) Run(ctx context.Context) {
if j.svc != nil {
paused, err := j.svc.Sweep(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
} else if paused > 0 {
log.Info().Int("paused", paused).Msg("guardrail sweep paused campaigns")
}
@@ -43,7 +43,7 @@ func (j *GuardrailJob) Run(ctx context.Context) {
if j.behaviorRepo != nil {
if _, err := j.behaviorRepo.PurgePlansBefore(ctx, time.Now().Add(-planRetention)); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/app/orgtransfer"
)
@@ -34,7 +34,7 @@ func (j *OrgTransferJob) Run(ctx context.Context) {
return
}
if _, err := j.svc.PurgeExpiredExports(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/app/placement"
)
@@ -41,7 +41,7 @@ func (p *PlacementPoller) Run(ctx context.Context) error {
return nil
}
if err := p.svc.ClassifyPending(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
return nil
@@ -56,7 +56,7 @@ func (p *PlacementPoller) Start(ctx context.Context) {
select {
case <-ticker.C:
if err := p.Run(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
case <-p.stopCh:
return
+8 -8
View File
@@ -5,12 +5,12 @@ import (
"fmt"
"time"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/warmbly/warmbly/internal/models"
"github.com/warmbly/warmbly/internal/notify"
"github.com/warmbly/warmbly/internal/notify/templates"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/repository"
)
@@ -72,26 +72,26 @@ func (j *TrialExpirationJob) Run(ctx context.Context) error {
// Find expired trials without paid subscription
expiredSubs, err := repository.GetExpiredTrialsWithoutPayment(ctx, j.db)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return fmt.Errorf("failed to get expired trials: %w", err)
}
for _, sub := range expiredSubs {
// Pause all active campaigns for this organization
if err := repository.PauseCampaignsByOrganizationID(ctx, j.db, sub.OrganizationID, "paused_trial_expired"); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
// Continue processing other organizations
}
// Disable warmup on all email accounts (they're already blocked, but clean up)
if err := repository.DisableWarmupByOrganizationID(ctx, j.db, sub.OrganizationID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
// Continue processing other organizations
}
// Mark subscription as expired
if err := repository.MarkSubscriptionTrialExpired(ctx, j.db, sub.ID); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
// Continue processing other users
}
@@ -132,7 +132,7 @@ func (j *TrialExpirationJob) notifyTrialExpired(ctx context.Context, userID inte
}
if err := j.emailNotificationService.Send(ctx, []string{userEmail}, nil, nil, subject, body); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
}
@@ -159,14 +159,14 @@ func (s *TrialExpirationScheduler) Start(ctx context.Context) {
// Run immediately on start
if err := s.job.Run(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
for {
select {
case <-ticker.C:
if err := s.job.Run(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
case <-s.stopCh:
return
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
emailverifyapp "github.com/warmbly/warmbly/internal/app/emailverify"
)
@@ -39,7 +39,7 @@ func (j *DeliveryEvidenceJob) Start(ctx context.Context) {
for {
n, err := j.evidence.CreditCleanDeliveries(ctx, j.batch)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
break
}
if n < j.batch || ctx.Err() != nil {
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/app/warmupcontent"
)
@@ -39,7 +39,7 @@ func (p *WarmupBatchPoller) Run(ctx context.Context) error {
return nil
}
if err := p.svc.PollBatches(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
return nil
@@ -54,7 +54,7 @@ func (p *WarmupBatchPoller) Start(ctx context.Context) {
select {
case <-ticker.C:
if err := p.Run(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
case <-p.stopCh:
return
+4 -4
View File
@@ -5,7 +5,7 @@ import (
"sync"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/app/warmupcontent"
"github.com/warmbly/warmbly/internal/repository"
@@ -37,7 +37,7 @@ func (j *WarmupGenerationJob) Run(ctx context.Context) error {
}
settings, err := j.repo.GetGenerationSettings(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
if settings == nil || !settings.ScheduleEnabled {
@@ -54,7 +54,7 @@ func (j *WarmupGenerationJob) Run(ctx context.Context) error {
j.mu.Unlock()
if err := j.svc.RunScheduled(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
return nil
@@ -86,7 +86,7 @@ func (s *WarmupGenerationScheduler) Start(ctx context.Context) {
select {
case <-ticker.C:
if err := s.job.Run(ctx); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
}
case <-s.stopCh:
return
+3 -3
View File
@@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
"github.com/warmbly/warmbly/internal/repository"
)
@@ -24,13 +24,13 @@ func (j *WebsiteTrackingRetentionJob) Run(ctx context.Context) error {
}
cutoffs, err := j.repo.RetentionCutoffs(ctx, time.Now())
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
var last error
for _, c := range cutoffs {
if _, err := j.repo.PruneBefore(ctx, c.OrganizationID, c.Before); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
last = err
}
}
+4 -4
View File
@@ -7,7 +7,7 @@ import (
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/sesv2"
"github.com/aws/aws-sdk-go-v2/service/sesv2/types"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
)
type EmailNotificationService interface {
@@ -29,7 +29,7 @@ type emailNotificationService struct {
func NewEmailNotficiationService(ctx context.Context, name, address string) (EmailNotificationService, error) {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return nil, err
}
@@ -67,7 +67,7 @@ func (s *emailNotificationService) Send(ctx context.Context, to, cc, bcc []strin
_, err := s.Client.SendEmail(ctx, input)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
@@ -100,7 +100,7 @@ func (s *emailNotificationService) SendOutreach(ctx context.Context, to []string
_, err := s.Client.SendEmail(ctx, input)
if err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
return nil
+2 -2
View File
@@ -11,9 +11,9 @@ import (
"strings"
"time"
"github.com/getsentry/sentry-go"
wsmtp "github.com/warmbly/warmbly/internal/client/smtpimap/smtp"
"github.com/warmbly/warmbly/internal/config"
"github.com/warmbly/warmbly/internal/observability/errs"
)
const smtpSendTimeout = 30 * time.Second
@@ -59,7 +59,7 @@ func (s *smtpEmailNotificationService) send(ctx context.Context, to, cc, bcc []s
}
if err := s.deliver(ctx, recipients, msg); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return err
}
return nil
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"os"
"strings"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// ─── Centralized Business Details ────────────────────────────────
@@ -86,7 +86,7 @@ func renderEmail(subject, content string) (string, error) {
}
var buf bytes.Buffer
if err := baseTmpl.Execute(&buf, data); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", err
}
return buf.String(), nil
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"html/template"
"time"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// Danger-zone (scheduled deletion) emails. These move off the old
@@ -162,7 +162,7 @@ var (
func renderDeletion(tmpl *template.Template, subject string, data any) (string, error) {
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", err
}
return renderEmail(subject, buf.String())
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"fmt"
"html/template"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// Notification digest: several pending notifications bundled into one email
@@ -67,7 +67,7 @@ func GenerateDigestHTML(count int, items []DigestItem) (string, error) {
}{Count: count, Items: items, AppURL: AppURL}
var buf bytes.Buffer
if err := digestTmpl.Execute(&buf, data); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", err
}
return renderEmail(fmt.Sprintf("%d updates in your Warmbly workspace", count), buf.String())
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"bytes"
"html/template"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// Team invitation rendered on the shared base shell. The org name and
@@ -49,7 +49,7 @@ func GenerateInvitationHTML(inviterName, orgName, acceptURL string) (string, err
}{InviterName: inviterName, OrgName: orgName, AcceptURL: acceptURL}
var buf bytes.Buffer
if err := invitationTmpl.Execute(&buf, data); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", err
}
return renderEmail("You've been invited to Warmbly", buf.String())
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"bytes"
"html/template"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// Dashboard-style content. Small uppercase eyebrow, slate-900 plain
@@ -47,7 +47,7 @@ func GenerateLoginCodeHTML(code string) (string, error) {
data := struct{ Code string }{Code: code}
var buf bytes.Buffer
if err := loginCodeTmpl.Execute(&buf, data); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", err
}
return renderEmail("Your Login Code", buf.String())
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"bytes"
"html/template"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
)
// Generic transactional notification used by the in-app notification
@@ -55,7 +55,7 @@ func GenerateNotificationHTML(title, body, ctaURL, ctaLabel string) (string, err
}{Title: title, Body: body, CTAURL: ctaURL, CTALabel: ctaLabel}
var buf bytes.Buffer
if err := notificationTmpl.Execute(&buf, data); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", err
}
subject := title
@@ -4,7 +4,7 @@ import (
"bytes"
"html/template"
"github.com/getsentry/sentry-go"
"github.com/warmbly/warmbly/internal/observability/errs"
)
const registrationCodeContent = `
@@ -37,7 +37,7 @@ func GenerateRegistrationCodeHTML(code string) (string, error) {
data := struct{ Code string }{Code: code}
var buf bytes.Buffer
if err := registrationCodeTmpl.Execute(&buf, data); err != nil {
sentry.CaptureException(err)
errs.CaptureException(err)
return "", err
}
return renderEmail("Your Verification Code", buf.String())

Some files were not shown because too many files have changed in this diff Show More