Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track time it takes to get a response from DevX API #875

Merged
merged 11 commits into from
Apr 1, 2021
4 changes: 3 additions & 1 deletion src/app/services/actions/permissions-action-creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,14 @@ export function fetchScopes(query?: IQuery): Function {
}
throw (response);
} catch (error) {
const errorMessage = error instanceof Response ?
`ApiError: ${error.status}` : `${error}`;
telemetry.trackException(
new Error(errorTypes.NETWORK_ERROR),
SeverityLevel.Error,
{
ComponentName: componentNames.FETCH_PERMISSIONS_ACTION,
Message: `${error}`
Message: errorMessage
});
return dispatch(fetchScopesError(error));
}
Expand Down
42 changes: 25 additions & 17 deletions src/app/services/actions/query-action-creator-util.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { AuthenticationHandlerOptions, ResponseType } from '@microsoft/microsoft-graph-client';
import { MSALAuthenticationProviderOptions } from
'@microsoft/microsoft-graph-client/lib/src/MSALAuthenticationProviderOptions';
import {
AuthenticationHandlerOptions,
ResponseType,
} from '@microsoft/microsoft-graph-client';
import { MSALAuthenticationProviderOptions } from '@microsoft/microsoft-graph-client/lib/src/MSALAuthenticationProviderOptions';
import { IAction } from '../../../types/action';
import { ContentType } from '../../../types/enums';
import { IQuery } from '../../../types/query-runner';
import { IRequestOptions } from '../../../types/request';
import { GraphClient } from '../graph-client';
import { authProvider } from '../graph-client/msal-agent';
import { DEFAULT_USER_SCOPES } from '../graph-constants';
import { DEFAULT_USER_SCOPES, GRAPH_API_SANDBOX_URL } from '../graph-constants';
import { QUERY_GRAPH_SUCCESS } from '../redux-constants';
import { queryRunningStatus } from './query-loading-action-creators';

Expand All @@ -19,22 +21,21 @@ export function queryResponse(response: object): IAction {
}

export async function anonymousRequest(dispatch: Function, query: IQuery) {

const authToken = '{token:https://graph.microsoft.com/}';
const escapedUrl = encodeURIComponent(query.sampleUrl);
const graphUrl = `https://proxy.apisandbox.msdn.microsoft.com/svc?url=${escapedUrl}`;
const graphUrl = `${GRAPH_API_SANDBOX_URL}/svc?url=${escapedUrl}`;
const sampleHeaders: any = {};
if (query.sampleHeaders && query.sampleHeaders.length > 0) {
query.sampleHeaders.forEach(header => {
query.sampleHeaders.forEach((header) => {
sampleHeaders[header.name] = header.value;
});
}

const headers = {
'Authorization': `Bearer ${authToken}`,
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json',
'SdkVersion': 'GraphExplorer/4.0',
...sampleHeaders
SdkVersion: 'GraphExplorer/4.0',
...sampleHeaders,
};

const options: IRequestOptions = { method: query.selectedVerb, headers };
Expand All @@ -44,16 +45,20 @@ export async function anonymousRequest(dispatch: Function, query: IQuery) {
return fetch(graphUrl, options);
}

export function authenticatedRequest(dispatch: Function, query: IQuery,
scopes: string[] = DEFAULT_USER_SCOPES.split(' ')) {
export function authenticatedRequest(
dispatch: Function,
query: IQuery,
scopes: string[] = DEFAULT_USER_SCOPES.split(' ')
) {
return makeRequest(query.selectedVerb, scopes)(dispatch, query);
}

export function isImageResponse(contentType: string | undefined) {
if (!contentType) { return false; }
if (!contentType) {
return false;
}
return (
contentType === 'application/octet-stream' ||
contentType.includes('image/')
contentType === 'application/octet-stream' || contentType.includes('image/')
);
}

Expand Down Expand Up @@ -98,13 +103,16 @@ const makeRequest = (httpVerb: string, scopes: string[]): Function => {
sampleHeaders.SdkVersion = 'GraphExplorer/4.0';

if (query.sampleHeaders && query.sampleHeaders.length > 0) {
query.sampleHeaders.forEach(header => {
query.sampleHeaders.forEach((header) => {
sampleHeaders[header.name] = header.value;
});
}

const msalAuthOptions = new MSALAuthenticationProviderOptions(scopes);
const middlewareOptions = new AuthenticationHandlerOptions(authProvider, msalAuthOptions);
const middlewareOptions = new AuthenticationHandlerOptions(
authProvider,
msalAuthOptions
);
const client = GraphClient.getInstance()
.api(query.sampleUrl)
.middlewareOptions([middlewareOptions])
Expand Down
4 changes: 3 additions & 1 deletion src/app/services/actions/samples-action-creators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ export function fetchSamples(): Function {
const res = await response.json();
return dispatch(fetchSamplesSuccess(res.sampleQueries));
} catch (error) {
const errorMessage = error instanceof Response ?
`ApiError: ${error.status}` : `${error}`;
telemetry.trackException(
new Error(errorTypes.NETWORK_ERROR),
SeverityLevel.Error,
{
ComponentName: componentNames.FETCH_SAMPLES_ACTION,
Message: `${error}`
Message: errorMessage
});
return dispatch(fetchSamplesError({ error }));
}
Expand Down
20 changes: 12 additions & 8 deletions src/app/services/actions/snippet-action-creator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SeverityLevel } from '@microsoft/applicationinsights-web';
import { componentNames, errorTypes, telemetry } from '../../../telemetry';
import { IAction } from '../../../types/action';
import { IRequestOptions } from '../../../types/request';
import { sanitizeQueryUrl } from '../../utils/query-url-sanitization';
import { parseSampleUrl } from '../../utils/sample-url-generation';
import { GET_SNIPPET_ERROR, GET_SNIPPET_PENDING, GET_SNIPPET_SUCCESS } from '../redux-constants';
Expand Down Expand Up @@ -41,31 +42,34 @@ export function getSnippet(language: string): Function {

dispatch(getSnippetPending());

const method = 'POST';
const headers = {
'Content-Type': 'application/http'
};
// tslint:disable-next-line: max-line-length
const body = `${sampleQuery.selectedVerb} /${queryVersion}/${requestUrl + search} HTTP/1.1\r\nHost: graph.microsoft.com\r\nContent-Type: application/json\r\n\r\n${JSON.stringify(sampleQuery.sampleBody)}`;
const options: IRequestOptions = { method, headers, body };
const obj: any = {};
const response = await fetch(snippetsUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/http'
},
body
});

const response = await fetch(snippetsUrl, options);
if (response.ok) {
const result = await response.text();
obj[language] = result;
return dispatch(getSnippetSuccess(obj));
}
throw (response);
} catch (error) {
const errorMessage = error instanceof Response ?
`ApiError: ${error.status}` : `${error}`;
const sanitizedUrl = sanitizeQueryUrl(sampleQuery.sampleUrl);
telemetry.trackException(
new Error(errorTypes.NETWORK_ERROR),
SeverityLevel.Error,
{
ComponentName: componentNames.GET_SNIPPET_ACTION,
QuerySignature: `${sampleQuery.selectedVerb} ${sanitizedUrl}`,
Message: `${error}`
Language: language,
Message: errorMessage
}
);
return dispatch(getSnippetError(error));
Expand Down
1 change: 1 addition & 0 deletions src/app/services/graph-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export const ADMIN_AUTH_URL = 'https://signIn.microsoftonline.com/common/adminco
export const TOKEN_URL = 'https://signIn.microsoftonline.com/common/oauth2/v2.0/token';
export const DEFAULT_USER_SCOPES = 'openid profile User.Read';
export const DEVX_API_URL = 'https://graphexplorerapi.azurewebsites.net';
export const GRAPH_API_SANDBOX_URL = 'https://proxy.apisandbox.msdn.microsoft.com';
26 changes: 19 additions & 7 deletions src/app/utils/open-api-parser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import {
IOpenApiParseContent, IParameters, IParameterValue,
IParsedOpenApiResponse, IPathValue, IQueryParameter
IOpenApiParseContent,
IParameters,
IParameterValue,
IParsedOpenApiResponse,
IPathValue,
IQueryParameter,
} from '../../types/open-api';

export function parseOpenApiResponse(params: IOpenApiParseContent): IParsedOpenApiResponse {
const { response: { paths }, url } = params;
export function parseOpenApiResponse(
params: IOpenApiParseContent
): IParsedOpenApiResponse {
const {
response: { paths },
url,
} = params;

try {
const parameters: IParameters[] = [];
Expand All @@ -16,7 +25,7 @@ export function parseOpenApiResponse(params: IOpenApiParseContent): IParsedOpenA
parameters.push({
verb,
values: getVerbParameterValues(pathValues[`${verb}`]),
links: getLinkValues(pathValues[`${verb}`])
links: getLinkValues(pathValues[`${verb}`]),
});
});

Expand All @@ -35,7 +44,10 @@ function getVerbParameterValues(values: IPathValue): IParameterValue[] {
if (parameter.name && parameter.in === 'query') {
parameterValues.push({
name: parameter.name,
items: (parameter.schema && parameter.schema.items) ? parameter.schema.items.enum : []
items:
parameter.schema && parameter.schema.items
? parameter.schema.items.enum
: [],
});
}
});
Expand All @@ -52,4 +64,4 @@ function getLinkValues(values: IPathValue): string[] {
}
}
return [];
}
}
73 changes: 55 additions & 18 deletions src/app/utils/query-url-sanitization.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/* eslint-disable no-useless-escape */
import { GRAPH_URL } from '../services/graph-constants';
import { isAllAlpha, sanitizeQueryParameter } from './query-parameter-sanitization';
import {
isAllAlpha,
sanitizeQueryParameter,
} from './query-parameter-sanitization';
import { parseSampleUrl } from './sample-url-generation';

// Matches strings with deprecation identifier
Expand Down Expand Up @@ -33,25 +36,48 @@ export function isFunctionCall(segment: string): boolean {
}

/**
* @param url - query url to be sanitized e.g. https://graph.microsoft.com/v1.0/users/{user-id}
* Sanitize Graph API Sandbox URL used when a user is not signed in
* @param url - URL to be sanitized
*/
export function sanitizeGraphAPISandboxUrl(url: string): string {
MIchaelMainer marked this conversation as resolved.
Show resolved Hide resolved
const urlObject = new URL(url);
const queryParams = urlObject.searchParams;
// This query parameter holds Graph query URL
const queryUrl = queryParams.get('url');
if (queryUrl) {
queryParams.set('url', sanitizeQueryUrl(queryUrl));
}
return urlObject.toString();
}

/**
* @param url - query URL to be sanitized e.g. https://graph.microsoft.com/v1.0/users/{user-id}
*/
export function sanitizeQueryUrl(url: string): string {
url = decodeURIComponent(url);

const { search, queryVersion, requestUrl } = parseSampleUrl(url);
const queryString: string = search ? `?${sanitizeQueryParameters(search)}` : '';
const queryString: string = search
? `?${sanitizeQueryParameters(search)}`
: '';

// Sanitize item path specified in query url
let resourceUrl = requestUrl;
if (resourceUrl) {
resourceUrl = requestUrl.replace(ITEM_PATH_REGEX, (match: string): string => {
return `${match.substring(0, match.indexOf(':'))}:<value>`;
});
resourceUrl = requestUrl.replace(
ITEM_PATH_REGEX,
(match: string): string => {
return `${match.substring(0, match.indexOf(':'))}:<value>`;
}
);

// Split requestUrl into segments that can be sanitized individually
const urlSegments = resourceUrl.split('/');
urlSegments.forEach((segment, index) => {
const sanitizedSegment = sanitizePathSegment(urlSegments[index - 1], segment);
const sanitizedSegment = sanitizePathSegment(
urlSegments[index - 1],
segment
);
resourceUrl = resourceUrl.replace(segment, sanitizedSegment);
});
}
Expand All @@ -69,23 +95,34 @@ export function sanitizeQueryUrl(url: string): string {
function sanitizePathSegment(previousSegment: string, segment: string): string {
const segmentsToIgnore = ['$value', '$count', '$ref'];

if (isAllAlpha(segment) || isDeprecation(segment) || SANITIZED_ITEM_PATH_REGEX.test(segment)
|| segmentsToIgnore.includes(segment.toLowerCase()) || ENTITY_NAME_REGEX.test(segment)) {
if (
isAllAlpha(segment) ||
isDeprecation(segment) ||
SANITIZED_ITEM_PATH_REGEX.test(segment) ||
segmentsToIgnore.includes(segment.toLowerCase()) ||
ENTITY_NAME_REGEX.test(segment)
) {
return segment;
}

// Check if segment is in this form: users('<some-id>|<UPN>') and tranform to users(<value>)
if (isFunctionCall(segment)) {
const openingBracketIndex = segment.indexOf('(');
const textWithinBrackets = segment.substr(openingBracketIndex + 1, segment.length - 2);
const sanitizedText = textWithinBrackets.split(',').map(text => {
if (text.includes('=')) {
let key = text.split('=')[0];
key = !isAllAlpha(key) ? '<key>' : key;
return `${key}=<value>`;
}
return '<value>';
}).join(',');
const textWithinBrackets = segment.substr(
openingBracketIndex + 1,
segment.length - 2
);
const sanitizedText = textWithinBrackets
.split(',')
.map((text) => {
if (text.includes('=')) {
let key = text.split('=')[0];
key = !isAllAlpha(key) ? '<key>' : key;
return `${key}=<value>`;
}
return '<value>';
})
.join(',');
return `${segment.substring(0, openingBracketIndex)}(${sanitizedText})`;
}

Expand Down
4 changes: 2 additions & 2 deletions src/app/views/query-response/snippets/snippets-helper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function Snippet(props: ISnippetProps) {
iconProps={copyIcon}
onClick={async () => {
genericCopy(snippet);
trackCopyEvent(sampleQuery, language);
trackSnippetCopyEvent(sampleQuery, language);
}}
/>
<Monaco
Expand All @@ -90,7 +90,7 @@ function Snippet(props: ISnippetProps) {
);
}

function trackCopyEvent(query: IQuery, language: string) {
function trackSnippetCopyEvent(query: IQuery, language: string) {
const sanitizedUrl = sanitizeQueryUrl(query.sampleUrl);
telemetry.trackEvent(eventTypes.BUTTON_CLICK_EVENT,
{
Expand Down
4 changes: 2 additions & 2 deletions src/app/views/query-runner/request/auth/Auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function Auth(props: any) {

const handleCopy = async () => {
await genericCopy(accessToken!);
trackCopyEvent();
trackTokenCopyEvent();
};

useEffect(() => {
Expand Down Expand Up @@ -70,7 +70,7 @@ export function Auth(props: any) {
</div>);
}

function trackCopyEvent() {
function trackTokenCopyEvent() {
telemetry.trackEvent(
eventTypes.BUTTON_CLICK_EVENT,
{
Expand Down
3 changes: 2 additions & 1 deletion src/telemetry/ITelemetry.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { SeverityLevel } from '@microsoft/applicationinsights-web';
import { ComponentType } from 'react';
import { IQuery } from '../types/query-runner';
import { IRequestOptions } from '../types/request';

export default interface ITelemetry {
initialize(): void;
trackEvent(name: string, properties: {}): void;
trackReactComponent(Component: ComponentType, componentName?: string): ComponentType;
trackTabClickEvent(tabKey: string, sampleQuery: IQuery): void;
trackTabClickEvent(tabKey: string, sampleQuery?: IQuery): void;
trackLinkClickEvent(url: string, componentName: string): void;
trackException(error: Error, severityLevel: SeverityLevel, properties: {}): void;
}
Loading