Skip to content

Commit

Permalink
Add help links and remove deprecated API (#417)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcospassos authored Aug 25, 2024
1 parent d9972b2 commit da5ba76
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 196 deletions.
37 changes: 6 additions & 31 deletions src/channel/httpBeaconChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Logger, NullLogger} from '../logging';
import {CidAssigner} from '../cid';
import {formatMessage} from '../error';
import {CLIENT_LIBRARY} from '../constants';
import {Help} from '../help';

export type Configuration = {
appId: string,
Expand Down Expand Up @@ -72,38 +73,12 @@ export class HttpBeaconChannel implements DuplexChannel<string, Envelope<string,
);

const isRetryable = HttpBeaconChannel.isRetryable(problem.status);
const help = Help.forStatusCode(problem.status);

switch (response.status) {
case 401:
this.logger.error(
'The request was not authorized, most likely due to invalid credentials. '
+ 'For help, see https://croct.help/sdk/js/invalid-credentials',
);

break;

case 403:
this.logger.error(
'The origin of the request is not allowed in your application settings. '
+ 'For help, see https://croct.help/sdk/js/cors',
);

break;

case 423:
this.logger.error(
'The application has exceeded the monthly active users (MAU) quota. '
+ 'For help, see https://croct.help/sdk/js/mau-exceeded',
);

break;

default:
if (!isRetryable) {
this.logger.error(`Beacon rejected with non-retryable status: ${problem.title}`);
}

break;
if (help !== undefined) {
this.logger.error(help);
} else if (!isRetryable) {
this.logger.error(`Beacon rejected with non-retryable status: ${problem.title}`);
}

return Promise.reject(
Expand Down
38 changes: 18 additions & 20 deletions src/contentFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {BASE_ENDPOINT_URL, CLIENT_LIBRARY} from './constants';
import {formatMessage} from './error';
import {Logger, NullLogger} from './logging';
import type {ApiKey} from './apiKey';
import {Help} from './help';

export type ErrorResponse = {
type: string,
Expand Down Expand Up @@ -54,10 +55,6 @@ export type DynamicContentOptions = BasicOptions & {
clientId?: string,
clientIp?: string,
clientAgent?: string,
/**
* @deprecated Use `clientAgent` instead. This option will be removed in future releases.
*/
userAgent?: string,
userToken?: Token|string,
previewToken?: Token|string,
context?: EvaluationContext,
Expand Down Expand Up @@ -140,6 +137,8 @@ export class ContentFetcher {

abortController.abort();

this.logHelp(response.status);

reject(new ContentError(response));
},
options.timeout,
Expand All @@ -151,10 +150,12 @@ export class ContentFetcher {
response => response.json()
.then(body => {
if (response.ok) {
resolve(body);
} else {
reject(new ContentError(body));
return resolve(body);
}

this.logHelp(response.status);

reject(new ContentError(body));
})
.catch(error => {
if (!response.ok) {
Expand Down Expand Up @@ -211,15 +212,6 @@ export class ContentFetcher {
const dynamic = ContentFetcher.isDynamicContent(options);

if (dynamic) {
if (options.userAgent !== undefined) {
this.logger.warn(
'The `userAgent` option is deprecated and '
+ 'will be removed in future releases. '
+ 'Please update the part of your code calling the `fetch` method '
+ 'to use the `clientAgent` option instead.',
);
}

if (options.clientId !== undefined) {
headers['X-Client-Id'] = options.clientId;
}
Expand All @@ -232,10 +224,8 @@ export class ContentFetcher {
headers['X-Token'] = options.userToken.toString();
}

const clientAgent = options.clientAgent ?? options.userAgent;

if (clientAgent !== undefined) {
headers['X-Client-Agent'] = clientAgent;
if (options.clientAgent !== undefined) {
headers['X-Client-Agent'] = options.clientAgent;
}

if (options.context !== undefined) {
Expand Down Expand Up @@ -264,6 +254,14 @@ export class ContentFetcher {
});
}

private logHelp(statusCode: number): void {
const help = Help.forStatusCode(statusCode);

if (help !== undefined) {
this.logger.error(help);
}
}

private static isDynamicContent(options: FetchOptions): options is DynamicContentOptions {
return options.static !== true;
}
Expand Down
76 changes: 37 additions & 39 deletions src/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {formatMessage} from './error';
import {getLength, getLocation, Location} from './sourceLocation';
import {Logger, NullLogger} from './logging';
import type {ApiKey} from './apiKey';
import {Help} from './help';

export type Campaign = {
name?: string,
Expand Down Expand Up @@ -37,10 +38,6 @@ export type EvaluationOptions = {
clientId?: string,
clientIp?: string,
clientAgent?: string,
/**
* @deprecated Use `clientAgent` instead. This option will be removed in future releases.
*/
userAgent?: string,
userToken?: Token|string,
timeout?: number,
context?: EvaluationContext,
Expand Down Expand Up @@ -154,12 +151,12 @@ export class Evaluator {
return Promise.reject(new QueryError(response));
}

const body: JsonObject = {
const payload: JsonObject = {
query: query,
};

if (options.context !== undefined) {
body.context = options.context;
payload.context = options.context;
}

return new Promise((resolve, reject) => {
Expand All @@ -177,33 +174,37 @@ export class Evaluator {

abortController.abort();

this.logHelp(response.status);

reject(new EvaluationError(response));
},
options.timeout,
);
}

const promise = this.fetch(body, abortController.signal, options);
const promise = this.fetch(payload, abortController.signal, options);

promise.then(
response => response.json()
.then(data => {
.then(body => {
if (response.ok) {
return resolve(data);
return resolve(body);
}

const errorResponse: ErrorResponse = data;
this.logHelp(response.status);

const problem: ErrorResponse = body;

switch (errorResponse.type) {
switch (problem.type) {
case EvaluationErrorType.INVALID_QUERY:
case EvaluationErrorType.EVALUATION_FAILED:
case EvaluationErrorType.TOO_COMPLEX_QUERY:
reject(new QueryError(errorResponse as QueryErrorResponse));
reject(new QueryError(problem as QueryErrorResponse));

break;

default:
reject(new EvaluationError(errorResponse));
reject(new EvaluationError(problem));

break;
}
Expand All @@ -215,37 +216,26 @@ export class Evaluator {

throw error;
}),
)
.catch(
error => {
if (!abortController.signal.aborted) {
reject(
new EvaluationError({
title: formatMessage(error),
type: EvaluationErrorType.UNEXPECTED_ERROR,
detail: 'Please try again or contact Croct support if the error persists.',
status: 500, // Internal Server Error
}),
);
}
},
);
).catch(
error => {
if (!abortController.signal.aborted) {
reject(
new EvaluationError({
title: formatMessage(error),
type: EvaluationErrorType.UNEXPECTED_ERROR,
detail: 'Please try again or contact Croct support if the error persists.',
status: 500, // Internal Server Error
}),
);
}
},
);
});
}

private fetch(body: JsonObject, signal: AbortSignal, options: EvaluationOptions): Promise<Response> {
const {appId, apiKey} = this.configuration;
const {clientId, clientIp, userToken} = options;
const clientAgent = options.clientAgent ?? options.userAgent;

if (options.userAgent !== undefined) {
this.logger.warn(
'The `userAgent` option is deprecated and '
+ 'will be removed in future releases. '
+ 'Please update the part of your code calling the `evaluate` method '
+ 'to use the `clientAgent` option instead.',
);
}
const {clientId, clientIp, userToken, clientAgent} = options;

const headers: Record<string, string> = {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -292,6 +282,14 @@ export class Evaluator {
});
}

private logHelp(statusCode: number): void {
const help = Help.forStatusCode(statusCode);

if (help !== undefined) {
this.logger.error(help);
}
}

public toJSON(): never {
// Prevent sensitive configuration from being serialized
throw new Error('Unserializable value.');
Expand Down
24 changes: 24 additions & 0 deletions src/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export namespace Help {
export function forStatusCode(statusCode: number): string|undefined {
switch (statusCode) {
case 401:
return 'The request was not authorized, most likely due to invalid credentials. '
+ 'For help, see https://croct.help/sdk/js/invalid-credentials';

case 403:
return 'The origin of the request is not allowed in your application settings. '
+ 'For help, see https://croct.help/sdk/js/cors';

case 408:
return 'The request timed out. '
+ 'For help, see https://croct.help/sdk/js/timeout';

case 423:
return 'The application has exceeded the monthly active users (MAU) quota. '
+ 'For help, see https://croct.help/sdk/js/mau-exceeded';

default:
return undefined;
}
}
}
14 changes: 6 additions & 8 deletions test/channel/httpBeaconChannel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {FixedAssigner} from '../../src/cid';
import {Beacon} from '../../src/trackingEvents';
import {Token} from '../../src/token';
import {CLIENT_LIBRARY} from '../../src/constants';
import {Help} from '../../src/help';

describe('An HTTP beacon channel', () => {
beforeEach(() => {
Expand Down Expand Up @@ -170,7 +171,7 @@ describe('An HTTP beacon channel', () => {
type NonRetryableErrorScenario = {
status: number,
title: string,
log: string,
log?: string,
};

it.each<NonRetryableErrorScenario>([
Expand All @@ -182,8 +183,6 @@ describe('An HTTP beacon channel', () => {
{
status: 401,
title: 'Unauthorized request',
log: 'The request was not authorized, most likely due to invalid credentials. '
+ 'For help, see https://croct.help/sdk/js/invalid-credentials',
},
{
status: 402,
Expand All @@ -193,17 +192,16 @@ describe('An HTTP beacon channel', () => {
{
status: 403,
title: 'Unallowed origin',
log: 'The origin of the request is not allowed in your application settings. '
+ 'For help, see https://croct.help/sdk/js/cors',
},
{
status: 423,
title: 'Quota exceeded',
log: 'The application has exceeded the monthly active users (MAU) quota. '
+ 'For help, see https://croct.help/sdk/js/mau-exceeded',
},
])('should report a non-retryable error if the response status is $status', async scenario => {
const {status, title, log} = scenario;
const {status, title} = scenario;
const log = scenario.log ?? Help.forStatusCode(status);

expect(log).toBeDefined();

fetchMock.mock(endpointUrl, {
status: status,
Expand Down
Loading

0 comments on commit da5ba76

Please sign in to comment.