hypertune
Version:
[Hypertune](https://www.hypertune.com/) is the most flexible platform for feature flags, A/B testing, analytics and app configuration. Built with full end-to-end type-safety, Git-style version control and local, synchronous, in-memory flag evaluation. Opt
260 lines (243 loc) • 7.21 kB
text/typescript
import {
defaultCacheSize,
prodLogsEndpointUrl,
prodEdgeCdnBaseUrl,
uniqueId,
sdkVersion,
} from "../shared";
import {
LogsHandler,
CreateOptions,
LocalLogger,
LogLevel,
ObjectValue,
ObjectValueWithVariables,
Query,
} from "../shared/types";
import Context from "./Context";
import Node, { NodeProps } from "./Node";
import Logger from "./Logger";
import { isBrowser } from "./environment";
import getMetadata from "../shared/helpers/getMetadata";
import HypertuneEdgeInitDataProvider from "./initDataProviders/HypertuneEdgeInitDataProvider";
import VercelEdgeConfigInitDataProvider from "./initDataProviders/VercelEdgeConfigInitDataProvider";
import defaultLocalLogger from "../shared/helpers/defaultLocalLogger";
const minIntervalMs = 1_000;
const defaultIntervalMs = 2_000;
/**
* TODO
* - Remove `queryCode`; consolidate with `query`; benefit of having them
* separate was `queryCode` can be codegen'd differently, but this is not
* necessary
* - Add default values to arguments where possible
* - Move all arguments into `CreateOptions` with type parameters if they have
* generated types, then codegen concrete `CreateSourceOptions`
*/
export default function create<T extends Node = Node>({
NodeConstructor = Node as new (props: NodeProps) => T,
token,
query = null,
queryId,
queryCode,
variableValues = {},
override = null,
options = {},
}: {
NodeConstructor?: new (props: NodeProps) => T;
token: string;
query?: Query<ObjectValueWithVariables> | null;
queryId?: string;
queryCode?: string;
variableValues?: ObjectValue;
override?: object | null;
options?: CreateOptions;
}): T {
const traceId = uniqueId();
const loggerId = uniqueId();
const logsHandler = wrapLogsHandler(
loggerId,
options.logsHandler,
options.localLogger
);
const localLogger = wrapLocalLogger(
loggerId,
options.logsHandler,
options.localLogger
);
let logger: Logger;
const remoteLoggingMode =
options.remoteLogging?.mode ?? (isBrowser ? "session" : "normal");
try {
logger = new Logger({
id: loggerId,
traceId,
token,
remoteLoggingMode,
remoteFlushIntervalMs:
options.remoteLogging?.flushIntervalMs !== undefined
? options.remoteLogging.flushIntervalMs === null ||
options.remoteLogging.flushIntervalMs === 0 // TODO: Deprecate
? null
: clampInterval(options.remoteLogging.flushIntervalMs)
: defaultIntervalMs,
remoteLoggingEndpointUrl:
options.remoteLogging?.endpointUrl ?? prodLogsEndpointUrl,
logsHandler,
localLogger,
});
} catch (error) {
localLogger(LogLevel.Error, "Pre-initialize error.", getMetadata(error));
return new NodeConstructor({
context: null,
logger: null,
parent: null,
step: null,
expression: null,
initDataHash: null,
});
}
const usesVercelEdgeConfig =
options.initDataProvider &&
options.initDataProvider instanceof VercelEdgeConfigInitDataProvider;
logger.log(LogLevel.Debug, null, "Detected environment.", {
isBrowser,
usesVercelEdgeConfig,
});
try {
const context = new Context({
traceId,
query,
initQuery: queryId
? { type: "StoredQuery", id: queryId }
: queryCode
? { type: "GraphqlQuery", code: queryCode }
: { type: "Query", query },
variableValues,
initData: options.initData ?? null,
lastInitDataRefreshTime:
options.lastInitDataRefreshTime ??
options.lastDataProviderInitTime ??
null,
initDataProvider:
options.initDataProvider !== undefined
? options.initDataProvider
: new HypertuneEdgeInitDataProvider({
token,
baseUrl: prodEdgeCdnBaseUrl,
branchName: options.branchName,
localLogger,
}),
initDataRefreshIntervalMs:
// Prioritize the new option else use the old option if it's not set to
// 0 else use the default interval. In all cases, force the value to be
// at least the minimum interval.
clampInterval(
options.initDataRefreshIntervalMs ??
(options.initIntervalMs || defaultIntervalMs)
),
shouldRefreshInitData:
// Prioritize the new option else if the old option is set to 0,
// disable, else enable except when using Vercel Edge Config.
options.shouldRefreshInitData ??
options.shouldCheckForUpdates ??
(options.initIntervalMs === 0 ? false : !usesVercelEdgeConfig),
shouldRefreshInitDataOnCreate:
options.shouldRefreshInitDataOnCreate ?? !usesVercelEdgeConfig,
shouldSkipInitDataUpdateOnRefresh:
options.shouldSkipInitDataUpdateOnRefresh ?? false,
logger,
cacheSize: options.cacheSize ?? defaultCacheSize,
override: override ?? null,
});
return new NodeConstructor({
context,
logger,
parent: null,
step: null,
expression: context.initData?.reducedExpression ?? null,
initDataHash: context.initData?.hash ?? null,
});
} catch (error) {
logger.log(
LogLevel.Error,
/* commitId */ null,
"Initialize error.",
getMetadata(error)
);
return new NodeConstructor({
context: null,
logger,
parent: null,
step: null,
expression: null,
initDataHash: null,
});
}
}
function clampInterval(intervalMs: number): number {
return Math.max(intervalMs, minIntervalMs);
}
function wrapLogsHandler(
loggerId: string,
logsHandler: LogsHandler | undefined,
localLogger: LocalLogger | undefined
): LogsHandler {
if (!localLogger && !logsHandler) {
return (reductionLogs) => {
reductionLogs.messageList.forEach(({ level, message, metadata }) =>
defaultLocalLogger(level, message, metadata)
);
};
}
return (reductionLogs) => {
const messages =
!logsHandler && !localLogger
? reductionLogs.messageList
: reductionLogs.messageList.map(({ level, message, metadata }) => ({
level,
message,
metadata: {
sdkVersion,
loggerId,
...metadata,
},
}));
if (logsHandler) {
logsHandler({ ...reductionLogs, messageList: messages });
return;
}
if (localLogger) {
messages.forEach(({ level, message, metadata }) => {
localLogger(level, message, metadata);
});
}
};
}
function wrapLocalLogger(
loggerId: string,
logsHandler: LogsHandler | undefined,
localLogger: LocalLogger | undefined
): LocalLogger {
if (!localLogger && !logsHandler) {
return defaultLocalLogger;
}
return (level, message, baseMetadata) => {
const metadata = {
sdkVersion,
loggerId,
...baseMetadata,
};
if (logsHandler) {
logsHandler({
messageList: [{ level, message, metadata }],
eventList: [],
exposureList: [],
evaluationList: [],
});
return;
}
if (localLogger) {
localLogger(level, message, metadata);
}
};
}