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 12 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
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 @@ -152,6 +153,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.10.0",
"packages/sdk/server-node": "9.7.2",
"packages/sdk/cloudflare": "2.6.3",
"packages/sdk/fastly": "0.0.1",
"packages/shared/sdk-server-edge": "2.5.2",
"packages/sdk/vercel": "1.3.21",
"packages/sdk/akamai-base": "2.1.20",
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": {
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure about this key being capitalized.

"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');
});
});
});
46 changes: 46 additions & 0 deletions packages/sdk/fastly/__tests__/api/LDClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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(),
},
},
};
});

const mockEventProcessor = internal.EventProcessor as jest.Mock;
describe('Edge LDClient', () => {
it('uses clientSideID endpoints', async () => {
const client = new LDClient(
'client-side-id',
createBasicPlatform().info,
{
sendEvents: true,
},
'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',
},
});
});
});
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