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

Make it possible to use node-fetch in HTTPService #35

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
8 changes: 2 additions & 6 deletions apps/web/src/pages/devtools.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { MarketingLayout } from "components/MarketingLayout";
import { NextPageWithLayout } from "./_app";
import { HttpService } from "@tryabby/core";

import { createAbby } from "@tryabby/next";
import abbyDevtools from "@tryabby/devtools";
Expand All @@ -23,7 +22,7 @@ const config = {
flags: ["ToggleMeIfYoureExcited", "showEasterEgg", "showHeading"],
};

const { useAbby, AbbyProvider, useFeatureFlag, withDevtools } =
const { useAbby, AbbyProvider, useFeatureFlag, withDevtools, __abby__ } =
createAbby(config);

export const AbbyProdDevtools = withDevtools(abbyDevtools, {
Expand Down Expand Up @@ -253,10 +252,7 @@ OuterPage.getLayout = (page) => (
);

export const getStaticProps = async () => {
const data = await HttpService.getProjectData({
projectId: config.projectId,
environment: config.currentEnvironment,
});
const data = await __abby__.loadProjectData();
return {
props: { abbyData: data },
};
Expand Down
29 changes: 8 additions & 21 deletions packages/angular/src/lib/abby.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import { Inject, Injectable } from "@angular/core";
import {
AbbyConfig,
ABConfig,
Abby,
AbbyEventType,
HttpService,
} from "@tryabby/core";
import { AbbyConfig, ABConfig, Abby, AbbyEventType, HttpService } from "@tryabby/core";
import { FlagStorageService, TestStorageService } from "./StorageService";
import {
from,
Expand All @@ -24,10 +18,7 @@ import { F } from "ts-toolbelt";
import { Route } from "@angular/router";
import { ABBY_CONFIG_TOKEN } from "./abby.module";

type LocalData<
FlagName extends string = string,
TestName extends string = string
> = {
type LocalData<FlagName extends string = string, TestName extends string = string> = {
tests: Record<
TestName,
ABConfig & {
Expand Down Expand Up @@ -57,9 +48,7 @@ export class AbbyService<
this.config.debug ? console.log(`ng.AbbyService`, ...args) : () => {};
private cookieChanged$ = new Subject<void>();

constructor(
@Inject(ABBY_CONFIG_TOKEN) config: F.Narrow<AbbyConfig<FlagName, Tests>>
) {
constructor(@Inject(ABBY_CONFIG_TOKEN) config: F.Narrow<AbbyConfig<FlagName, Tests>>) {
this.abby = new Abby<FlagName, TestName, Tests>(
config,
{
Expand Down Expand Up @@ -102,9 +91,7 @@ export class AbbyService<
return this.resolveData().pipe(
map((data) => this.abby.getTestVariant(testName)),
tap((variant) => (this.selectedVariants[testName as string] = variant)),
tap((variant) =>
this.log(`getVariant(${testName as string}) =>`, variant)
)
tap((variant) => this.log(`getVariant(${testName as string}) =>`, variant))
);
}

Expand All @@ -123,7 +110,7 @@ export class AbbyService<
},
});

HttpService.sendData({
this.abby.sendData({
url: this.config.apiUrl,
type: AbbyEventType.ACT,
data: {
Expand All @@ -134,9 +121,9 @@ export class AbbyService<
});
}

public getFeatureFlagValue<
F extends NonNullable<ConfigType["flags"]>[number]
>(name: F): Observable<boolean> {
public getFeatureFlagValue<F extends NonNullable<ConfigType["flags"]>[number]>(
name: F
): Observable<boolean> {
this.log(`getFeatureFlagValue(${name})`);

return this.resolveData().pipe(
Expand Down
100 changes: 48 additions & 52 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import {
ABBY_AB_STORAGE_PREFIX,
ABBY_FF_STORAGE_PREFIX,
AbbyDataResponse,
} from "./shared/";
import { HttpService } from "./shared";
import { ABBY_AB_STORAGE_PREFIX, ABBY_FF_STORAGE_PREFIX, AbbyDataResponse } from "./shared/";
import { HttpService, AbbyEvent, AbbyEventType } from "./shared";
import { F } from "ts-toolbelt";
import { getWeightedRandomVariant } from "./mathHelpers";
import { parseCookies } from "./helpers";
import type fetch from "node-fetch";

export * from "./shared/index";

Expand All @@ -24,10 +21,7 @@ type Settings<FlagName extends string = string> = {
};
};

type LocalData<
FlagName extends string = string,
TestName extends string = string
> = {
type LocalData<FlagName extends string = string, TestName extends string = string> = {
tests: Record<
TestName,
ABConfig & {
Expand Down Expand Up @@ -61,13 +55,27 @@ export class Abby<
TestName extends string,
Tests extends Record<string, ABConfig>
> {
constructor(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constructor usally comes between the variables and the functions of a class

private config: F.Narrow<AbbyConfig<FlagName, Tests>>,
private persistantTestStorage?: PersistentStorage,
private persistantFlagStorage?: PersistentStorage,
nodeFetch?: typeof fetch | typeof globalThis.fetch
) {
this.#data.flags = (config.flags ?? []).reduce((acc, flag) => {
acc[flag] = null;
return acc;
}, {} as any);
this.#data.tests = config.tests ?? ({} as any);
this.httpService = new HttpService({ fetch2: nodeFetch });
}

private httpService;

private log = (...args: any[]) =>
this.config.debug ? console.log(`core.Abby`, ...args) : () => {};

private testDevtoolOverrides: Map<
keyof Tests,
Tests[keyof Tests]["variants"][number]
> = new Map();
private testDevtoolOverrides: Map<keyof Tests, Tests[keyof Tests]["variants"][number]> =
new Map();

private flagDevtoolOverrides: Map<FlagName, boolean> = new Map();

Expand All @@ -76,30 +84,13 @@ export class Abby<
flags: {} as any,
};

private listeners = new Set<
(newData: LocalData<FlagName, TestName>) => void
>();
private listeners = new Set<(newData: LocalData<FlagName, TestName>) => void>();

private dataInitialized: Boolean = false;

private flagOverrides = new Map<string, boolean>();

private testOverrides: Map<
keyof Tests,
Tests[keyof Tests]["variants"][number]
> = new Map();

constructor(
private config: F.Narrow<AbbyConfig<FlagName, Tests>>,
private persistantTestStorage?: PersistentStorage,
private persistantFlagStorage?: PersistentStorage
) {
this.#data.flags = (config.flags ?? []).reduce((acc, flag) => {
acc[flag] = null;
return acc;
}, {} as any);
this.#data.tests = config.tests ?? ({} as any);
}
private testOverrides: Map<keyof Tests, Tests[keyof Tests]["variants"][number]> = new Map();

/**
* Helper function to load the projects data from the A/BBY API
Expand All @@ -108,16 +99,17 @@ export class Abby<
async loadProjectData() {
this.log(`loadProjectData()`);

const data = await HttpService.getProjectData({
const data = await this.httpService.getProjectData({
projectId: this.config.projectId,
environment: this.config.currentEnvironment as string,
url: this.config.apiUrl,
});
if (!data) {
this.log(`loadProjectData() => no data`);
return;
return null;
}
this.init(data);
return data;
}

async getProjectDataAsync(): Promise<LocalData<FlagName, TestName>> {
Expand Down Expand Up @@ -166,16 +158,13 @@ export class Abby<
this.log(`getProjectData()`);

return {
tests: Object.entries(this.#data.tests).reduce(
(acc, [testName, test]) => {
acc[testName as TestName] = {
...(test as Tests[TestName]),
selectedVariant: this.getTestVariant(testName as TestName),
};
return acc;
},
this.#data.tests
),
tests: Object.entries(this.#data.tests).reduce((acc, [testName, test]) => {
acc[testName as TestName] = {
...(test as Tests[TestName]),
selectedVariant: this.getTestVariant(testName as TestName),
};
return acc;
}, this.#data.tests),
flags: Object.keys(this.#data.flags).reduce((acc, flagName) => {
acc[flagName as FlagName] = this.getFeatureFlag(flagName as FlagName);
return acc;
Expand Down Expand Up @@ -229,9 +218,7 @@ export class Abby<
* 3. DevDefault from config
*/
if (process.env.NODE_ENV === "development") {
const devOverride = (this.config.settings?.flags?.devOverrides as any)?.[
key
];
const devOverride = (this.config.settings?.flags?.devOverrides as any)?.[key];
if (devOverride != null) {
return devOverride;
}
Expand Down Expand Up @@ -293,10 +280,7 @@ export class Abby<
* @param key the name of the test
* @param override the value to override the test variant with
*/
updateLocalVariant<T extends keyof Tests>(
key: T,
override: Tests[T]["variants"][number]
) {
updateLocalVariant<T extends keyof Tests>(key: T, override: Tests[T]["variants"][number]) {
this.testOverrides.set(key, override);
this.persistantTestStorage?.set(key as string, override);

Expand Down Expand Up @@ -380,4 +364,16 @@ export class Abby<
}
});
}

sendData({
url,
type,
data,
}: {
url?: string;
type: AbbyEventType;
data: Omit<AbbyEvent, "type">;
}) {
this.httpService.sendData({ url, type, data });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.httpService.sendData({ url, type, data });
return this.httpService.sendData({ url, type, data });

}
}
27 changes: 15 additions & 12 deletions packages/core/src/shared/http.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { TypeOf } from "zod";
import { ABBY_BASE_URL } from "./constants";
import type { AbbyEventType, AbbyEvent, AbbyDataResponse } from "./index";
export abstract class HttpService {
static async getProjectData({
import fetch from "node-fetch";
export class HttpService {
Tim-53 marked this conversation as resolved.
Show resolved Hide resolved
constructor({ fetch2 }: { fetch2?: typeof globalThis.fetch | typeof fetch } = {}) {
this.#fetchFunction = fetch2 ?? globalThis.fetch;
}
#fetchFunction: typeof globalThis.fetch | typeof fetch;
Tim-53 marked this conversation as resolved.
Show resolved Hide resolved

async getProjectData({
projectId,
environment,
url,
Expand All @@ -11,24 +18,23 @@ export abstract class HttpService {
url?: string;
}) {
try {
const res = await fetch(
const res = await this.#fetchFunction(
`${url ?? ABBY_BASE_URL}api/dashboard/${projectId}/data${
environment ? `?environment=${environment}` : ""
}`
);

if (!res.ok) return null;

const data = (await res.json()) as AbbyDataResponse;
return data;
} catch (err) {
console.error(
"[ABBY]: failed to load project data, falling back to defaults"
);
console.error("[ABBY]: failed to load project data, falling back to defaults");
return null;
}
}

static sendData({
sendData({
url,
type,
data,
Expand All @@ -37,14 +43,11 @@ export abstract class HttpService {
type: AbbyEventType;
data: Omit<AbbyEvent, "type">;
}) {
if (
typeof window === "undefined" ||
window.location.hostname === "localhost"
) {
if (typeof window === "undefined" || window.location.hostname === "localhost") {
// don't send data in development
return;
}
return fetch(`${url ?? ABBY_BASE_URL}api/data`, {
return this.#fetchFunction(`${url ?? ABBY_BASE_URL}api/data`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down
Loading