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

feat: Add Fastly Edge SDK #723

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
00ecc9a
Start on Fastly SDK
ldhenry Dec 13, 2024
809e1cd
polyfill node:events in fastly sdk instead of example
ldhenry Dec 18, 2024
0914d82
Add tests and cleanup
ldhenry Jan 3, 2025
be7aa07
Fix test and revert change to sdk-server-edge
ldhenry Jan 3, 2025
40f05ab
Add CI
ldhenry Jan 3, 2025
77485da
Add crypto-js types
ldhenry Jan 3, 2025
aff265d
fix typedoc
ldhenry Jan 3, 2025
7d47d12
Add fastly to release-please
ldhenry Jan 3, 2025
cfddcdc
Prep for alpha release
ldhenry Jan 3, 2025
6079a15
Merge main
ldhenry Jan 3, 2025
f9b9561
Prep for 0.0.1 release
ldhenry Jan 3, 2025
7fd2d96
Update homepage url
ldhenry Jan 3, 2025
ce436a2
merge main
ldhenry Feb 10, 2025
3717d13
Add jsr.json and pin version in example
ldhenry Feb 10, 2025
e96311c
Change name in platformInfo
ldhenry Feb 10, 2025
9c7c966
lowercase in .sdk_metadata
ldhenry Feb 10, 2025
91819c9
Add eslint config
ldhenry Feb 10, 2025
584eef5
Add remaining eslint configs
ldhenry Feb 10, 2025
08cb672
Add more eslint helpers
ldhenry Feb 10, 2025
53891b1
Add @trivago/prettier-plugin-sort-imports
ldhenry Feb 10, 2025
1fb5431
Downgrade @typescript-eslint version
ldhenry Feb 10, 2025
9c6d3dd
Add ts-jest
ldhenry Feb 10, 2025
ed6764e
Add @types/jest
ldhenry Feb 10, 2025
4282427
Bump dependencies
ldhenry Feb 10, 2025
a422de4
Bump example version
ldhenry Feb 10, 2025
6271e7c
Clean up LDClient implementation and add eventsUri option
ldhenry Feb 10, 2025
dff0e69
Bump @fastly/cli
ldhenry Feb 10, 2025
7c1bc03
Use release-please to set version
ldhenry Feb 10, 2025
2eef68d
Fix test
ldhenry Feb 10, 2025
b41b07b
Remove the need for ts-ignore
ldhenry Feb 10, 2025
dda0ce5
Add newline to tsconfig.json
ldhenry Feb 10, 2025
389c52e
Swap photos with PD versions
ldhenry Feb 19, 2025
a73b761
Reformat photo credits
ldhenry Feb 19, 2025
0111854
merge main
ldhenry Feb 19, 2025
3a67b2b
Add link to free Fastly account
ldhenry Feb 19, 2025
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
24 changes: 24 additions & 0 deletions .github/workflows/fastly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: sdk/fastly

on:
push:
branches: [main, 'feat/**']
paths-ignore:
- '**.md' #Do not need to run CI for markdown changes.
pull_request:
branches: [main, 'feat/**']
paths-ignore:
- '**.md'

jobs:
build-test-fastly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- id: shared
name: Shared CI Steps
uses: ./actions/ci
with:
workspace_name: '@launchdarkly/fastly-server-sdk'
workspace_path: packages/sdk/fastly
1 change: 1 addition & 0 deletions .github/workflows/manual-publish-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
- packages/shared/sdk-server-edge
- packages/shared/akamai-edgeworker-sdk
- packages/sdk/cloudflare
- packages/sdk/fastly
- packages/sdk/server-node
- packages/sdk/vercel
- packages/sdk/akamai-base
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/manual-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ on:
- packages/shared/sdk-server-edge
- packages/shared/akamai-edgeworker-sdk
- packages/sdk/cloudflare
- packages/sdk/fastly
- packages/sdk/react-native
- packages/sdk/server-node
- packages/sdk/react-universal
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jobs:
package-sdk-server-edge-released: ${{ steps.release.outputs['packages/shared/sdk-server-edge--release_created'] }}
package-akamai-edgeworker-sdk-released: ${{ steps.release.outputs['packages/shared/akamai-edgeworker-sdk--release_created'] }}
package-cloudflare-released: ${{ steps.release.outputs['packages/sdk/cloudflare--release_created'] }}
package-fastly-released: ${{ steps.release.outputs['packages/sdk/fastly--release_created'] }}
package-react-native-released: ${{ steps.release.outputs['packages/sdk/react-native--release_created'] }}
package-server-node-released: ${{ steps.release.outputs['packages/sdk/server-node--release_created'] }}
package-vercel-released: ${{ steps.release.outputs['packages/sdk/vercel--release_created'] }}
Expand Down Expand Up @@ -153,6 +154,26 @@ jobs:
workspace_path: packages/sdk/cloudflare
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}

release-fastly:
runs-on: ubuntu-latest
needs: ['release-please', 'release-sdk-server']
permissions:
id-token: write
contents: write
if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-fastly-released == 'true'}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
registry-url: 'https://registry.npmjs.org'
- id: release-fastly
name: Full release of packages/sdk/fastly
uses: ./actions/full-release
with:
workspace_path: packages/sdk/fastly
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}

release-react-native:
runs-on: ubuntu-latest
needs: ['release-please', 'release-sdk-client']
Expand Down
1 change: 1 addition & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"packages/shared/sdk-server": "2.11.1",
"packages/sdk/server-node": "9.7.4",
"packages/sdk/cloudflare": "2.6.5",
"packages/sdk/fastly": "0.0.1",
"packages/shared/sdk-server-edge": "2.5.4",
"packages/sdk/vercel": "1.3.23",
"packages/sdk/akamai-base": "2.1.23",
Expand Down
9 changes: 9 additions & 0 deletions .sdk_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
"tag-prefix": "cloudflare-server-sdk-"
}
},
"fastly": {
"name": "Fastly SDK",
"type": "edge",
"path": "packages/sdk/fastly",
"languages": ["JavaScript", "TypeScript"],
"releases": {
"tag-prefix": "faslty-server-sdk-"
}
},
"react-native": {
"name": "React Native SDK",
"type": "client-side",
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"packages/sdk/server-node",
"packages/sdk/cloudflare",
"packages/sdk/cloudflare/example",
"packages/sdk/fastly",
"packages/sdk/fastly/example",
"packages/sdk/react-native",
"packages/sdk/react-native/example",
"packages/sdk/react-universal",
Expand Down
62 changes: 62 additions & 0 deletions packages/sdk/fastly/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# LaunchDarkly SDK for Fastly

The LaunchDarkly SDK for Fastly is designed primarily for use in [Fastly Compute Platform](https://www.fastly.com/documentation/guides/compute/). It follows the server-side LaunchDarkly model for multi-user contexts. It is not intended for use in desktop and embedded systems applications.

# ⛔️⛔️⛔️⛔️

> [!CAUTION]
> This library is an alpha version and should not be considered ready for production use while this message is visible.

# ☝️☝️☝️☝️☝️☝️

## Install

```shell
# npm
npm i @launchdarkly/fastly-server-sdk

# yarn
yarn add @launchdarkly/fastly-server-sdk
```

## Quickstart

See the full [example app](https://github.com/launchdarkly/js-core/tree/main/packages/sdk/fastly/example).

## Developing this SDK

```shell
# at js-core repo root
yarn && yarn build && cd packages/sdk/fastly

# run tests
yarn test
```

## Verifying SDK build provenance with the SLSA framework

LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md).

## About LaunchDarkly

- LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can:
- Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.
- Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
- Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
- Grant access to certain features based on user attributes, like payment plan (eg: users on the 'gold' plan get access to more features than users in the 'silver' plan).
- Disable parts of your application to facilitate maintenance, without taking everything offline.
- LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
- Explore LaunchDarkly
- [launchdarkly.com](https://www.launchdarkly.com/ 'LaunchDarkly Main Website') for more information
- [docs.launchdarkly.com](https://docs.launchdarkly.com/ 'LaunchDarkly Documentation') for our documentation and SDK reference guides
- [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ 'LaunchDarkly API Documentation') for our API documentation
- [blog.launchdarkly.com](https://blog.launchdarkly.com/ 'LaunchDarkly Blog Documentation') for the latest product updates

[sdk-fastly-ci-badge]: https://github.com/launchdarkly/js-core/actions/workflows/fastly.yml/badge.svg
[sdk-fastly-ci]: https://github.com/launchdarkly/js-core/actions/workflows/fastly.yml
[sdk-fastly-npm-badge]: https://img.shields.io/npm/v/@launchdarkly/fastly-server-sdk.svg?style=flat-square
[sdk-fastly-npm-link]: https://www.npmjs.com/package/@launchdarkly/fastly-server-sdk
[sdk-fastly-ghp-badge]: https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8
[sdk-fastly-ghp-link]: https://launchdarkly.github.io/js-core/packages/sdk/fastly/docs/
[sdk-fastly-dm-badge]: https://img.shields.io/npm/dm/@launchdarkly/fastly-server-sdk.svg?style=flat-square
[sdk-fastly-dt-badge]: https://img.shields.io/npm/dt/@launchdarkly/fastly-server-sdk.svg?style=flat-square
8 changes: 8 additions & 0 deletions packages/sdk/fastly/__mocks__/fastly:kv-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const KVStore = jest.fn().mockImplementation(() => ({
get: jest.fn(),
put: jest.fn(),
delete: jest.fn(),
getMulti: jest.fn(),
putMulti: jest.fn(),
deleteMulti: jest.fn(),
}));
125 changes: 125 additions & 0 deletions packages/sdk/fastly/__tests__/api/EdgeFeatureStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { AsyncStoreFacade, LDFeatureStore } from '@launchdarkly/js-server-sdk-common';

import { EdgeFeatureStore } from '../../src/api/EdgeFeatureStore';
import mockEdgeProvider from '../../src/utils/mockEdgeProvider';
import * as testData from './testData.json';

describe('EdgeFeatureStore', () => {
const sdkKey = 'sdkKey';
const kvKey = `LD-Env-${sdkKey}`;
const mockLogger = {
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
const mockGet = mockEdgeProvider.get as jest.Mock;
let featureStore: LDFeatureStore;
let asyncFeatureStore: AsyncStoreFacade;

beforeEach(() => {
mockGet.mockImplementation(() => Promise.resolve(JSON.stringify(testData)));
featureStore = new EdgeFeatureStore(mockEdgeProvider, sdkKey, 'MockEdgeProvider', mockLogger);
asyncFeatureStore = new AsyncStoreFacade(featureStore);
});

afterEach(() => {
jest.resetAllMocks();
});

describe('get', () => {
test('get flag', async () => {
const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');

expect(mockGet).toHaveBeenCalledWith(kvKey);
expect(flag).toMatchObject(testData.flags.testFlag1);
});

test('invalid flag key', async () => {
const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'invalid');

expect(flag).toBeUndefined();
});

test('get segment', async () => {
const segment = await asyncFeatureStore.get({ namespace: 'segments' }, 'testSegment1');

expect(mockGet).toHaveBeenCalledWith(kvKey);
expect(segment).toMatchObject(testData.segments.testSegment1);
});

test('invalid segment key', async () => {
const segment = await asyncFeatureStore.get({ namespace: 'segments' }, 'invalid');

expect(segment).toBeUndefined();
});

test('invalid kv key', async () => {
mockGet.mockImplementation(() => Promise.resolve(null));
const flag = await asyncFeatureStore.get({ namespace: 'features' }, 'testFlag1');

expect(flag).toBeNull();
});
});

describe('all', () => {
test('all flags', async () => {
const flags = await asyncFeatureStore.all({ namespace: 'features' });

expect(mockGet).toHaveBeenCalledWith(kvKey);
expect(flags).toMatchObject(testData.flags);
});

test('all segments', async () => {
const segment = await asyncFeatureStore.all({ namespace: 'segments' });

expect(mockGet).toHaveBeenCalledWith(kvKey);
expect(segment).toMatchObject(testData.segments);
});

test('invalid DataKind', async () => {
const flag = await asyncFeatureStore.all({ namespace: 'InvalidDataKind' });

expect(flag).toEqual({});
});

test('invalid kv key', async () => {
mockGet.mockImplementation(() => Promise.resolve(null));
const segment = await asyncFeatureStore.all({ namespace: 'segments' });

expect(segment).toEqual({});
});
});

describe('initialized', () => {
test('is initialized', async () => {
const isInitialized = await asyncFeatureStore.initialized();

expect(mockGet).toHaveBeenCalledWith(kvKey);
expect(isInitialized).toBeTruthy();
});

test('not initialized', async () => {
mockGet.mockImplementation(() => Promise.resolve(null));
const isInitialized = await asyncFeatureStore.initialized();

expect(mockGet).toHaveBeenCalledWith(kvKey);
expect(isInitialized).toBeFalsy();
});
});

describe('init & getDescription', () => {
test('init', (done) => {
const cb = jest.fn(() => {
done();
});
featureStore.init(testData, cb);
});

test('getDescription', async () => {
const description = featureStore.getDescription?.();

expect(description).toEqual('MockEdgeProvider');
});
});
});
68 changes: 68 additions & 0 deletions packages/sdk/fastly/__tests__/api/LDClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { internal } from '@launchdarkly/js-server-sdk-common';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All tests in this directory were copied over from @launchdarkly/sdk-server-edge


import LDClient from '../../src/api/LDClient';
import { createBasicPlatform } from '../createBasicPlatform';

jest.mock('@launchdarkly/js-sdk-common', () => {
const actual = jest.requireActual('@launchdarkly/js-sdk-common');
return {
...actual,
...{
internal: {
...actual.internal,
DiagnosticsManager: jest.fn(),
EventProcessor: jest.fn(),
},
},
};
});

let mockEventProcessor = internal.EventProcessor as jest.Mock;
beforeEach(() => {
mockEventProcessor = internal.EventProcessor as jest.Mock;
mockEventProcessor.mockClear();
});

describe('Edge LDClient', () => {
it('uses clientSideID endpoints', async () => {
const client = new LDClient('client-side-id', createBasicPlatform().info, {
sendEvents: true,
eventsBackendName: 'launchdarkly',
});
await client.waitForInitialization({ timeout: 10 });
const passedConfig = mockEventProcessor.mock.calls[0][0];

expect(passedConfig).toMatchObject({
sendEvents: true,
serviceEndpoints: {
includeAuthorizationHeader: false,
analyticsEventPath: '/events/bulk/client-side-id',
diagnosticEventPath: '/events/diagnostic/client-side-id',
events: 'https://events.launchdarkly.com',
polling: 'https://sdk.launchdarkly.com',
streaming: 'https://stream.launchdarkly.com',
},
});
});
it('uses custom eventsUri when specified', async () => {
const client = new LDClient('client-side-id', createBasicPlatform().info, {
sendEvents: true,
eventsBackendName: 'launchdarkly',
eventsUri: 'https://custom-base-uri.launchdarkly.com',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added the option to set a custom eventsUri. baseUri doesn't really make sense for these integrations so I also added logic to set baseUri to eventsUri if eventsUri is specified in order to pass the validation in sdk-server-common.

});
await client.waitForInitialization({ timeout: 10 });
const passedConfig = mockEventProcessor.mock.calls[0][0];

expect(passedConfig).toMatchObject({
sendEvents: true,
serviceEndpoints: {
includeAuthorizationHeader: false,
analyticsEventPath: '/events/bulk/client-side-id',
diagnosticEventPath: '/events/diagnostic/client-side-id',
events: 'https://custom-base-uri.launchdarkly.com',
polling: 'https://custom-base-uri.launchdarkly.com',
streaming: 'https://stream.launchdarkly.com',
},
});
});
});
17 changes: 17 additions & 0 deletions packages/sdk/fastly/__tests__/api/createOptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { BasicLogger } from '@launchdarkly/js-server-sdk-common';

import createOptions, { defaultOptions } from '../../src/api/createOptions';

describe('createOptions', () => {
test('default options', () => {
expect(createOptions({})).toEqual(defaultOptions);
});

test('override logger', () => {
const logger = new BasicLogger({ name: 'test' });
expect(createOptions({ logger })).toEqual({
...defaultOptions,
logger,
});
});
});
Loading
Loading