Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ interface PersistentStorage {
set: (key: string, value: string) => void;
}

type flagCacheConfig = {
Comment thread
Tim-53 marked this conversation as resolved.
Outdated
refetchFlags: boolean,
Comment thread
Tim-53 marked this conversation as resolved.
Outdated
timeToLive: number
Comment thread
Tim-53 marked this conversation as resolved.
Outdated
}

export type AbbyConfig<
FlagName extends string = string,
Tests extends Record<string, ABConfig> = Record<string, ABConfig>
Expand All @@ -54,6 +59,7 @@ export type AbbyConfig<
flags?: Array<FlagName>;
settings?: Settings<F.NoInfer<FlagName>>;
debug?: boolean;
flagCacheConfig?: flagCacheConfig,
};

export class Abby<
Expand All @@ -62,7 +68,7 @@ export class Abby<
Tests extends Record<string, ABConfig>
> {
private log = (...args: any[]) =>
this.config.debug ? console.log(`core.Abby`, ...args) : () => {};
this.config.debug ? console.log(`core.Abby`, ...args) : () => { };
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

do we use console?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We dont have a proper logger integrated right now , so is this used to log info in debugging mode atm.


private testDevtoolOverrides: Map<
keyof Tests,
Expand All @@ -71,6 +77,8 @@ export class Abby<

private flagDevtoolOverrides: Map<FlagName, boolean> = new Map();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

guess this shouldn't be "just booleans" anymore, right?
the fix should be another PR :)


#flagTimeoutMap:Map<string, Date> = new Map;
Comment thread
Tim-53 marked this conversation as resolved.
Outdated

#data: LocalData<FlagName, TestName> = {
tests: {} as any,
flags: {} as any,
Expand Down Expand Up @@ -151,6 +159,8 @@ export class Abby<
return acc;
}, (this.config.tests ?? {}) as any),
flags: data.flags.reduce((acc, { name, isEnabled }) => {
const validUntil = new Date(new Date().getTime() + 1000 * 60 *( this.config.flagCacheConfig?.timeToLive ?? 1)); // flagdefault timeout is 1 minute
Comment thread
Tim-53 marked this conversation as resolved.
Outdated
this.#flagTimeoutMap.set(name, validUntil)
acc[name] = isEnabled;
return acc;
}, {} as Record<string, boolean>),
Expand Down Expand Up @@ -203,6 +213,37 @@ export class Abby<
return this.getProjectData();
}

/**
* Helper function to retrieve the time a flag is valid
* @param key
* @returns
*/
getFeatureFlagTimeout<F extends FlagName>(key: F) {
return this.#flagTimeoutMap.get(key)
}

/**
* Helper function to check if a featureflag should be refetched
* @param key name of the featureflag
* @returns value of flag
*/
getValidFlag<F extends FlagName>(key: F) {
const flagTime = this.#flagTimeoutMap.get(key)
if (!flagTime) return this.#data.flags[key];
const now = new Date();
if (flagTime.getTime() <= now.getTime()) {
Comment thread
Tim-53 marked this conversation as resolved.
Outdated
this.refetchFlags()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this is async.. so without waiting the return down below may be old though 🤷

}
return this.#data.flags[key];
}

/**
* helper function to make testing easier
*/
refetchFlags() {
this.loadProjectData();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
this.loadProjectData();
return this.loadProjectData();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

loadProjectData() has return type void.

}

/**
* Function to get the value of a feature flag. This includes
* the overrides from the dev tools and the local overrides if in development mode
Expand All @@ -213,8 +254,6 @@ export class Abby<
getFeatureFlag<T extends FlagName>(key: T) {
this.log(`getFeatureFlag()`, key);

const storedValue = this.#data.flags[key];

const localOverride = this.flagOverrides?.get(key);

if (localOverride != null) {
Expand All @@ -227,7 +266,7 @@ export class Abby<
* 1. DevTools
* 2. DevOverrides from config
* 3. DevDefault from config
*/
*/
if (process.env.NODE_ENV === "development") {
const devOverride = (this.config.settings?.flags?.devOverrides as any)?.[
key
Expand All @@ -240,6 +279,8 @@ export class Abby<
}
}

const storedValue = this.config.flagCacheConfig?.refetchFlags ? this.getValidFlag(key) : this.#data.flags[key];
Comment thread
Tim-53 marked this conversation as resolved.
Outdated

if (storedValue != null) {
this.log(`getFeatureFlag() => storedValue:`, storedValue);
return storedValue;
Expand Down
138 changes: 138 additions & 0 deletions packages/core/tests/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,146 @@ describe("Abby", () => {

expect(abby.getFeatureFlag("flag1")).toBe(false);
});

it("refetches an expired flag", async () =>{
const date = new Date() //current date
vi.setSystemTime(date)
const abby = new Abby({
projectId: "expired",
flags: ["flag1", "flag2"],
flagCacheConfig: {
refetchFlags: true,
timeToLive: 2
}
});
await abby.loadProjectData()
const expiredDate = new Date(new Date().getTime() + 1000 * 60 * 10) //date in 100 minutes
vi.setSystemTime(expiredDate)
const spy = vi.spyOn(abby, "refetchFlags")

expect(abby.getFeatureFlag("flag1")).toBeTruthy();
expect(abby.getFeatureFlag("flag2")).toBeFalsy();
expect(spy).toBeCalled()
})

it("non expired flag does not get refetched", async () => {
const date = new Date() //current date
vi.setSystemTime(date)
const abby = new Abby({
projectId: "expired",
flags: ["flag1", "flag2"],
flagCacheConfig: {
refetchFlags: true,
timeToLive: 2
}
});

await abby.loadProjectData();

const spy = vi.spyOn(abby, "refetchFlags")

expect(abby.getFeatureFlag("flag1")).toBeTruthy();
expect(abby.getFeatureFlag("flag2")).toBeFalsy();
expect(spy).not.toBeCalled()
})

it("respects the featureFlagCacheConfig refetchFlags value set to false", async () => {
const date = new Date() //current date
vi.setSystemTime(date)
const abby = new Abby({
projectId: "expired",
flags: ["flag1", "flag2"],
flagCacheConfig: {
refetchFlags: false,
timeToLive: 2
}
});

await abby.loadProjectData();

const spy = vi.spyOn(abby, "refetchFlags")

expect(abby.getFeatureFlag("flag1")).toBeTruthy();
expect(abby.getFeatureFlag("flag2")).toBeFalsy();
expect(spy).not.toBeCalled()
})

it("", async () => {
const date = new Date() //current date
vi.setSystemTime(date)
const abby = new Abby({
projectId: "expired",
flags: ["flag1", "flag2"],
flagCacheConfig: {
refetchFlags: true,
timeToLive: 2
}
});

await abby.loadProjectData();

const spy = vi.spyOn(abby, "refetchFlags")

//set date to 5 Minutes in the future
const dateIn3Minutes = new Date(new Date().getTime() + 1000 * 60 * 5);
vi.setSystemTime(dateIn3Minutes)

expect(abby.getFeatureFlag("flag1")).toBeTruthy();
expect(abby.getFeatureFlag("flag2")).toBeFalsy();
expect(spy).toBeCalled()
})

it("respects the featureFlagCacheCOnfig expiration time", async () => {
const date = new Date() //current date
vi.setSystemTime(date)
const abby = new Abby({
projectId: "expired",
flags: ["flag1", "flag2"],
flagCacheConfig: {
refetchFlags: true,
timeToLive: 2
}
});

await abby.loadProjectData();

const spy = vi.spyOn(abby, "refetchFlags")

expect(abby.getFeatureFlag("flag1")).toBeTruthy();
expect(abby.getFeatureFlag("flag2")).toBeFalsy();
expect(spy).not.toBeCalled()

//set date to 5 Minutes in the future
const dateIn3Minutes = new Date(new Date().getTime() + 1000 * 60 * 5);
vi.setSystemTime(dateIn3Minutes)
expect(abby.getFeatureFlag("flag1")).toBeTruthy();
expect(abby.getFeatureFlag("flag2")).toBeFalsy();
expect(spy).toBeCalled()
})

});

it("respects the default behaviour", async () => {
const date = new Date() //current date
vi.setSystemTime(date)
const abby = new Abby({
projectId: "expired",
flags: ["flag1", "flag2"],
});

await abby.loadProjectData();

const spy = vi.spyOn(abby, "refetchFlags")

//set date to 5 Minutes in the future
const dateIn3Minutes = new Date(new Date().getTime() + 1000 * 60 * 5);
vi.setSystemTime(dateIn3Minutes)

expect(abby.getFeatureFlag("flag1")).toBeTruthy();
expect(abby.getFeatureFlag("flag2")).toBeFalsy();
expect(spy).not.toBeCalled()
})

describe("Math helpers", () => {
it("validates weight", () => {
const variants = ["variant1", "variant2"];
Expand Down
30 changes: 30 additions & 0 deletions packages/core/tests/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,34 @@ export const handlers = [
);
}
),
rest.get(
"https://www.tryabby.com/api/dashboard/expired/data",
// `${ABBY_BASE_URL}/api/dashboard/expired/data`,
(req, res, ctx) => {
return res(
ctx.json({
tests: [
{
name: "test",
weights: [1, 1, 1, 1],
},
{
name: "test2",
weights: [1, 0],
},
],
flags: [
{
name: "flag1",
isEnabled: true,
},
{
name: "flag2",
isEnabled: false,
},
],
} as AbbyDataResponse)
);
}
),
];
Loading