Skip to content

Commit 28edbc2

Browse files
authored
Merge pull request #2 from sourcegraph/vo/build-link-url
Vo/build link url and inform user about missing configs. Additional TODOs for next PR in comments.
2 parents 5e39b00 + 3a2a55d commit 28edbc2

8 files changed

+1375
-80
lines changed

mocha.opts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
--recursive
2+
--watch-extensions ts
3+
--timeout 200
4+
src/**/*.test.ts

package.json

+63-5
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,75 @@
2222
"editor/title": [],
2323
"commandPalette": []
2424
},
25-
"configuration": {}
25+
"configuration": {
26+
"title": "Sentry extension settings",
27+
"properties": {
28+
"sentry.organization": {
29+
"description": "Name of the Sentry organization",
30+
"type": "string",
31+
"default": ""
32+
},
33+
"sentry.projects": {
34+
"type": "array",
35+
"items": {
36+
"name": { "type" : "string" },
37+
"type" : "object",
38+
"description": "Sentry project receiving logs",
39+
"projectId": {
40+
"description": "Sentry project id, e.g. 12351512",
41+
"type": "string"
42+
},
43+
"patternProperties" : {
44+
"repoMatch": {
45+
"type" : "string",
46+
"description": "Regex to match repos associated to this Sentry project, e.g. github\\.com/sourcegraph/sourcegraph"
47+
},
48+
"fileMatch": {
49+
"type" : "string",
50+
"description": "Regex to match files associated with this project, e.g. (web|shared)/.*\\.tsx?$"
51+
},
52+
"lineMatch": {
53+
"type" : "string",
54+
"description" : "Regex to match lines associated with this project, e.g. throw new Error\\([\"']([^'\"]+)[\"']\\)"
55+
}
56+
},
57+
"additionalProperties" : {
58+
"contentText": {
59+
"description" : "Text shown in Sentry link, e.g. View sourcegraph/sourcegraph_dot_com errors",
60+
"type" : "string"
61+
},
62+
"hoverMessage": {
63+
"description" : "Hovertext shown on Sentry link, e.g. View errors matching '$1' in Sentry",
64+
"type" : "string"
65+
},
66+
"query": {
67+
"description" : "Query derived from error handling code $1",
68+
"type" : "string"
69+
}
70+
}
71+
}
72+
}
73+
}
74+
}
2675
},
2776
"version": "0.0.0-DEVELOPMENT",
2877
"repository": {
2978
"type": "git",
3079
"url": "https://github.com/sourcegraph/sourcegraph-sentry.git"
3180
},
3281
"license": "Apache-2.0",
33-
"main": "dist/sourcegraph-sentry.js",
82+
"main": "dist/extension.js",
3483
"scripts": {
3584
"tslint": "tslint -p tsconfig.json './src/**/*.ts'",
3685
"typecheck": "tsc -p tsconfig.json",
37-
"build": "parcel build --out-file dist/sourcegraph-sentry.js src/sourcegraph-sentry.ts",
86+
"test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --require ts-node/register --require source-map-support/register --opts mocha.opts",
87+
"cover": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --require ts-node/register --require source-map-support/register --all mocha --opts mocha.opts --timeout 10000",
88+
"build": "parcel build --out-file dist/extension.js src/extension.ts",
3889
"symlink-package": "mkdirp dist && lnfs ./package.json ./dist/package.json",
39-
"serve": "npm run symlink-package && parcel serve --no-hmr --out-file dist/sourcegraph-sentry.js src/sourcegraph-sentry.ts",
90+
"serve": "npm run symlink-package && parcel serve --no-hmr --out-file dist/extension.js src/extension.ts",
4091
"watch:typecheck": "tsc -p tsconfig.json -w",
4192
"watch:build": "tsc -p tsconfig.dist.json -w",
42-
"sourcegraph:prepublish": "npm run build"
93+
"sourcegraph:prepublish": "npm run typecheck && npm run test && npm run build"
4394
},
4495
"browserslist": [
4596
"last 1 Chrome versions",
@@ -51,11 +102,18 @@
51102
"@sourcegraph/prettierrc": "^2.2.0",
52103
"@sourcegraph/tsconfig": "^4.0.0",
53104
"@sourcegraph/tslint-config": "^12.3.1",
105+
"@types/expect": "^1.20.4",
106+
"@types/mocha": "^5.2.6",
107+
"expect": "^24.4.0",
54108
"lnfs-cli": "^2.1.0",
55109
"mkdirp": "^0.5.1",
110+
"mocha": "^6.0.2",
111+
"nyc": "^13.3.0",
56112
"parcel-bundler": "^1.12.0",
57113
"rxjs": "^6.4.0",
114+
"source-map-support": "^0.5.11",
58115
"sourcegraph": "^23.0.0",
116+
"ts-node": "^8.0.3",
59117
"tslint": "^5.13.1",
60118
"typescript": "^3.3.3333"
61119
}

src/extension.ts

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { from } from 'rxjs'
2+
import { filter, switchMap } from 'rxjs/operators'
3+
import * as sourcegraph from 'sourcegraph'
4+
import {
5+
checkMissingConfig,
6+
createDecoration,
7+
getParamsFromUriPath,
8+
isFileMatched,
9+
matchSentryProject,
10+
} from './handler'
11+
import { resolveSettings, Settings } from './settings'
12+
13+
/**
14+
* Params derived from the document's URI.
15+
*/
16+
interface Params {
17+
repo: string | null
18+
file: string | null
19+
}
20+
21+
const DECORATION_TYPE = sourcegraph.app.createDecorationType()
22+
const SETTINGSCONFIG = resolveSettings(sourcegraph.configuration.get<Settings>().value)
23+
const SENTRYORGANIZATION = SETTINGSCONFIG['sentry.organization']
24+
25+
/**
26+
* Common error log patterns to use in case no line matching regexes
27+
* are set in the sentry extension settings.
28+
*/
29+
const COMMON_ERRORLOG_PATTERNS = [
30+
/throw new Error+\(['"]([^'"]+)['"]\)/gi,
31+
/console\.(log|error|info|warn)\(['"`]([^'"`]+)['"`]\)/gi,
32+
/log\.(Printf|Print|Println)\(['"]([^'"]+)['"]\)/gi,
33+
]
34+
35+
// TODO: Refactor to use activeEditor
36+
export function activate(context: sourcegraph.ExtensionContext): void {
37+
sourcegraph.workspace.onDidOpenTextDocument.subscribe(textDocument => {
38+
const params: Params = getParamsFromUriPath(textDocument.uri)
39+
const sentryProjects = SETTINGSCONFIG['sentry.projects']
40+
41+
// Retrieve the Sentry project that this document reports to.
42+
// TODO: Move this outside of activate() and into a separate, testable function.
43+
const sentryProject = sentryProjects && matchSentryProject(params, sentryProjects)
44+
let missingConfigData: string[] = []
45+
let fileMatched: boolean | null
46+
47+
if (sentryProject) {
48+
missingConfigData = checkMissingConfig(sentryProject)
49+
fileMatched = isFileMatched(params, sentryProject)
50+
// Do not decorate lines if the document file format does not match the
51+
// file matching patterns listed in the Sentry extension configurations.
52+
if (fileMatched === false) {
53+
return
54+
}
55+
}
56+
if (sourcegraph.app.activeWindowChanges) {
57+
const activeEditor = from(sourcegraph.app.activeWindowChanges).pipe(
58+
filter((window): window is sourcegraph.Window => window !== undefined),
59+
switchMap(window => window.activeViewComponentChanges),
60+
filter((editor): editor is sourcegraph.CodeEditor => editor !== undefined)
61+
)
62+
// When the active editor changes, publish new decorations.
63+
context.subscriptions.add(
64+
activeEditor.subscribe(editor => {
65+
sentryProject
66+
? decorateEditor(
67+
editor,
68+
missingConfigData,
69+
sentryProject.projectId,
70+
sentryProject.patternProperties.lineMatches
71+
)
72+
: decorateEditor(editor, missingConfigData)
73+
})
74+
)
75+
}
76+
})
77+
}
78+
79+
// TODO: Refactor so that it calls a new function that returns TextDocumentDecoration[],
80+
// and add tests for that new function (kind of like getBlameDecorations())
81+
function decorateEditor(
82+
editor: sourcegraph.CodeEditor,
83+
missingConfigData: string[],
84+
sentryProjectId?: string,
85+
lineMatches?: RegExp[]
86+
): void {
87+
const decorations: sourcegraph.TextDocumentDecoration[] = []
88+
for (const [index, line] of editor.document.text!.split('\n').entries()) {
89+
let match: RegExpExecArray | null
90+
for (let pattern of lineMatches ? lineMatches : COMMON_ERRORLOG_PATTERNS) {
91+
pattern = new RegExp(pattern, 'gi')
92+
do {
93+
match = pattern.exec(line)
94+
if (match) {
95+
decorations.push(decorateLine(index, match, missingConfigData, sentryProjectId))
96+
}
97+
} while (match)
98+
pattern.lastIndex = 0 // reset
99+
}
100+
}
101+
editor.setDecorations(DECORATION_TYPE, decorations)
102+
}
103+
104+
/**
105+
* Decorate a line that matches either the line match pattern from the Sentry extension configurations
106+
* or that matches common error loggin patterns.
107+
* @param index for decoration range
108+
* @param match for a line containing an error query
109+
* @param sentryProjectId Sentry project id retrieved from Sentry extension settings
110+
*/
111+
export function decorateLine(
112+
index: number,
113+
match: RegExpExecArray,
114+
missingConfigData: string[],
115+
sentryProjectId?: string
116+
): sourcegraph.TextDocumentDecoration {
117+
const lineDecorationText = createDecoration(missingConfigData, sentryProjectId)
118+
const decoration: sourcegraph.TextDocumentDecoration = {
119+
range: new sourcegraph.Range(index, 0, index, 0),
120+
isWholeLine: true,
121+
after: {
122+
backgroundColor: missingConfigData.length === 0 ? '#e03e2f' : '#f2736d',
123+
color: 'rgba(255, 255, 255, 0.8)',
124+
contentText: lineDecorationText.content,
125+
hoverMessage: lineDecorationText.hover,
126+
// Depending on the line matching pattern the query m is indexed in position 1 or 2.
127+
// TODO: Specify which capture group should be used through configuration.
128+
// TODO: If !SENTRYORGANIZATION is missing in config, link to $USER/settings and hint
129+
// user to fill it out.
130+
linkURL: !SENTRYORGANIZATION
131+
? ''
132+
: sentryProjectId
133+
? buildUrl(match.length > 2 ? match[2] : match[1], sentryProjectId).toString()
134+
: buildUrl(match.length > 2 ? match[2] : match[1]).toString(),
135+
},
136+
}
137+
return decoration
138+
}
139+
140+
/**
141+
* Build URL to the Sentry issues stream page with the Sentry Org, query and, if available, Sentry project ID.
142+
* @param errorQuery extracted from the error handling code matching the config matching pattern.
143+
* @param sentryProjectId from the associated Sentry project receiving logs from the document's repo.
144+
* @return URL to the Sentry unresolved issues stream page for this kind of query.
145+
*/
146+
// TODO: Use URLSearchParams instead of encodeURIComponent
147+
function buildUrl(errorQuery: string, sentryProjectId?: string): URL {
148+
const url = new URL(
149+
'https://sentry.io/organizations/' +
150+
encodeURIComponent(SENTRYORGANIZATION!) +
151+
'/issues/' +
152+
(sentryProjectId
153+
? '?project=' +
154+
encodeURIComponent(sentryProjectId) +
155+
'&query=is%3Aunresolved+' +
156+
encodeURIComponent(errorQuery) +
157+
'&statsPeriod=14d'
158+
: '')
159+
)
160+
return url
161+
}

src/handler.test.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import expect from 'expect'
2+
import { getParamsFromUriPath, matchSentryProject } from './handler'
3+
import { Settings } from './settings'
4+
5+
describe('getParamsFromUriPath', () => {
6+
it('extracts repo and file params', () =>
7+
expect(
8+
getParamsFromUriPath('git://github.com/sourcegraph/sourcegraph?264...#web/src/e2e/index.e2e.test.tsx')
9+
).toEqual({ repo: 'sourcegraph/sourcegraph', file: '#web/src/e2e/index.e2e.test.tsx' }))
10+
11+
it('return empty repo if host is not GitHub', () =>
12+
expect(getParamsFromUriPath('git://unknownhost.com/sourcegraph/testrepo#http/req/main.go')).toEqual({
13+
repo: null,
14+
file: '#http/req/main.go',
15+
}))
16+
17+
it('return empty file if document has no file format', () =>
18+
expect(getParamsFromUriPath('git://github.com/sourcegraph/sourcegraph/testrepo#formatless')).toEqual({
19+
repo: 'sourcegraph/sourcegraph',
20+
file: null,
21+
}))
22+
})
23+
24+
const projects: Settings['sentry.projects'] = [
25+
{
26+
name: 'Webapp typescript errors',
27+
projectId: '1334031',
28+
patternProperties: {
29+
repoMatch: /sourcegraph\/sourcegraph/,
30+
fileMatches: [/(web|shared)\/.*\.tsx?/, /(dev)\/.*\\.ts?/],
31+
lineMatches: [
32+
/throw new Error+\(['"]([^'"]+)['"]\)/,
33+
/console\.(warn|debug|info|error|log)\(['"`]([^'"`]+)['"`]\)/,
34+
/log\.(Printf|Print|Println)\(['"]([^'"]+)['"]\)/,
35+
],
36+
},
37+
additionalProperties: {
38+
contentText: 'View sourcegraph/sourcegraph_dot_com errors',
39+
hoverMessage: 'View errors matching "$1" in Sentry',
40+
query: '$1',
41+
},
42+
},
43+
]
44+
45+
const params = {
46+
repo: 'sourcegraph/sourcegraph',
47+
file: '#web/src/storm/index.tsx',
48+
}
49+
50+
describe('matchSentryProject', () => {
51+
it('extracts repo and file params', () =>
52+
expect(matchSentryProject(params, projects)).toEqual({
53+
projectId: '1334031',
54+
lineMatches: [
55+
/throw new Error+\(['"]([^'"]+)['"]\)/,
56+
/console\.(warn|debug|info|error|log)\(['"`]([^'"`]+)['"`]\)/,
57+
/log\.(Printf|Print|Println)\(['"]([^'"]+)['"]\)/,
58+
],
59+
}))
60+
})

0 commit comments

Comments
 (0)