launchdarkly-js-sdk-common 5.4.0 → 5.5.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ name: Manual Publish Package
2
+ on:
3
+ workflow_dispatch:
4
+ inputs:
5
+ dry-run:
6
+ description: 'Is this a dry run. If so no package will be published.'
7
+ type: boolean
8
+ required: true
9
+ prerelease:
10
+ description: 'Is this a prerelease. If so, then the latest tag will not be updated in npm.'
11
+ type: boolean
12
+ required: true
13
+
14
+ jobs:
15
+ publish-package:
16
+ runs-on: ubuntu-latest
17
+ permissions:
18
+ id-token: write
19
+ contents: write
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: actions/setup-node@v4
24
+ with:
25
+ node-version: 20.x
26
+ registry-url: 'https://registry.npmjs.org'
27
+
28
+ - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.2.0
29
+ name: 'Get NPM token'
30
+ with:
31
+ aws_assume_role: ${{ vars.AWS_ROLE_ARN }}
32
+ ssm_parameter_pairs: '/production/common/releasing/npm/token = NODE_AUTH_TOKEN'
33
+
34
+ - name: Install Dependencies
35
+ run: npm install
36
+
37
+ - id: publish-npm
38
+ name: Publish NPM Package
39
+ uses: ./.github/actions/publish-npm
40
+ with:
41
+ dry-run: ${{ inputs.dry-run }}
42
+ prerelease: ${{ inputs.prerelease }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launchdarkly-js-sdk-common",
3
- "version": "5.4.0",
3
+ "version": "5.5.0-beta.2",
4
4
  "description": "LaunchDarkly SDK for JavaScript - common code",
5
5
  "author": "LaunchDarkly <team@launchdarkly.com>",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,119 @@
1
+ const UNKNOWN_HOOK_NAME = 'unknown hook';
2
+ const BEFORE_EVALUATION_STAGE_NAME = 'beforeEvaluation';
3
+ const AFTER_EVALUATION_STAGE_NAME = 'afterEvaluation';
4
+
5
+ function tryExecuteStage(logger, method, hookName, stage, def) {
6
+ try {
7
+ return stage();
8
+ } catch (err) {
9
+ logger?.error(`An error was encountered in "${method}" of the "${hookName}" hook: ${err}`);
10
+ return def;
11
+ }
12
+ }
13
+
14
+ function getHookName(logger, hook) {
15
+ try {
16
+ return hook.getMetadata().name || UNKNOWN_HOOK_NAME;
17
+ } catch {
18
+ logger.error(`Exception thrown getting metadata for hook. Unable to get hook name.`);
19
+ return UNKNOWN_HOOK_NAME;
20
+ }
21
+ }
22
+
23
+ function executeBeforeEvaluation(logger, hooks, hookContext) {
24
+ return hooks.map(hook =>
25
+ tryExecuteStage(
26
+ logger,
27
+ BEFORE_EVALUATION_STAGE_NAME,
28
+ getHookName(logger, hook),
29
+ () => hook?.beforeEvaluation?.(hookContext, {}) ?? {},
30
+ {}
31
+ )
32
+ );
33
+ }
34
+
35
+ function executeAfterEvaluation(logger, hooks, hookContext, updatedData, result) {
36
+ // This iterates in reverse, versus reversing a shallow copy of the hooks,
37
+ // for efficiency.
38
+ for (let hookIndex = hooks.length - 1; hookIndex >= 0; hookIndex -= 1) {
39
+ const hook = hooks[hookIndex];
40
+ const data = updatedData[hookIndex];
41
+ tryExecuteStage(
42
+ logger,
43
+ AFTER_EVALUATION_STAGE_NAME,
44
+ getHookName(logger, hook),
45
+ () => hook?.afterEvaluation?.(hookContext, data, result) ?? {},
46
+ {}
47
+ );
48
+ }
49
+ }
50
+
51
+ function executeBeforeIdentify(logger, hooks, hookContext) {
52
+ return hooks.map(hook =>
53
+ tryExecuteStage(
54
+ logger,
55
+ BEFORE_EVALUATION_STAGE_NAME,
56
+ getHookName(logger, hook),
57
+ () => hook?.beforeIdentify?.(hookContext, {}) ?? {},
58
+ {}
59
+ )
60
+ );
61
+ }
62
+
63
+ function executeAfterIdentify(logger, hooks, hookContext, updatedData, result) {
64
+ // This iterates in reverse, versus reversing a shallow copy of the hooks,
65
+ // for efficiency.
66
+ for (let hookIndex = hooks.length - 1; hookIndex >= 0; hookIndex -= 1) {
67
+ const hook = hooks[hookIndex];
68
+ const data = updatedData[hookIndex];
69
+ tryExecuteStage(
70
+ logger,
71
+ AFTER_EVALUATION_STAGE_NAME,
72
+ getHookName(logger, hook),
73
+ () => hook?.afterIdentify?.(hookContext, data, result) ?? {},
74
+ {}
75
+ );
76
+ }
77
+ }
78
+
79
+ class HookRunner {
80
+ constructor(logger, initialHooks) {
81
+ this._logger = logger;
82
+ this._hooks = initialHooks ? [...initialHooks] : [];
83
+ }
84
+
85
+ withEvaluation(key, context, defaultValue, method) {
86
+ if (this._hooks.length === 0) {
87
+ return method();
88
+ }
89
+ const hooks = [...this._hooks];
90
+ const hookContext = {
91
+ flagKey: key,
92
+ context,
93
+ defaultValue,
94
+ };
95
+
96
+ const hookData = executeBeforeEvaluation(this._logger, hooks, hookContext);
97
+ const result = method();
98
+ executeAfterEvaluation(this._logger, hooks, hookContext, hookData, result);
99
+ return result;
100
+ }
101
+
102
+ identify(context, timeout) {
103
+ const hooks = [...this._hooks];
104
+ const hookContext = {
105
+ context,
106
+ timeout,
107
+ };
108
+ const hookData = executeBeforeIdentify(this._logger, hooks, hookContext);
109
+ return result => {
110
+ executeAfterIdentify(this._logger, hooks, hookContext, hookData, result);
111
+ };
112
+ }
113
+
114
+ addHook(hook) {
115
+ this._hooks.push(hook);
116
+ }
117
+ }
118
+
119
+ module.exports = HookRunner;
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ const messages = require('./messages');
17
17
  const { checkContext, getContextKeys } = require('./context');
18
18
  const { InspectorTypes, InspectorManager } = require('./InspectorManager');
19
19
  const timedPromise = require('./timedPromise');
20
+ const HookRunner = require('./HookRunner');
20
21
 
21
22
  const changeEvent = 'change';
22
23
  const internalChangeEvent = 'internal-change';
@@ -40,6 +41,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
40
41
  const sendEvents = options.sendEvents;
41
42
  let environment = env;
42
43
  let hash = options.hash;
44
+ const hookRunner = new HookRunner(logger, options.hooks);
43
45
 
44
46
  const persistentStorage = PersistentStorage(platform.localStorage, logger);
45
47
 
@@ -256,11 +258,16 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
256
258
  logger.warn(messages.identifyDisabled());
257
259
  return utils.wrapPromiseCallback(Promise.resolve(utils.transformVersionedValuesToValues(flags)), onDone);
258
260
  }
261
+ let afterIdentify;
259
262
  const clearFirst = useLocalStorage && persistentFlagStore ? persistentFlagStore.clearFlags() : Promise.resolve();
260
263
  return utils.wrapPromiseCallback(
261
264
  clearFirst
262
265
  .then(() => anonymousContextProcessor.processContext(context))
263
266
  .then(verifyContext)
267
+ .then(context => {
268
+ afterIdentify = hookRunner.identify(context, undefined);
269
+ return context;
270
+ })
264
271
  .then(validatedContext =>
265
272
  requestor
266
273
  .fetchFlagSettings(validatedContext, newHash)
@@ -277,12 +284,14 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
277
284
  })
278
285
  )
279
286
  .then(flagValueMap => {
287
+ afterIdentify?.({ status: 'completed' });
280
288
  if (streamActive) {
281
289
  connectStream();
282
290
  }
283
291
  return flagValueMap;
284
292
  })
285
293
  .catch(err => {
294
+ afterIdentify?.({ status: 'error' });
286
295
  emitter.maybeReportError(err);
287
296
  return Promise.reject(err);
288
297
  }),
@@ -299,11 +308,16 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
299
308
  }
300
309
 
301
310
  function variation(key, defaultValue) {
302
- return variationDetailInternal(key, defaultValue, true, false, false, true).value;
311
+ const { value } = hookRunner.withEvaluation(key, ident.getContext(), defaultValue, () =>
312
+ variationDetailInternal(key, defaultValue, true, false, false, true)
313
+ );
314
+ return value;
303
315
  }
304
316
 
305
317
  function variationDetail(key, defaultValue) {
306
- return variationDetailInternal(key, defaultValue, true, true, false, true);
318
+ return hookRunner.withEvaluation(key, ident.getContext(), defaultValue, () =>
319
+ variationDetailInternal(key, defaultValue, true, true, false, true)
320
+ );
307
321
  }
308
322
 
309
323
  function variationDetailInternal(key, defaultValue, sendEvent, includeReasonInEvent, isAllFlags, notifyInspection) {
@@ -826,6 +840,10 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
826
840
  return initializationStateTracker.getInitializationPromise();
827
841
  }
828
842
 
843
+ function addHook(hook) {
844
+ hookRunner.addHook(hook);
845
+ }
846
+
829
847
  const client = {
830
848
  waitForInitialization,
831
849
  waitUntilReady: () => initializationStateTracker.getReadyPromise(),
@@ -840,6 +858,7 @@ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
840
858
  flush: flush,
841
859
  allFlags: allFlags,
842
860
  close: close,
861
+ addHook: addHook,
843
862
  };
844
863
 
845
864
  return {
package/typings.d.ts CHANGED
@@ -40,6 +40,184 @@ declare module 'launchdarkly-js-sdk-common' {
40
40
  error: (message: string) => void;
41
41
  }
42
42
 
43
+ /**
44
+ * Contextual information provided to evaluation stages.
45
+ */
46
+ export interface EvaluationSeriesContext {
47
+ /**
48
+ * The flag key the evaluation is for.
49
+ */
50
+ readonly flagKey: string;
51
+ /**
52
+ * Optional in case evaluations are performed before a context is set.
53
+ */
54
+ readonly context?: LDContext;
55
+ /**
56
+ * The default value that was provided.
57
+ */
58
+ readonly defaultValue: unknown;
59
+
60
+ /**
61
+ * Implementation note: Omitting method name because of the associated size.
62
+ * If we need this functionality, then we may want to consider adding it and
63
+ * taking the associated size hit.
64
+ */
65
+ }
66
+
67
+ /**
68
+ * Implementation specific hook data for evaluation stages.
69
+ *
70
+ * Hook implementations can use this to store data needed between stages.
71
+ */
72
+ export interface EvaluationSeriesData {
73
+ readonly [index: string]: unknown;
74
+ }
75
+
76
+ /**
77
+ * Meta-data about a hook implementation.
78
+ */
79
+ export interface HookMetadata {
80
+ /**
81
+ * Name of the hook.
82
+ */
83
+ readonly name: string;
84
+ }
85
+
86
+ /**
87
+ * Contextual information provided to identify stages.
88
+ */
89
+ export interface IdentifySeriesContext {
90
+ /**
91
+ * The context associated with the identify operation.
92
+ */
93
+ readonly context: LDContext;
94
+ /**
95
+ * The timeout, in seconds, associated with the identify operation.
96
+ */
97
+ readonly timeout?: number;
98
+ }
99
+
100
+ /**
101
+ * Implementation specific hook data for identify stages.
102
+ *
103
+ * Hook implementations can use this to store data needed between stages.
104
+ */
105
+ export interface IdentifySeriesData {
106
+ readonly [index: string]: unknown;
107
+ }
108
+
109
+ /**
110
+ * The status an identify operation completed with.
111
+ *
112
+ * An example in which an error may occur is lack of network connectivity
113
+ * preventing the SDK from functioning.
114
+ */
115
+ export type IdentifySeriesStatus = 'completed' | 'error';
116
+
117
+ /**
118
+ * The result applies to a single identify operation. An operation may complete
119
+ * with an error and then later complete successfully. Only the first completion
120
+ * will be executed in the identify series.
121
+ *
122
+ * For example, a network issue may cause an identify to error since the SDK
123
+ * can't refresh its cached data from the cloud at that moment, but then later
124
+ * the when the network issue is resolved, the SDK will refresh cached data.
125
+ */
126
+ export interface IdentifySeriesResult {
127
+ status: IdentifySeriesStatus;
128
+ }
129
+
130
+ /**
131
+ * Interface for extending SDK functionality via hooks.
132
+ */
133
+ export interface Hook {
134
+ /**
135
+ * Get metadata about the hook implementation.
136
+ */
137
+ getMetadata(): HookMetadata;
138
+
139
+ /**
140
+ * This method is called during the execution of a variation method
141
+ * before the flag value has been determined. The method is executed synchronously.
142
+ *
143
+ * @param hookContext Contains information about the evaluation being performed. This is not
144
+ * mutable.
145
+ * @param data A record associated with each stage of hook invocations. Each stage is called with
146
+ * the data of the previous stage for a series. The input record should not be modified.
147
+ * @returns Data to use when executing the next state of the hook in the evaluation series. It is
148
+ * recommended to expand the previous input into the return. This helps ensure your stage remains
149
+ * compatible moving forward as more stages are added.
150
+ * ```js
151
+ * return {...data, "my-new-field": /*my data/*}
152
+ * ```
153
+ */
154
+ beforeEvaluation?(
155
+ hookContext: EvaluationSeriesContext,
156
+ data: EvaluationSeriesData,
157
+ ): EvaluationSeriesData;
158
+
159
+ /**
160
+ * This method is called during the execution of the variation method
161
+ * after the flag value has been determined. The method is executed synchronously.
162
+ *
163
+ * @param hookContext Contains read-only information about the evaluation
164
+ * being performed.
165
+ * @param data A record associated with each stage of hook invocations. Each
166
+ * stage is called with the data of the previous stage for a series.
167
+ * @param detail The result of the evaluation. This value should not be
168
+ * modified.
169
+ * @returns Data to use when executing the next state of the hook in the evaluation series. It is
170
+ * recommended to expand the previous input into the return. This helps ensure your stage remains
171
+ * compatible moving forward as more stages are added.
172
+ * ```js
173
+ * return {...data, "my-new-field": /*my data/*}
174
+ * ```
175
+ */
176
+ afterEvaluation?(
177
+ hookContext: EvaluationSeriesContext,
178
+ data: EvaluationSeriesData,
179
+ detail: LDEvaluationDetail,
180
+ ): EvaluationSeriesData;
181
+
182
+ /**
183
+ * This method is called during the execution of the identify process before the operation
184
+ * completes, but after any context modifications are performed.
185
+ *
186
+ * @param hookContext Contains information about the evaluation being performed. This is not
187
+ * mutable.
188
+ * @param data A record associated with each stage of hook invocations. Each stage is called with
189
+ * the data of the previous stage for a series. The input record should not be modified.
190
+ * @returns Data to use when executing the next state of the hook in the evaluation series. It is
191
+ * recommended to expand the previous input into the return. This helps ensure your stage remains
192
+ * compatible moving forward as more stages are added.
193
+ * ```js
194
+ * return {...data, "my-new-field": /*my data/*}
195
+ * ```
196
+ */
197
+ beforeIdentify?(hookContext: IdentifySeriesContext, data: IdentifySeriesData): IdentifySeriesData;
198
+
199
+ /**
200
+ * This method is called during the execution of the identify process before the operation
201
+ * completes, but after any context modifications are performed.
202
+ *
203
+ * @param hookContext Contains information about the evaluation being performed. This is not
204
+ * mutable.
205
+ * @param data A record associated with each stage of hook invocations. Each stage is called with
206
+ * the data of the previous stage for a series. The input record should not be modified.
207
+ * @returns Data to use when executing the next state of the hook in the evaluation series. It is
208
+ * recommended to expand the previous input into the return. This helps ensure your stage remains
209
+ * compatible moving forward as more stages are added.
210
+ * ```js
211
+ * return {...data, "my-new-field": /*my data/*}
212
+ * ```
213
+ */
214
+ afterIdentify?(
215
+ hookContext: IdentifySeriesContext,
216
+ data: IdentifySeriesData,
217
+ result: IdentifySeriesResult,
218
+ ): IdentifySeriesData;
219
+ }
220
+
43
221
  /**
44
222
  * LaunchDarkly initialization options that are supported by all variants of the JS client.
45
223
  * The browser SDK and Electron SDK may support additional options.
@@ -277,6 +455,25 @@ declare module 'launchdarkly-js-sdk-common' {
277
455
  * Inspectors can be used for collecting information for monitoring, analytics, and debugging.
278
456
  */
279
457
  inspectors?: LDInspection[];
458
+
459
+ /**
460
+ * Initial set of hooks for the client.
461
+ *
462
+ * Hooks provide entrypoints which allow for observation of SDK functions.
463
+ *
464
+ * LaunchDarkly provides integration packages, and most applications will not
465
+ * need to implement their own hooks. Refer to the `@launchdarkly/node-server-sdk-otel`
466
+ * for instrumentation for the `@launchdarkly/node-server-sdk`.
467
+ *
468
+ * Example:
469
+ * ```typescript
470
+ * import { init } from '@launchdarkly/node-server-sdk';
471
+ * import { TheHook } from '@launchdarkly/some-hook';
472
+ *
473
+ * const client = init('my-sdk-key', { hooks: [new TheHook()] });
474
+ * ```
475
+ */
476
+ hooks?: Hook[];
280
477
  }
281
478
 
282
479
  /**
@@ -909,6 +1106,16 @@ declare module 'launchdarkly-js-sdk-common' {
909
1106
  * closing is finished. It will never be rejected.
910
1107
  */
911
1108
  close(onDone?: () => void): Promise<void>;
1109
+
1110
+ /**
1111
+ * Add a hook to the client. In order to register a hook before the client
1112
+ * starts, please use the `hooks` property of {@link LDOptions}.
1113
+ *
1114
+ * Hooks provide entrypoints which allow for observation of SDK functions.
1115
+ *
1116
+ * @param Hook The hook to add.
1117
+ */
1118
+ addHook(hook: Hook): void;
912
1119
  }
913
1120
 
914
1121
  /**