Skip to content

Commit

Permalink
feat: api error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
VojtechVidra committed Dec 22, 2023
1 parent 6f66a9d commit 08a164f
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 17 deletions.
4 changes: 4 additions & 0 deletions src/cloud/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const f = <T>(
}).then(async (res) => {
const text = await res.text();
const resBody = (text ? JSON.parse(text) : undefined) as T;
if (!res.ok) {
const errorBody = resBody as undefined | { message?: string };
throw new Error(errorBody?.message ?? res.statusText);
}
return resBody;
});

Expand Down
48 changes: 36 additions & 12 deletions src/cloud/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,25 @@ import { startFlow } from "../public-methods";
import { init as flowsInit } from "../init";
import type { FlowsCloudOptions } from "../types";
import { hash } from "../utils";
import { log } from "../log";
import { validateFlowsOptions, validateCloudFlowsOptions } from "../validation";
import { api } from "./api";

export * from "../index";

export const init = async (options: FlowsCloudOptions): Promise<void> => {
const cloudValidationResult = validateCloudFlowsOptions(options);
const coreValidationResult = validateFlowsOptions(options);
const validationResult = !cloudValidationResult.valid
? cloudValidationResult
: coreValidationResult;
if (validationResult.error)
log.error(
`Error validating options at: options.${validationResult.error.path.join(".")} with value:`,
validationResult.error.value,
);
if (!validationResult.valid) return;

const apiUrl = options.customApiUrl ?? "https://api.flows-cloud.com";

const flows = await api(apiUrl)
Expand All @@ -15,8 +29,11 @@ export const init = async (options: FlowsCloudOptions): Promise<void> => {
userHash: options.userId ? await hash(options.userId) : undefined,
})
.catch((err) => {
// eslint-disable-next-line no-console -- useful for debugging
console.error(`Failed to fetch flows from cloud with projectId: ${options.projectId}`, err);
log.error(
`Failed to load data from cloud for %c${options.projectId}%c, make sure projectId is correct and your project domains are correctly set up.\n`,
"font-weight:bold",
err,
);
});

flowsInit({
Expand All @@ -28,16 +45,20 @@ export const init = async (options: FlowsCloudOptions): Promise<void> => {
const { flowHash, flowId, type, projectId, stepIndex, stepHash, userId } = event;

void (async () =>
api(apiUrl).sendEvent({
eventTime: new Date().toISOString(),
flowHash,
flowId,
projectId,
type,
stepHash,
stepIndex: stepIndex?.toString(),
userHash: userId ? await hash(userId) : undefined,
}))();
api(apiUrl)
.sendEvent({
eventTime: new Date().toISOString(),
flowHash,
flowId,
projectId,
type,
stepHash,
stepIndex: stepIndex?.toString(),
userHash: userId ? await hash(userId) : undefined,
})
.catch((err) => {
log.error("Failed to send event to cloud\n", err);
}))();
},
onLocationChange: (pathname, context) => {
const params = new URLSearchParams(pathname.split("?")[1] ?? "");
Expand All @@ -50,6 +71,9 @@ export const init = async (options: FlowsCloudOptions): Promise<void> => {
.then((flow) => {
context.addFlowData({ ...flow, draft: true });
startFlow(flow.id, { startDraft: true });
})
.catch((err) => {
log.error("Failed to load preview flow\n", err);
});
},
});
Expand Down
4 changes: 2 additions & 2 deletions src/init.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FlowsContext } from "./flows-context";
import { changeWaitMatch, formWaitMatch, locationMatch } from "./form";
import { addHandlers } from "./handlers";
import { log } from "./log";
import { endFlow, startFlow } from "./public-methods";
import type { FlowsInitOptions } from "./types";
import { validateFlowsOptions } from "./validation";
Expand All @@ -15,8 +16,7 @@ export const init = (options: FlowsInitOptions): void => {
const _init = (options: FlowsInitOptions): void => {
const validationResult = validateFlowsOptions(options);
if (validationResult.error)
// eslint-disable-next-line no-console -- useful for user debugging
console.error(
log.error(
`Error validating options at: options.${validationResult.error.path.join(".")} with value:`,
validationResult.error.value,
);
Expand Down
11 changes: 11 additions & 0 deletions src/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const log = {
error: (message: string, ...args: unknown[]): void => {
// eslint-disable-next-line no-console -- ignore
console.error(
`%cFlows%c ${message}`,
"color:#fff;background:#ec6441;padding:2px 4px;border-radius:4px",
"",
...args,
);
},
};
4 changes: 2 additions & 2 deletions src/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { FlowModalStep, FlowTooltipStep, Placement } from "./types";
import type { FlowState } from "./flow-state";
import { isModalStep, isTooltipStep } from "./utils";
import { Icons } from "./icons";
import { log } from "./log";

const DISTANCE = 4;
const ARROW_SIZE = 6;
Expand Down Expand Up @@ -60,8 +61,7 @@ const updateTooltip = ({
}
})
.catch((err) => {
// eslint-disable-next-line no-console -- Error log
console.warn("Error computing position", err);
log.error("Error computing position\n", err);
});
};

Expand Down
7 changes: 6 additions & 1 deletion src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
CompareValue,
UserPropertyMatchGroup,
FlowsInitOptions,
FlowsCloudOptions,
} from "./types";

const WaitOptionsStruct: Describe<WaitStepOptions> = object({
Expand Down Expand Up @@ -134,7 +135,7 @@ const FlowStruct: Describe<Flow> = object({
),
});

const OptionsStruct: Describe<FlowsInitOptions> = object({
const OptionsStruct: Describe<FlowsInitOptions> = type({
flows: optional(array(FlowStruct)),
onNextStep: optional(func()) as Describe<FlowsInitOptions["onNextStep"]>,
onPrevStep: optional(func()) as Describe<FlowsInitOptions["onPrevStep"]>,
Expand All @@ -148,6 +149,9 @@ const OptionsStruct: Describe<FlowsInitOptions> = object({
customApiUrl: optional(string()),
onLocationChange: optional(func()) as Describe<FlowsInitOptions["onLocationChange"]>,
});
const CloudOptionsStruct: Describe<Omit<FlowsCloudOptions, keyof FlowsInitOptions>> = type({
customApiUrl: optional(string()),
});

const validateStruct =
<T>(struct: Describe<T>) =>
Expand All @@ -164,6 +168,7 @@ const validateStruct =
export const isValidFlowsOptions = (options: unknown): options is FlowsOptions =>
OptionsStruct.is(options);
export const validateFlowsOptions = validateStruct(OptionsStruct);
export const validateCloudFlowsOptions = validateStruct(CloudOptionsStruct);

export const isValidFlow = (flow: unknown): flow is Flow => FlowStruct.is(flow);
export const validateFlow = validateStruct(FlowStruct);

0 comments on commit 08a164f

Please sign in to comment.