Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8bfbb50
feat: add GitHub Action for source maps upload
FelineFantasy Jul 7, 2026
e5e6e18
docs: add GitHub Action example to source-maps documentation
FelineFantasy Jul 7, 2026
ff1351f
Merge branch 'main' into main
FelineFantasy Jul 7, 2026
77532e6
Merge branch 'main' into main
FelineFantasy Jul 7, 2026
637ddd5
refactor: move action to .github/actions/ and rewrite to json api
FelineFantasy Jul 7, 2026
ea8e989
style: remove deprecated root action.yml file
FelineFantasy Jul 7, 2026
1bc0d90
docs: update workflow example with new action path and inputs
FelineFantasy Jul 7, 2026
cf7bfdb
Merge branch 'main' into main
FelineFantasy Jul 7, 2026
f612c9f
Merge branch 'develop' into main
FelineFantasy Jul 7, 2026
9fa8670
Merge branch 'develop' into main
FelineFantasy Jul 7, 2026
0e09426
Merge branch 'develop' into main
FelineFantasy Jul 7, 2026
e712313
Merge branch 'develop' into main
FelineFantasy Jul 7, 2026
0822280
Merge branch 'develop' into main
FelineFantasy Jul 7, 2026
4f6b8f5
Merge branch 'develop' into main
FelineFantasy Jul 7, 2026
4c96083
Merge branch 'develop' into main
FelineFantasy Jul 7, 2026
d71c8c3
Merge branch 'develop' into main
FelineFantasy Jul 7, 2026
0a31472
Merge branch 'develop' into main
FelineFantasy Jul 8, 2026
9503869
fix: correct bundle_url for nested files in upload action
FelineFantasy Jul 8, 2026
a90ef90
docs: fix session cookie expression escaping in source-maps.md
FelineFantasy Jul 8, 2026
1ba4146
Merge pull request #296 from Telemetry-Tracker/develop
unjica Jul 9, 2026
fc180bd
Merge branch 'develop' into main
unjica Jul 9, 2026
d559103
fix: use path.relative for bundle_url in action
FelineFantasy Jul 9, 2026
980d099
docs: translate comments to English in action
FelineFantasy Jul 9, 2026
6b9307b
Merge branch 'main' of https://github.com/Telemetry-Tracker/telemetry…
unjica Jul 9, 2026
b39757e
fix(ci): drop migrate from release email workflow
unjica Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions .github/actions/upload-source-maps/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
name: 'Upload Source Maps'
description: 'Uploads source maps to Telemetry-Tracker on release'
inputs:
session_cookie:
description: 'Dashboard session cookie for authentication'
required: true
project_id:
description: 'Telemetry-Tracker Project ID (X-Project-Id)'
required: true
release:
description: 'Release version/tag'
required: true
app:
description: 'Application name'
required: true
artifact_path:
description: 'Path to look for source map files recursively'
required: false
default: './dist'
base_url:
description: 'Base URL for your deployed JS bundles (e.g., https://example.com)'
required: true

runs:
using: 'composite'
steps:
- name: Upload maps via JSON API
shell: bash
run: |
node -e "
const fs = require('fs');
const path = require('path');
const http = require('https');

function findFiles(dir) {
let results = [];
if (!fs.existsSync(dir)) return results;
const list = fs.readdirSync(dir);
list.forEach(file => {
file = path.join(dir, file);
const stat = fs.statSync(file);
if (stat && stat.isDirectory()) results = results.concat(findFiles(file));
else if (file.endsWith('.map')) results.push(file);
});
return results;
}

function uploadFile(filePath) {
return new Promise((resolve, reject) => {
const content = fs.readFileSync(filePath, 'utf8');
const artifactPath = '${{ inputs.artifact_path }}';
// 1. Get path relative to artifact_path
const resolvedArtifactPath = path.resolve(artifactPath);
const resolvedFilePath = path.resolve(filePath);
const relativePath = path.relative(resolvedArtifactPath, resolvedFilePath);
// 2. Remove .map to get the original JS path
const jsPath = relativePath.replace(/\.map$/, '');
// 3. Build the full URL
const bundleUrl = \`\${{ inputs.base_url }}/\${jsPath}\`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing slash doubles URL path

Medium Severity

bundle_url is built as `${base_url}/${jsPath}` without normalizing base_url. A trailing slash on base_url yields a double slash in the path, which can fail strict pathname matching during symbolication.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b39757e. Configure here.


const payload = JSON.stringify({
app: '${{ inputs.app }}',
release: '${{ inputs.release }}',
bundle_url: bundleUrl,
content: JSON.parse(content)
});

const options = {
hostname: 'api.telemetry-tracker.com',
path: '/api/project/source-maps',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded production API hostname

Medium Severity

The upload step always posts to api.telemetry-tracker.com with no input for API base URL. Self-hosted or staging deployments cannot target their own API host, so the action fails or uploads maps to the wrong environment unless the file is forked and edited.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b39757e. Configure here.

method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Project-Id': '${{ inputs.project_id }}',
'Cookie': '${{ inputs.session_cookie }}',
'Content-Length': Buffer.byteLength(payload)
}
};

const req = http.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode >= 400) {
reject(new Error(\`Failed to upload \${path.basename(filePath)}: \${res.statusCode} - \${body}\`));
} else {
console.log(\`Successfully uploaded \${path.basename(filePath)}\`);
resolve();
}
});
});

req.on('error', (e) => reject(e));
req.write(payload);
req.end();
});
}

(async () => {
try {
const files = findFiles('${{ inputs.artifact_path }}');
if (files.length === 0) {
console.log('No source maps found to upload.');
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing maps exits success

Low Severity

When no .map files are found under artifact_path, the script logs and returns without process.exit(1). A misconfigured path or failed build step still leaves the release upload job green.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b39757e. Configure here.

}
for (const file of files) {
await uploadFile(file);
}
} catch (err) {
console.error(err.message);
process.exit(1);
}
})();
"
6 changes: 0 additions & 6 deletions .github/workflows/release-email.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,6 @@ jobs:
echo "reason=$(node -e "console.log(JSON.parse(process.argv[1]).reason)" "$BUMP")" >> "$GITHUB_OUTPUT"
echo "Bump decision: $BUMP"

- name: Apply database migrations
if: steps.bump.outputs.should_send == 'true'
run: pnpm --filter api exec prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}

- name: Send product update email
if: steps.bump.outputs.should_send == 'true'
working-directory: apps/api
Expand Down
11 changes: 6 additions & 5 deletions docs/MARKETING-EMAIL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,20 @@ Delivery is recorded in `MarketingReleaseEmailSend` (one row per subscriber per
Pushing a semver tag `vX.Y.Z` to GitHub triggers **Release product email** when **X** (major) or **Y** (minor) increases vs the previous tag; **Z**-only (patch/hotfix) tags are skipped:

1. Resolves the previous semver tag and compares **X** and **Y** (Z-only → skip).
2. Runs `prisma migrate deploy` against production (applies pending migrations from the tagged commit, including `MarketingReleaseEmailSend`).
3. Runs `send-release-email.ts --version=X.Y.Z --previous-version=…` with repository secrets.
2. Runs `send-release-email.ts --version=X.Y.Z --previous-version=…` with repository secrets.

Required GitHub repository secrets (production):
Apply production migrations **before tagging** ([RELEASE.md](./RELEASE.md#3-database-migrations-production)); this workflow does not run `prisma migrate deploy` (GitHub Actions cannot reach Railway private `*.railway.internal` URLs).

Required GitHub secrets in the **`production`** environment:

| Secret | Purpose |
|--------|---------|
| `DATABASE_URL` | Production Postgres (`MarketingSubscriber`) |
| `DATABASE_URL` | Production Postgres **public** URL (Railway TCP proxy / public host — not `postgres.railway.internal`) |
| `RESEND_API_KEY` | Resend API |
| `TELEMETRY_EMAIL_FROM` | From address |
| `TELEMETRY_DASHBOARD_ORIGIN` | Unsubscribe / docs links (e.g. `https://telemetry-tracker.com`) |

Store these in the GitHub **`production`** environment (the workflow job uses `environment: production`). To retry a send without re-tagging, use **Actions → Release product email → Run workflow** with `version` and `previous_version`.
Store these in the GitHub **`production`** environment. To retry a send without re-tagging, use **Actions → Release product email → Run workflow** with `version` and `previous_version`.

The workflow runs **after** the tag is pushed — finalize `CHANGELOG.md` on `main` **before** tagging so the email body matches the release.

Expand Down
4 changes: 2 additions & 2 deletions docs/RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ Always document **migrations**, **new env vars**, and **breaking changes** in CH

1. **Finalize CHANGELOG** — rename `[Unreleased]` to `[X.Y.Z] - YYYY-MM-DD`. Prefer doing this in the **`develop` → `main`** release PR so `develop` and `main` stay aligned; if you commit on `main` after promotion, you **must** sync `develop` in step 9.
2. **Deploy** — Railway rebuilds `main` automatically when the release PR merges; see [Deploy runbook](#deploy-runbook-railway).
3. **Production DB** — run [migrations](#3-database-migrations-production) **before tagging** on MINOR (Y) / MAJOR (X) releases (the [Release product email](../.github/workflows/release-email.yml) workflow also runs `prisma migrate deploy` immediately before send, but apply migrations here as part of the normal release gate).
3. **Production DB** — run [migrations](#3-database-migrations-production) **before tagging** on MINOR (Y) / MAJOR (X) releases. The release email workflow reads production Postgres but does not migrate (use the public `DATABASE_URL` in GitHub secrets — see [MARKETING-EMAIL.md](./MARKETING-EMAIL.md#automated-send-minor--major-tags)).
4. **Post-deploy** — [verification](#post-deploy-verification).
5. **Tag** — after deploy and migrations are green (tag push triggers the product update email workflow for MINOR/MAJOR — **X** or **Y** bump, not Z-only hotfixes):
```bash
Expand Down Expand Up @@ -198,7 +198,7 @@ On push and pull requests to **`develop`** and **`main`**, CI runs ([`.github/wo

### 3. Database migrations (production)

CI does **not** migrate your production database during normal builds. Apply migrations after API deploy and **before pushing a MINOR (Y) / MAJOR (X) tag** (see [On `main` after promotion](#on-main-after-promotion)). The [Release product email](../.github/workflows/release-email.yml) workflow also runs `prisma migrate deploy` immediately before sending so delivery records can be written.
CI does **not** migrate your production database during normal builds. Apply migrations after API deploy and **before pushing a MINOR (Y) / MAJOR (X) tag** (see [On `main` after promotion](#on-main-after-promotion)). The [Release product email](../.github/workflows/release-email.yml) workflow connects to production Postgres for the send ledger only — use Railway’s **public** database URL in GitHub secrets, not `postgres.railway.internal`.

```bash
DATABASE_URL="postgresql://..." pnpm --filter api exec prisma migrate deploy
Expand Down
26 changes: 26 additions & 0 deletions docs/source-maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,29 @@ Symbolication is display-only; grouping fingerprints stay on raw minified stacks
- [sdk-core.md](./sdk-core.md) — `trackError`, `release` in payloads
- [ARCHITECTURE.md](./ARCHITECTURE.md) — ingest pipeline
- [ENTITLEMENTS.md](./ENTITLEMENTS.md) — retention by plan tier

## GitHub Action Workflow Example

You can automatically upload source maps on every release using our GitHub Action:

```yaml
name: Release Configuration
on:
release:
types: [published]

jobs:
upload-maps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Upload Source Maps
uses: ./.github/actions/upload-source-maps
with:
session_cookie: ${{ secrets.TT_SESSION_COOKIE }}
project_id: "your-project-uuid-here"
release: ${{ github.event.release.tag_name }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Release tag keeps v prefix

Medium Severity

The workflow example passes github.event.release.tag_name (e.g. v1.6.0) as release, while upload API docs and tests use semver without a v prefix. Maps are keyed by exact release string, so symbolication will not match errors sent with 1.6.0.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b39757e. Configure here.

app: "my-telemetry-app"
artifact_path: "./dist"
base_url: "https://example.com"
```
Loading