mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-08 08:03:28 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
/worker
|
||||
/backend
|
||||
/seed
|
||||
/forms
|
||||
/migrate
|
||||
/updater
|
||||
/warmblyctl
|
||||
|
||||
+19
-3
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+290
@@ -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,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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, ",") {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+65
@@ -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':
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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.
|
||||
});
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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,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
|
||||
|
||||
@@ -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,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ", ") + ")"
|
||||
}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user