Skip to content

Commit

Permalink
[Code] Add a resolveSnippets API to resolve APM stacktrace item to so…
Browse files Browse the repository at this point in the history
…urce code (#43822)

* [Code] Add a resolveSnippets API to resolve APM stacktrace item to source code

* add unit test

* update endpoint
  • Loading branch information
mw-ding committed Aug 29, 2019
1 parent 4f4bb02 commit 50ba977
Show file tree
Hide file tree
Showing 8 changed files with 280 additions and 3 deletions.
15 changes: 15 additions & 0 deletions x-pack/legacy/plugins/code/model/search.ts
Expand Up @@ -109,6 +109,17 @@ export interface SymbolSearchRequest extends SearchRequest {
repoScope?: RepositoryUri[];
}

export interface CodeIntegrationRequest {
repoUri: RepositoryUri;
revision?: string;
}

export interface ResolveSnippetsIntegrationRequest extends CodeIntegrationRequest {
filePath: string;
lineNumStart: number;
lineNumEnd?: number;
}

// The base interface of any kind of search result.
export interface SearchResult {
total: number;
Expand Down Expand Up @@ -176,6 +187,10 @@ export interface CommitSearchResult extends DocumentSearchResult {
commits: CommitSearchResultItem[];
}

export interface IntegrationsSearchResult extends SearchResult {
results?: SearchResultItem[];
}

export interface SourceLocation {
line: number;
column: number;
Expand Down
3 changes: 3 additions & 0 deletions x-pack/legacy/plugins/code/server/indexer/schema/document.ts
Expand Up @@ -42,6 +42,9 @@ export const DocumentSchema = {
type: 'text',
analyzer: 'path_hierarchy_analyzer',
},
keyword: {
type: 'keyword',
},
},
},
content: {
Expand Down
32 changes: 32 additions & 0 deletions x-pack/legacy/plugins/code/server/routes/search.ts
Expand Up @@ -11,12 +11,14 @@ import {
CommitSearchRequest,
DocumentSearchRequest,
RepositorySearchRequest,
ResolveSnippetsIntegrationRequest,
SymbolSearchRequest,
} from '../../model';
import { Logger } from '../log';
import {
CommitSearchClient,
DocumentSearchClient,
IntegrationsSearchClient,
RepositorySearchClient,
SymbolSearchClient,
} from '../search';
Expand Down Expand Up @@ -149,6 +151,36 @@ export function documentSearchRoute(router: CodeServerRouter, log: Logger) {
}
},
});

// Resolve source code snippets base on APM's stacktrace item data including:
// * repoUri: ID of the repository
// * revision: Optional. Revision of the file.
// * filePath: the path of the file.
// * lineNumStart: the start line number of the snippet.
// * lineNumEnd: Optional. The end line number of the snippet.
router.route({
path: '/api/code/integration/snippets',
method: 'GET',
async handler(req: RequestFacade) {
const { repoUri, revision, filePath, lineNum, lineNumEnd } = req.query as RequestQueryFacade;

try {
const integRequest: ResolveSnippetsIntegrationRequest = {
repoUri: repoUri as string,
revision: revision ? (revision as string) : undefined,
filePath: filePath as string,
lineNumStart: lineNum ? parseInt(lineNum as string, 10) : 0,
lineNumEnd: lineNumEnd ? parseInt(lineNumEnd as string, 10) : undefined,
};

const integClient = new IntegrationsSearchClient(new EsClientWithRequest(req), log);
const res = await integClient.resolveSnippets(integRequest);
return res;
} catch (error) {
return Boom.internal(`Invalid request for resovling snippets.`);
}
},
});
}

export function symbolSearchRoute(router: CodeServerRouter, log: Logger) {
Expand Down
Expand Up @@ -13,7 +13,7 @@ import { DocumentSearchClient } from './document_search_client';
let docSearchClient: DocumentSearchClient;
let esClient;

// Setup the entire RepositorySearchClient.
// Setup the entire DocumentSearchClient.
function initSearchClient() {
const log: Logger = (sinon.stub() as any) as Logger;
esClient = initEsClient();
Expand Down
Expand Up @@ -33,7 +33,7 @@ const MAX_HIT_NUMBER = 5;
export class DocumentSearchClient extends AbstractSearchClient {
private HIGHLIGHT_PRE_TAG = '_@-';
private HIGHLIGHT_POST_TAG = '-@_';
private LINE_SEPARATOR = '\n';
protected LINE_SEPARATOR = '\n';

constructor(protected readonly client: EsClient, protected readonly log: Logger) {
super(client, log);
Expand Down Expand Up @@ -307,7 +307,7 @@ export class DocumentSearchClient extends AbstractSearchClient {
};
}

private getSourceContent(hitsContent: SourceHit[], doc: Document) {
protected getSourceContent(hitsContent: SourceHit[], doc: Document) {
const docInLines = doc.content.split(this.LINE_SEPARATOR);
let slicedRanges: LineRange[] = [];
if (hitsContent.length === 0) {
Expand Down
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/code/server/search/index.ts
Expand Up @@ -9,3 +9,4 @@ export * from './document_search_client';
export * from './repository_search_client';
export * from './symbol_search_client';
export * from './repository_object_client';
export * from './integraions_search_client';
@@ -0,0 +1,86 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import {
Document,
IntegrationsSearchResult,
ResolveSnippetsIntegrationRequest,
SearchResultItem,
SourceHit,
} from '../../model';
import { DocumentSearchIndexWithScope } from '../indexer/schema';
import { EsClient } from '../lib/esqueue';
import { Logger } from '../log';
import { DocumentSearchClient } from './document_search_client';

export class IntegrationsSearchClient extends DocumentSearchClient {
constructor(protected readonly client: EsClient, protected readonly log: Logger) {
super(client, log);
}

public async resolveSnippets(
req: ResolveSnippetsIntegrationRequest
): Promise<IntegrationsSearchResult> {
const { repoUri, filePath, lineNumStart, lineNumEnd } = req;
const index = DocumentSearchIndexWithScope([repoUri]);

const rawRes = await this.client.search({
index,
body: {
query: {
term: {
'path.keyword': {
value: filePath,
},
},
},
},
});

const hits: any[] = rawRes.hits.hits;
const results: SearchResultItem[] = hits.map(hit => {
const doc: Document = hit._source;
const { path, language } = doc;

const sourceContent = this.getSnippetContent(doc, lineNumStart, lineNumEnd);
const item: SearchResultItem = {
uri: repoUri,
filePath: path,
language: language!,
hits: 1,
compositeContent: sourceContent,
};
return item;
});
const total = rawRes.hits.total.value;
return {
results,
took: rawRes.took,
total,
};
}

private getSnippetContent(doc: Document, lineNumStart: number, lineNumEnd?: number) {
const hit: SourceHit = {
range: {
startLoc: {
line: lineNumStart - 1,
column: 0,
offset: 0,
},
endLoc: {
line: lineNumEnd === undefined ? lineNumStart - 1 : lineNumEnd - 1,
column: 0,
offset: 0,
},
},
score: 0,
term: '',
};

return super.getSourceContent([hit], doc);
}
}
@@ -0,0 +1,140 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import sinon from 'sinon';

import { AnyObject, EsClient } from '../lib/esqueue';
import { Logger } from '../log';
import { IntegrationsSearchClient } from './integraions_search_client';

let integSearchClient: IntegrationsSearchClient;
let esClient;

// Setup the entire RepositorySearchClient.
function initSearchClient() {
const log: Logger = (sinon.stub() as any) as Logger;
esClient = initEsClient();

integSearchClient = new IntegrationsSearchClient(esClient, log);
}

const mockSearchResults = [
// 1. The first response is a valid DocumentSearchResult with 1 doc
{
took: 1,
hits: {
total: {
value: 1,
},
hits: [
{
_source: {
repoUri: 'github.com/Microsoft/TypeScript-Node-Starter',
path: 'src/types/express-flash.d.ts',
content:
"\n/// <reference types='express' />\n\n// Add RequestValidation Interface on to Express's Request Interface.\ndeclare namespace Express {\n interface Request extends Flash {}\n}\n\ninterface Flash {\n flash(type: string, message: any): void;\n}\n\ndeclare module 'express-flash';\n\n",
language: 'typescript',
qnames: ['express-flash', 'Express', 'Request', 'Flash', 'flash'],
},
highlight: {
content: [
'declare namespace Express {\n interface Request extends Flash {}\n}\n\ninterface Flash {\n flash(type: _@-string-@_',
],
},
},
],
},
},
// 2. The second response is a valid DocumentSearchResult with 0 doc
{
took: 1,
hits: {
total: {
value: 0,
},
hits: [],
},
aggregations: {
repoUri: {
buckets: [],
},
language: {
buckets: [],
},
},
},
];

// Setup the mock EsClient.
function initEsClient(): EsClient {
esClient = {
search: async (_: AnyObject): Promise<any> => {
Promise.resolve({});
},
};
const searchStub = sinon.stub(esClient, 'search');

// Binding the mock search results to the stub.
mockSearchResults.forEach((result, index) => {
searchStub.onCall(index).returns(Promise.resolve(result));
});

return (esClient as any) as EsClient;
}

beforeEach(() => {
initSearchClient();
});

test('Document search', async () => {
// 1. The first response should have 1 result.
const responseWithResult = await integSearchClient.resolveSnippets({
repoUri: 'github.com/Microsoft/TypeScript-Node-Starter',
filePath: 'src/types/express-flash.d.ts',
lineNumStart: 3,
lineNumEnd: 7,
});
expect(responseWithResult).toEqual(
expect.objectContaining({
took: 1,
total: 1,
results: [
{
uri: 'github.com/Microsoft/TypeScript-Node-Starter',
filePath: 'src/types/express-flash.d.ts',
compositeContent: {
// Content is shorted
content:
"\n/// <reference types='express' />\n\n// Add RequestValidation Interface on to Express's Request Interface.\ndeclare namespace Express {\n interface Request extends Flash {}\n}\n\ninterface Flash {\n",
// Line mapping data is populated
lineMapping: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '..'],
// Highlight ranges are calculated
ranges: [
{
endColumn: 1,
endLineNumber: 7,
startColumn: 1,
startLineNumber: 3,
},
],
},
language: 'typescript',
hits: 1,
},
],
})
);

// 2. The first response should have 0 results.
const responseWithEmptyResult = await integSearchClient.resolveSnippets({
repoUri: 'github.com/Microsoft/TypeScript-Node-Starter',
filePath: 'src/types/foo-bar',
lineNumStart: 3,
lineNumEnd: 7,
});
expect(responseWithEmptyResult.results!.length).toEqual(0);
expect(responseWithEmptyResult.total).toEqual(0);
});

0 comments on commit 50ba977

Please sign in to comment.