Skip to content

Commit 646a766

Browse files
authored
Add Sentry integration for error reporting (#333)
* Add Sentry integration for error reporting * Right I was working on something else * Use uuid * Add published distinction * Create separate package for the sentry stuff * dont be like that
1 parent 9f3390a commit 646a766

File tree

10 files changed

+267
-5
lines changed

10 files changed

+267
-5
lines changed

.config/rollup.base.config.mjs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
normalizeId,
2828
resolveId
2929
} from '../scripts/utils/packages.js'
30+
import { envAsBoolean } from '@socketsecurity/registry/lib/env'
3031

3132
const require = createRequire(import.meta.url)
3233

@@ -45,6 +46,11 @@ const {
4546
tsconfigPath
4647
} = constants
4748

49+
const IS_SENTRY_BUILD = envAsBoolean(process.env['SOCKET_WITH_SENTRY']);
50+
console.log('IS_SENTRY_BUILD:', IS_SENTRY_BUILD);
51+
const IS_PUBLISH = envAsBoolean(process.env['SOCKET_IS_PUBLISHED'])
52+
console.log('IS_PUBLISH:', IS_PUBLISH);
53+
4854
const SOCKET_INTEROP = '_socketInterop'
4955

5056
const constantsSrcPath = path.join(rootSrcPath, `${CONSTANTS}.ts`)
@@ -125,6 +131,33 @@ function isAncestorsExternal(id, depStats) {
125131
return true
126132
}
127133

134+
135+
function sentryAliasingPlugin() {
136+
return {
137+
name: 'sentry-alias-plugin',
138+
order: 'post',
139+
resolveId(source, importer) {
140+
// By default use the noop file for crash handler.
141+
// When at build-time the `SOCKET_WITH_SENTRY` flag is set, route to use
142+
// the Sentry specific files instead.
143+
if (source === './initialize-crash-handler') {
144+
return IS_SENTRY_BUILD
145+
? `${rootSrcPath}/initialize-sentry.ts`
146+
: `${rootSrcPath}/initialize-crash-handler.ts`;
147+
}
148+
149+
if (source === './handle-crash') {
150+
return IS_SENTRY_BUILD
151+
? `${rootSrcPath}/handle-crash-with-sentry.ts`
152+
: `${rootSrcPath}/handle-crash.ts`;
153+
}
154+
155+
return null;
156+
}
157+
};
158+
}
159+
160+
128161
export default function baseConfig(extendConfig = {}) {
129162
const depStats = {
130163
dependencies: { __proto__: null },
@@ -215,6 +248,7 @@ export default function baseConfig(extendConfig = {}) {
215248
},
216249
...extendConfig,
217250
plugins: [
251+
sentryAliasingPlugin(), // Should go real early.
218252
customResolver,
219253
jsonPlugin(),
220254
tsPlugin({

.config/rollup.dist.config.mjs

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@ import {
33
existsSync,
44
mkdirSync,
55
rmSync,
6-
writeFileSync
6+
writeFileSync,
7+
readFileSync
78
} from 'node:fs'
89
import path from 'node:path'
10+
import { spawnSync } from 'node:child_process'
11+
import { randomUUID } from 'node:crypto'
912

1013
import { globSync as tinyGlobSync } from 'tinyglobby'
1114

@@ -26,6 +29,8 @@ import {
2629
isBuiltin,
2730
normalizeId
2831
} from '../scripts/utils/packages.js'
32+
import { envAsBoolean } from '@socketsecurity/registry/lib/env'
33+
import assert from 'node:assert'
2934

3035
const {
3136
BABEL_RUNTIME,
@@ -44,6 +49,9 @@ const CONSTANTS_JS = `${CONSTANTS}.js`
4449
const CONSTANTS_STUB_CODE = createStubCode(`../${CONSTANTS_JS}`)
4550
const VENDOR_JS = `${VENDOR}.js`
4651

52+
const IS_SENTRY_BUILD = envAsBoolean(process.env['SOCKET_WITH_SENTRY']);
53+
const IS_PUBLISH = envAsBoolean(process.env['SOCKET_IS_PUBLISHED'])
54+
4755
const distConstantsPath = path.join(rootDistPath, CONSTANTS_JS)
4856
const distModuleSyncPath = path.join(rootDistPath, MODULE_SYNC)
4957
const distRequirePath = path.join(rootDistPath, REQUIRE)
@@ -52,6 +60,10 @@ const editablePkgJson = readPackageJsonSync(rootPath, { editable: true })
5260

5361
const processEnvTapRegExp =
5462
/\bprocess\.env(?:\.TAP|\[['"]TAP['"]\])(\s*\?[^:]+:\s*)?/g
63+
const processEnvSocketIsPublishedRegExp =
64+
/\bprocess\.env(?:\.SOCKET_IS_PUBLISHED|\[['"]SOCKET_IS_PUBLISHED['"]\])/g
65+
const processEnvSocketCliVersionRegExp =
66+
/\bprocess\.env(?:\.SOCKET_CLI_VERSION|\[['"]SOCKET_CLI_VERSION['"]\])/g
5567

5668
function createStubCode(relFilepath) {
5769
return `'use strict'\n\nmodule.exports = require('${relFilepath}')\n`
@@ -104,13 +116,28 @@ function updateDepStatsSync(depStats) {
104116
delete depStats.dependencies[key]
105117
}
106118
}
119+
120+
assert(Object.keys(editablePkgJson?.content?.bin).join(',') === 'socket,socket-npm,socket-npx', 'If this fails, make sure to update the rollup sentry override for .bin to match the regular build!');
121+
if (IS_SENTRY_BUILD) {
122+
editablePkgJson.content['name'] = '@socketsecurity/socket-with-sentry'
123+
editablePkgJson.content['description'] = "CLI tool for Socket.dev, includes Sentry error handling, otherwise identical to the regular `socket` package"
124+
editablePkgJson.content['bin'] = {
125+
"socket-with-sentry": "bin/cli.js",
126+
"socket-npm-with-sentry": "bin/npm-cli.js",
127+
"socket-npx-with-sentry": "bin/npx-cli.js"
128+
}
129+
// Add Sentry as a regular dep for this build
130+
depStats.dependencies['@sentry/node'] = '9.1.0';
131+
}
132+
107133
depStats.dependencies = toSortedObject(depStats.dependencies)
108134
depStats.devDependencies = toSortedObject(depStats.devDependencies)
109135
depStats.esm = toSortedObject(depStats.esm)
110136
depStats.external = toSortedObject(depStats.external)
111137
depStats.transitives = toSortedObject(depStats.transitives)
112138
// Write dep stats.
113139
writeFileSync(depStatsPath, `${formatObject(depStats)}\n`, 'utf8')
140+
114141
// Update dependencies with additional inlined modules.
115142
editablePkgJson
116143
.update({
@@ -120,6 +147,40 @@ function updateDepStatsSync(depStats) {
120147
}
121148
})
122149
.saveSync()
150+
151+
if (IS_SENTRY_BUILD) {
152+
// Replace the name in the package lock too, just in case.
153+
const lock = readFileSync('package-lock.json', 'utf8');
154+
// Note: this should just replace the first occurrence, even if there are more
155+
const lock2 = lock.replace('"name": "socket",', '"name": "@socketsecurity/socket-with-sentry",')
156+
writeFileSync('package-lock.json', lock2)
157+
}
158+
}
159+
160+
function versionBanner(_chunk) {
161+
let pkgJsonVersion = 'unknown';
162+
try { pkgJsonVersion = JSON.parse(readFileSync('package.json', 'utf8'))?.version ?? 'unknown' } catch {}
163+
164+
let gitHash = ''
165+
try {
166+
const obj = spawnSync('git', ['rev-parse','--short', 'HEAD']);
167+
if (obj.stdout) {
168+
gitHash = obj.stdout.toString('utf8').trim()
169+
}
170+
} catch {}
171+
172+
// Make each build generate a unique version id, regardless
173+
// Mostly for development: confirms the build refreshed. For prod
174+
// builds the git hash should suffice to identify the build.
175+
const rng = randomUUID().split('-')[0];
176+
177+
return `
178+
var SOCKET_CLI_PKG_JSON_VERSION = "${pkgJsonVersion}"
179+
var SOCKET_CLI_GIT_HASH = "${gitHash}"
180+
var SOCKET_CLI_BUILD_RNG = "${rng}"
181+
var SOCKET_PUB = ${IS_PUBLISH}
182+
var SOCKET_CLI_VERSION = "${pkgJsonVersion}:${gitHash}:${rng}${IS_PUBLISH ? ':pub':''}"
183+
`.trim().split('\n').map(s => s.trim()).join('\n')
123184
}
124185

125186
export default () => {
@@ -132,12 +193,15 @@ export default () => {
132193
},
133194
output: [
134195
{
196+
intro: versionBanner, // Note: "banner" would defeat "use strict"
135197
dir: path.relative(rootPath, distModuleSyncPath),
136198
entryFileNames: '[name].js',
137199
exports: 'auto',
138200
externalLiveBindings: false,
139201
format: 'cjs',
140-
freeze: false
202+
freeze: false,
203+
sourcemap: true,
204+
sourcemapDebugIds: true,
141205
}
142206
],
143207
external(id_) {
@@ -182,12 +246,15 @@ export default () => {
182246
},
183247
output: [
184248
{
249+
intro: versionBanner, // Note: "banner" would defeat "use strict"
185250
dir: path.relative(rootPath, distRequirePath),
186251
entryFileNames: '[name].js',
187252
exports: 'auto',
188253
externalLiveBindings: false,
189254
format: 'cjs',
190-
freeze: false
255+
freeze: false,
256+
sourcemap: true,
257+
sourcemapDebugIds: true,
191258
}
192259
],
193260
plugins: [
@@ -197,6 +264,19 @@ export default () => {
197264
find: processEnvTapRegExp,
198265
replace: (_match, ternary) => (ternary ? '' : 'false')
199266
}),
267+
// Replace `process.env.SOCKET_IS_PUBLISHED` with a boolean
268+
socketModifyPlugin({
269+
find: processEnvSocketIsPublishedRegExp,
270+
// Note: these are going to be bools in JS, not strings
271+
replace: () => (IS_PUBLISH ? 'true' : 'false')
272+
}),
273+
// Replace `process.env.SOCKET_CLI_VERSION` with var ref that rollup
274+
// adds to the top of each file.
275+
socketModifyPlugin({
276+
find: processEnvSocketCliVersionRegExp,
277+
replace: 'SOCKET_CLI_VERSION'
278+
}),
279+
200280
{
201281
generateBundle(_options, bundle) {
202282
for (const basename of Object.keys(bundle)) {

.github/workflows/provenance.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ jobs:
2121
scope: "@socketsecurity"
2222
- run: npm install -g npm@latest
2323
- run: npm ci
24-
- run: npm run build:dist
24+
- run: SOCKET_IS_PUBLISHED=1 npm run build:dist
25+
- run: npm publish --provenance --access public
26+
env:
27+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28+
- run: SOCKET_IS_PUBLISHED=1 SOCKET_WITH_SENTRY=1 npm run build:dist
2529
- run: npm publish --provenance --access public
2630
env:
2731
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

src/cli.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#!/usr/bin/env node
22

3+
// Keep this on top, no `from`, just init:
4+
import './initialize-crash-handler'
5+
36
import process from 'node:process'
47
import { pathToFileURL } from 'node:url'
58

@@ -20,6 +23,7 @@ import { cmdLogout } from './commands/logout/cmd-logout'
2023
import { cmdManifest } from './commands/manifest/cmd-manifest'
2124
import { cmdNpm } from './commands/npm/cmd-npm'
2225
import { cmdNpx } from './commands/npx/cmd-npx'
26+
import { cmdOops } from './commands/oops/cmd-oops'
2327
import { cmdOptimize } from './commands/optimize/cmd-optimize'
2428
import { cmdOrganizations } from './commands/organizations/cmd-organizations'
2529
import { cmdRawNpm } from './commands/raw-npm/cmd-raw-npm'
@@ -30,6 +34,7 @@ import { cmdScan } from './commands/scan/cmd-scan'
3034
import { cmdThreatFeed } from './commands/threat-feed/cmd-threat-feed'
3135
import { cmdWrapper } from './commands/wrapper/cmd-wrapper'
3236
import constants from './constants'
37+
import { handle } from './handle-crash'
3338
import { AuthError, InputError } from './utils/errors'
3439
import { logSymbols } from './utils/logging'
3540
import { meowWithSubcommands } from './utils/meow-with-subcommands'
@@ -55,6 +60,7 @@ void (async () => {
5560
logout: cmdLogout,
5661
npm: cmdNpm,
5762
npx: cmdNpx,
63+
oops: cmdOops,
5864
optimize: cmdOptimize,
5965
organization: cmdOrganizations,
6066
'raw-npm': cmdRawNpm,
@@ -106,6 +112,9 @@ void (async () => {
106112
if (errorBody) {
107113
console.error(`\n${errorBody}`)
108114
}
109-
process.exit(1)
115+
116+
process.exitCode = 1
117+
118+
await handle(err)
110119
}
111120
})()

src/commands/oops/cmd-oops.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import meowOrExit from 'meow'
2+
3+
import { CliCommandConfig } from '../../utils/meow-with-subcommands.ts'
4+
5+
const config: CliCommandConfig = {
6+
commandName: 'oops',
7+
description: 'Trigger an intentional error (for development)',
8+
hidden: true,
9+
flags: {},
10+
help: (parentName, config) => `
11+
Usage
12+
$ ${parentName} ${config.commandName}
13+
14+
Don't run me.
15+
`
16+
}
17+
18+
export const cmdOops = {
19+
description: config.description,
20+
hidden: config.hidden,
21+
run
22+
}
23+
24+
async function run(
25+
argv: readonly string[],
26+
importMeta: ImportMeta,
27+
{ parentName }: { parentName: string }
28+
): Promise<void> {
29+
meowOrExit(config.help(parentName, config), {
30+
argv,
31+
description: config.description,
32+
importMeta,
33+
flags: config.flags
34+
})
35+
36+
throw new Error('This error was intentionally left blank')
37+
}

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ type Constants = Omit<
5858
readonly ENV: ENV
5959
readonly DIST_TYPE: 'module-sync' | 'require'
6060
readonly IPC: IPC
61+
readonly IS_PUBLISHED: boolean
6162
readonly LOCK_EXT: '.lock'
6263
readonly MODULE_SYNC: 'module-sync'
6364
readonly NPM_REGISTRY_URL: 'https://registry.npmjs.org'
@@ -96,6 +97,7 @@ const BUN = 'bun'
9697
const CVE_ALERT_PROPS_FIRST_PATCHED_VERSION_IDENTIFIER =
9798
'firstPatchedVersionIdentifier'
9899
const CVE_ALERT_PROPS_VULNERABLE_VERSION_RANGE = 'vulnerableVersionRange'
100+
const IS_PUBLISHED = process.env['SOCKET_IS_PUBLISHED']
99101
const LOCK_EXT = '.lock'
100102
const MODULE_SYNC = 'module-sync'
101103
const NPM_REGISTRY_URL = 'https://registry.npmjs.org'
@@ -187,6 +189,7 @@ const constants = <Constants>createConstantsObject(
187189
// Lazily defined values are initialized as `undefined` to keep their key order.
188190
DIST_TYPE: undefined,
189191
ENV: undefined,
192+
IS_PUBLISHED,
190193
LOCK_EXT,
191194
MODULE_SYNC,
192195
NPM_REGISTRY_URL,

src/handle-crash-with-sentry.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// In a Sentry build, this file will replace the `handle_crash.ts`, see rollup.
2+
//
3+
// This is intended to send a caught-but-unexpected exception to Sentry
4+
// It only works in a special @socketsecurity/cli-with-sentry build.
5+
//
6+
// The regular build will not have the Sentry dependency at all because we
7+
// don't want to give people the idea that we're using it to gather telemetry.
8+
9+
// @ts-ignore
10+
import * as sentry from '@sentry/node'
11+
12+
// Note: Make sure not to exit() explicitly after calling this command. Sentry
13+
// needs some time to finish the fetch() but it doesn't return a promise.
14+
export async function handle(err: unknown) {
15+
if (process.env['SOCKET_CLI_DEBUG'] === '1') {
16+
console.log('Sending to Sentry...')
17+
}
18+
sentry.captureException(err)
19+
if (process.env['SOCKET_CLI_DEBUG'] === '1') {
20+
console.log('Request to Sentry initiated.')
21+
}
22+
23+
// "Sleep" for a second, just in case, hopefully enough time to initiate fetch
24+
return await new Promise(r => setTimeout(r, 1000))
25+
}

src/handle-crash.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// By default this doesn't do anything.
2+
// There's a special cli package in the @socketsecurity scope that is identical
3+
// to this package, except it actually handles error crash reporting.
4+
5+
import { envAsBoolean } from '@socketsecurity/registry/lib/env'
6+
7+
export async function handle(err: unknown) {
8+
if (envAsBoolean(process.env['SOCKET_CLI_DEBUG'])) {
9+
console.error('An unexpected but caught error happened:', err)
10+
}
11+
}

src/initialize-crash-handler.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// This is a placeholder
2+
// In a special @socketsecurity scoped build this will hold crash handler init
3+
// See the rollup config for details.

0 commit comments

Comments
 (0)