diff --git a/.changeset/lazy-vercel-world-bundle.md b/.changeset/lazy-vercel-world-bundle.md
new file mode 100644
index 0000000000..8dedcf1c64
--- /dev/null
+++ b/.changeset/lazy-vercel-world-bundle.md
@@ -0,0 +1,5 @@
+---
+'@workflow/next': patch
+---
+
+Bundle `@workflow/world-vercel` into the Next.js server output instead of leaving it external, so cold starts no longer resolve its module graph from disk one file at a time.
diff --git a/.changeset/queue-client-0-5-0.md b/.changeset/queue-client-0-5-0.md
new file mode 100644
index 0000000000..a1c6bfff58
--- /dev/null
+++ b/.changeset/queue-client-0-5-0.md
@@ -0,0 +1,5 @@
+---
+'@workflow/world-vercel': patch
+---
+
+Update `@vercel/queue` to `0.5.0`
diff --git a/docs/content/worlds/v4/postgres.mdx b/docs/content/worlds/v4/postgres.mdx
index 6120ad7e1e..6a39495a02 100644
--- a/docs/content/worlds/v4/postgres.mdx
+++ b/docs/content/worlds/v4/postgres.mdx
@@ -75,6 +75,14 @@ The migration is idempotent and can safely be run as a post-deployment lifecycle
To subscribe to the graphile-worker queue, your workflow app needs to start the world on server start. Here are examples for a few frameworks:
+
+This step is specific to worlds that run a background worker, such as Postgres
+World. Worlds whose queue delivers work over HTTP, including the Vercel world,
+have no worker to subscribe, so `start()` does nothing there while the import
+still costs you: pulling in `workflow/runtime` from a server-startup hook puts
+the whole runtime into the cold-start path before the first request is served.
+
+
diff --git a/docs/content/worlds/v5/postgres.mdx b/docs/content/worlds/v5/postgres.mdx
index 82757b83fd..caff939582 100644
--- a/docs/content/worlds/v5/postgres.mdx
+++ b/docs/content/worlds/v5/postgres.mdx
@@ -84,6 +84,14 @@ The migration is idempotent and can safely be run as a post-deployment lifecycle
To subscribe to the graphile-worker queue, your workflow app needs to start the world on server start. Here are examples for a few frameworks:
+
+This step is specific to worlds that run a background worker, such as Postgres
+World. Worlds whose queue delivers work over HTTP, including the Vercel world,
+have no worker to subscribe, so `start()` does nothing there while the import
+still costs you: pulling in `workflow/runtime` from a server-startup hook puts
+the whole runtime into the cold-start path before the first request is served.
+
+
diff --git a/packages/next/src/index.test.ts b/packages/next/src/index.test.ts
index ae03ffabb6..89d7c980e6 100644
--- a/packages/next/src/index.test.ts
+++ b/packages/next/src/index.test.ts
@@ -187,7 +187,7 @@ describe('withWorkflow builder config', () => {
});
});
- it('externalizes the built-in Vercel world while preserving user externals', async () => {
+ it('externalizes the non-bundlable Vercel packages while preserving user externals', async () => {
const config = withWorkflow({
serverExternalPackages: ['@node-rs/xxhash'],
});
@@ -196,10 +196,11 @@ describe('withWorkflow builder config', () => {
defaultConfig: {},
});
+ // @workflow/world-vercel and @vercel/queue are deliberately absent: they
+ // are bundled into the server output so a cold start does not resolve their
+ // module graphs from disk one file at a time.
expect(nextConfig.serverExternalPackages).toEqual([
'@node-rs/xxhash',
- '@workflow/world-vercel',
- '@vercel/queue',
'@vercel/oidc',
'@vercel/cli-auth',
'@napi-rs/keyring',
@@ -207,6 +208,68 @@ describe('withWorkflow builder config', () => {
expect(nextConfig.outputFileTracingIncludes).toBeUndefined();
});
+ function writeProjectWithQueueVersion(version: string): string {
+ const projectDir = mkdtempSync(
+ join(realTmpDir, 'workflow-next-queue-version-')
+ );
+ writeFile(
+ join(projectDir, 'node_modules', '@vercel', 'queue', 'package.json'),
+ JSON.stringify({
+ name: '@vercel/queue',
+ version,
+ main: 'index.js',
+ // The published package exports `.` and nothing else, so asking Node
+ // for the manifest by subpath throws. Mirroring the map here keeps a
+ // resolver-based lookup from passing against a fixture that the real
+ // package layout would reject.
+ exports: {
+ '.': {
+ require: './index.js',
+ },
+ },
+ })
+ );
+ writeFile(
+ join(projectDir, 'node_modules', '@vercel', 'queue', 'index.js'),
+ 'module.exports = {};'
+ );
+ return projectDir;
+ }
+
+ it('keeps an unbundlable @vercel/queue external', async () => {
+ // Older releases leave their dynamic `import()` unannotated, which Turbopack
+ // cannot resolve, so bundling that copy risks failing the build. Any version
+ // below the bundlable floor works here, without restating the floor.
+ process.chdir(writeProjectWithQueueVersion('0.0.1'));
+
+ const nextConfig = await withWorkflow({})('phase-production-build', {
+ defaultConfig: {},
+ });
+
+ expect(nextConfig.serverExternalPackages).toContain('@vercel/queue');
+ });
+
+ it('bundles a @vercel/queue version that carries the bundler-ignore hints', async () => {
+ // Any version above the bundlable floor, without restating the floor here.
+ process.chdir(writeProjectWithQueueVersion('99.0.0'));
+
+ const nextConfig = await withWorkflow({})('phase-production-build', {
+ defaultConfig: {},
+ });
+
+ expect(nextConfig.serverExternalPackages).not.toContain('@vercel/queue');
+ });
+
+ it('keeps the Vercel world external for the workflow/step bundles', async () => {
+ const config = withWorkflow({});
+
+ await config('phase-production-build', { defaultConfig: {} });
+
+ expect(builderConfigs[0].externalPackages).toEqual(
+ expect.arrayContaining(['@workflow/world-vercel', '@vercel/queue'])
+ );
+ });
+
it('preserves user webpack externals without adding Vercel world dependency externals', async () => {
const userWebpack = vi.fn((webpackConfig: any) => {
webpackConfig.externals = [{ react: 'commonjs react' }];
@@ -286,8 +349,6 @@ describe('withWorkflow builder config', () => {
expect(resolvedConfig.serverExternalPackages).toEqual([
'plain-external-a',
- '@workflow/world-vercel',
- '@vercel/queue',
'@vercel/oidc',
'@vercel/cli-auth',
'@napi-rs/keyring',
@@ -298,11 +359,11 @@ describe('withWorkflow builder config', () => {
'server-only',
'client-only',
'plain-external-a',
- '@workflow/world-vercel',
- '@vercel/queue',
'@vercel/oidc',
'@vercel/cli-auth',
'@napi-rs/keyring',
+ '@workflow/world-vercel',
+ '@vercel/queue',
],
});
@@ -351,8 +412,6 @@ describe('withWorkflow builder config', () => {
expect(resolvedConfig.serverExternalPackages).toEqual([
'plain-external-b',
- '@workflow/world-vercel',
- '@vercel/queue',
'@vercel/oidc',
'@vercel/cli-auth',
'@napi-rs/keyring',
@@ -363,11 +422,11 @@ describe('withWorkflow builder config', () => {
'server-only',
'client-only',
'plain-external-b',
- '@workflow/world-vercel',
- '@vercel/queue',
'@vercel/oidc',
'@vercel/cli-auth',
'@napi-rs/keyring',
+ '@workflow/world-vercel',
+ '@vercel/queue',
],
});
expect(warnSpy).not.toHaveBeenCalled();
diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts
index 98a3e465cf..da0feac73c 100644
--- a/packages/next/src/index.ts
+++ b/packages/next/src/index.ts
@@ -1,4 +1,4 @@
-import { copyFileSync, mkdirSync, statSync } from 'node:fs';
+import { copyFileSync, mkdirSync, readFileSync, statSync } from 'node:fs';
import { copyFile, mkdir, readFile } from 'node:fs/promises';
import { dirname, isAbsolute, join } from 'node:path';
import {
@@ -11,13 +11,36 @@ import semver from 'semver';
import { getNextBuilder } from './builder.js';
const VERCEL_WORLD_PACKAGE = '@workflow/world-vercel';
-const VERCEL_WORLD_DEPENDENCY_PACKAGES = [
- '@vercel/queue',
+const QUEUE_PACKAGE = '@vercel/queue';
+// Bundling `@vercel/queue` requires the version whose dynamic `import()` carries
+// `turbopackIgnore`/`webpackIgnore` annotations. Without them Turbopack cannot
+// resolve that import ("server relative imports are not implemented yet") and
+// fails the build, so an older copy has to stay external.
+const QUEUE_MIN_BUNDLABLE_VERSION = '0.5.0';
+// `@vercel/oidc` reaches `@vercel/cli-config`, and `@vercel/cli-auth` reaches
+// both that and `@napi-rs/keyring`. Those are CLI-shaped dependencies that never
+// run on a server: `xdg-app-paths` builds its config directory at module scope
+// and throws `The "path" argument must be of type string` once bundled, which
+// fails page-data collection. Keeping the three external stops any bundler from
+// walking into that tree.
+const VERCEL_WORLD_SERVER_EXTERNAL_PACKAGES = [
'@vercel/oidc',
'@vercel/cli-auth',
'@napi-rs/keyring',
];
-const VERCEL_WORLD_SERVER_EXTERNAL_PACKAGES = [
+// `@workflow/world-vercel` and the queue client are bundled into the Next.js
+// server build. Every module left external has to be resolved from disk on a
+// cold start, one file at a time, and `register()` in `instrumentation.ts` sits
+// in front of the first request. Bundling the world cuts the module count for
+// that import from ~440 to ~250 and takes ~50ms off `register()`.
+const VERCEL_WORLD_DEPENDENCY_PACKAGES = [
+ QUEUE_PACKAGE,
+ ...VERCEL_WORLD_SERVER_EXTERNAL_PACKAGES,
+];
+// The workflow and step bundles are built by esbuild, which resolves the
+// dynamic import fine but gains nothing from inlining the world: those bundles
+// are loaded once per invocation either way.
+const WORKFLOW_BUNDLE_EXTERNAL_PACKAGES = [
VERCEL_WORLD_PACKAGE,
...VERCEL_WORLD_DEPENDENCY_PACKAGES,
];
@@ -235,6 +258,64 @@ function resolveNextVersion(workingDir: string): string {
);
}
+/**
+ * Whether the `@vercel/queue` copy in the app's dependency tree can be bundled.
+ * Fails open when no version can be read: the copy in the graph is then the one
+ * `@workflow/world-vercel` pins, which ships alongside this plugin.
+ */
+function isBundlableQueueVersion(workingDir: string): boolean {
+ const version = readInstalledQueueVersion(workingDir);
+ if (version === undefined || !semver.valid(version)) {
+ return true;
+ }
+ return semver.gte(version, QUEUE_MIN_BUNDLABLE_VERSION);
+}
+
+/**
+ * The installed `@vercel/queue` version, read from the nearest `node_modules`
+ * copy at or above the app directory.
+ *
+ * The manifest is read from disk rather than looked up with
+ * `require.resolve('@vercel/queue/package.json')`: the published package's
+ * `exports` map declares only `.`, so Node answers a manifest subpath with
+ * `ERR_PACKAGE_PATH_NOT_EXPORTED` even when the package is installed. Resolving
+ * the package entry instead is no better, because a bundler or a test runner can
+ * substitute a resolver that answers from the toolchain's module graph rather
+ * than the app's.
+ *
+ * Returns undefined for a layout that keeps the package out of a reachable
+ * `node_modules` directory, which includes any pnpm app that does not depend on
+ * the queue client directly. Those layouts cannot externalize it by name under
+ * webpack either, so the caller bundles it.
+ */
+function readInstalledQueueVersion(workingDir: string): string | undefined {
+ let directory = workingDir;
+ let previous = '';
+ while (directory !== previous) {
+ const manifestPath = join(
+ directory,
+ 'node_modules',
+ ...QUEUE_PACKAGE.split('/'),
+ 'package.json'
+ );
+ if (fileExists(manifestPath)) {
+ try {
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as {
+ version?: unknown;
+ };
+ return typeof manifest.version === 'string'
+ ? manifest.version
+ : undefined;
+ } catch {
+ return undefined;
+ }
+ }
+ previous = directory;
+ directory = dirname(directory);
+ }
+ return undefined;
+}
+
function fileExists(path: string): boolean {
try {
const stats = statSync(path);
@@ -412,10 +493,8 @@ export function withWorkflow(
nextConfig.serverExternalPackages = [
...new Set([
...(nextConfig.serverExternalPackages || []),
- // Keep the Vercel world and its native-prone dependencies external so
- // local builds do not try to parse @vercel/queue's keyring dependency
- // tree.
...VERCEL_WORLD_SERVER_EXTERNAL_PACKAGES,
+ ...(isBundlableQueueVersion(process.cwd()) ? [] : [QUEUE_PACKAGE]),
]),
];
const existingCompiler = nextConfig.compiler ?? {};
@@ -532,7 +611,10 @@ export function withWorkflow(
// See: https://nextjs.org/docs/app/getting-started/server-and-client-components
'server-only',
'client-only',
- ...effectiveServerExternalPackages,
+ ...new Set([
+ ...effectiveServerExternalPackages,
+ ...WORKFLOW_BUNDLE_EXTERNAL_PACKAGES,
+ ]),
],
});
})();
diff --git a/packages/world-vercel/src/queue.test.ts b/packages/world-vercel/src/queue.test.ts
index 8c65bb4238..efa74129c5 100644
--- a/packages/world-vercel/src/queue.test.ts
+++ b/packages/world-vercel/src/queue.test.ts
@@ -11,19 +11,9 @@ import {
const {
mockSend,
MockConsumerDiscoveryError,
- MockDuplicateMessageError,
MockQueueClient,
mockHandleCallback,
} = vi.hoisted(() => {
- class MockDuplicateMessageError extends Error {
- public readonly idempotencyKey?: string;
- constructor(message: string, idempotencyKey?: string) {
- super(message);
- this.name = 'DuplicateMessageError';
- this.idempotencyKey = idempotencyKey;
- }
- }
-
class MockConsumerDiscoveryError extends Error {
constructor(message: string) {
super(message);
@@ -46,7 +36,6 @@ const {
return {
mockSend,
MockConsumerDiscoveryError,
- MockDuplicateMessageError,
MockQueueClient,
mockHandleCallback,
};
@@ -55,7 +44,6 @@ const {
vi.mock('@vercel/queue', () => ({
QueueClient: MockQueueClient,
ConsumerDiscoveryError: MockConsumerDiscoveryError,
- DuplicateMessageError: MockDuplicateMessageError,
}));
vi.mock('./utils.js', () => ({
@@ -266,13 +254,10 @@ describe('createQueue', () => {
}
});
- it('should silently handle idempotency key conflicts', async () => {
- mockSend.mockRejectedValue(
- new MockDuplicateMessageError(
- 'Duplicate idempotency key detected',
- 'my-key'
- )
- );
+ it('returns the message id for a repeated idempotency key', async () => {
+ // Repeated keys are accepted and deduplicated after the send, so the
+ // caller sees an ordinary message id rather than a conflict.
+ mockSend.mockResolvedValue({ messageId: 'msg-456' });
const originalEnv = process.env.VERCEL_DEPLOYMENT_ID;
process.env.VERCEL_DEPLOYMENT_ID = 'dpl_test';
@@ -285,7 +270,12 @@ describe('createQueue', () => {
{ idempotencyKey: 'my-key' }
);
- expect(result.messageId).toBe('msg_duplicate_my-key');
+ expect(result.messageId).toBe('msg-456');
+ expect(mockSend).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.anything(),
+ expect.objectContaining({ idempotencyKey: 'my-key' })
+ );
} finally {
if (originalEnv !== undefined) {
process.env.VERCEL_DEPLOYMENT_ID = originalEnv;
diff --git a/packages/world-vercel/src/queue.ts b/packages/world-vercel/src/queue.ts
index bb5a96f7c8..cb662f6bdd 100644
--- a/packages/world-vercel/src/queue.ts
+++ b/packages/world-vercel/src/queue.ts
@@ -1,10 +1,6 @@
import { AsyncLocalStorage } from 'node:async_hooks';
import type { Transport } from '@vercel/queue';
-import {
- ConsumerDiscoveryError,
- DuplicateMessageError,
- QueueClient,
-} from '@vercel/queue';
+import { ConsumerDiscoveryError, QueueClient } from '@vercel/queue';
import {
MessageId,
type Queue,
@@ -483,34 +479,22 @@ export function createQueue(config?: APIConfig): Queue {
/[^A-Za-z0-9-_]/g,
'-'
);
- try {
- const { messageId } = await client.send(sanitizedQueueName, wrapper, {
- idempotencyKey: opts?.idempotencyKey,
- delaySeconds: opts?.delaySeconds,
- headers: {
- ...getHeadersFromPayload(payload),
- ...opts?.headers,
- },
- });
- return {
- // messageId may be null when VQS fails over to a different region —
- // the event is ingested but the responding region cannot return an ID.
- messageId: messageId ? MessageId.parse(messageId) : null,
- };
- } catch (error) {
- // Silently handle idempotency key conflicts - the message was already queued.
- // This matches the behavior of world-local and world-postgres.
- if (error instanceof DuplicateMessageError) {
- // Return a placeholder messageId since the original is not available from the error.
- // Callers using idempotency keys shouldn't depend on the returned messageId.
- return {
- messageId: MessageId.parse(
- `msg_duplicate_${error.idempotencyKey ?? opts?.idempotencyKey ?? 'unknown'}`
- ),
- };
- }
- throw error;
- }
+ // A repeated `idempotencyKey` is accepted rather than rejected: the send
+ // returns a fresh message ID and only one of the messages is delivered, so
+ // there is no conflict for the caller to handle here.
+ const { messageId } = await client.send(sanitizedQueueName, wrapper, {
+ idempotencyKey: opts?.idempotencyKey,
+ delaySeconds: opts?.delaySeconds,
+ headers: {
+ ...getHeadersFromPayload(payload),
+ ...opts?.headers,
+ },
+ });
+ return {
+ // messageId may be null when the queue fails over to a different region:
+ // the event is ingested but the responding region cannot return an ID.
+ messageId: messageId ? MessageId.parse(messageId) : null,
+ };
};
const createQueueHandler: Queue['createQueueHandler'] = (
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 462c3c4b08..e80390e1da 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -22,8 +22,8 @@ catalogs:
specifier: 3.2.0
version: 3.2.0
'@vercel/queue':
- specifier: 0.4.0
- version: 0.4.0
+ specifier: 0.5.0
+ version: 0.5.0
'@vitest/coverage-v8':
specifier: ^4.1.10
version: 4.1.10
@@ -62,6 +62,7 @@ catalogs:
version: 4.3.6
overrides:
+ '@vercel/queue>@vercel/oidc': 3.2.0
'@opentelemetry/api': 1.9.1
rfc6902: 5.1.2
devalue: 5.9.0
@@ -1367,7 +1368,7 @@ importers:
dependencies:
'@vercel/queue':
specifier: 'catalog:'
- version: 0.4.0(@opentelemetry/api@1.9.1)
+ version: 0.5.0(@opentelemetry/api@1.9.1)
'@workflow/errors':
specifier: workspace:*
version: link:../errors
@@ -1422,7 +1423,7 @@ importers:
dependencies:
'@vercel/queue':
specifier: 'catalog:'
- version: 0.4.0(@opentelemetry/api@1.9.1)
+ version: 0.5.0(@opentelemetry/api@1.9.1)
'@workflow/errors':
specifier: workspace:*
version: link:../errors
@@ -1569,7 +1570,7 @@ importers:
version: 3.2.0
'@vercel/queue':
specifier: 'catalog:'
- version: 0.4.0(@opentelemetry/api@1.9.1)
+ version: 0.5.0(@opentelemetry/api@1.9.1)
'@workflow/errors':
specifier: workspace:*
version: link:../errors
@@ -9833,8 +9834,8 @@ packages:
resolution: {integrity: sha512-6pjdXyNfdCQnj1nyeB1rPtll/XUhmciyeZJD0rpIUUwmcIfR+utDl6+iFvlHvsBdqAVT8UC6ydObT0v+xldfUQ==}
engines: {node: '>=20.0.0'}
- '@vercel/queue@0.4.0':
- resolution: {integrity: sha512-2HctPb2+6Pj1DMxbPohMq574rE9OP3fUAGhwV0R4Qv1FHQIR+qti92G8FGjFMJGUSFxVlWdNwVfgHU2hZlM6Dw==}
+ '@vercel/queue@0.5.0':
+ resolution: {integrity: sha512-TqvoMuhZK9Hw2yal6ee6EZ+wQw55+qPrpev6O6jJrpJMC1myrCrt7bLZvb9GQDIR35h40EiETVaMZT4eFWBVpA==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@opentelemetry/api': 1.9.1
@@ -10616,10 +10617,6 @@ packages:
brace-expansion@2.0.2:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
- brace-expansion@5.0.4:
- resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
- engines: {node: 18 || 20 || >=22}
-
brace-expansion@5.0.6:
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
engines: {node: 18 || 20 || >=22}
@@ -26294,13 +26291,13 @@ snapshots:
'@vercel/queue@0.3.1':
dependencies:
- '@vercel/oidc': 3.8.2
+ '@vercel/oidc': 3.2.0
minimatch: 10.2.5
mixpart: 0.0.6
picocolors: 1.1.1
optional: true
- '@vercel/queue@0.4.0(@opentelemetry/api@1.9.1)':
+ '@vercel/queue@0.5.0(@opentelemetry/api@1.9.1)':
dependencies:
'@vercel/oidc': 3.2.0
minimatch: 10.2.5
@@ -27601,10 +27598,6 @@ snapshots:
dependencies:
balanced-match: 1.0.2
- brace-expansion@5.0.4:
- dependencies:
- balanced-match: 4.0.4
-
brace-expansion@5.0.6:
dependencies:
balanced-match: 4.0.4
@@ -29591,7 +29584,7 @@ snapshots:
glob@13.0.6:
dependencies:
- minimatch: 10.2.4
+ minimatch: 10.2.5
minipass: 7.1.3
path-scurry: 2.0.2
@@ -31361,7 +31354,7 @@ snapshots:
minimatch@10.2.4:
dependencies:
- brace-expansion: 5.0.4
+ brace-expansion: 5.0.6
minimatch@10.2.5:
dependencies:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ba8c3d839c..3879de4bde 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -12,7 +12,7 @@ catalog:
"@types/node": 22.19.0
"@vercel/functions": ^3.8.0
"@vercel/oidc": 3.2.0
- "@vercel/queue": 0.4.0
+ "@vercel/queue": 0.5.0
"@vitest/coverage-v8": ^4.1.10
"@vitest/runner": ^4.1.10
ai: 6.0.116
@@ -30,6 +30,12 @@ onlyBuiltDependencies:
- esbuild
overrides:
+ # `@vercel/queue` accepts any `@vercel/oidc` in the 3.x line, and 3.3+ pulls in
+ # `@vercel/cli-config`, whose `xdg-app-paths` dependency instantiates itself at
+ # module scope and throws `The "path" argument must be of type string` once a
+ # bundler inlines it. Pin the queue client's copy to the last dependency-free
+ # release so a bundled queue client keeps working.
+ '@vercel/queue>@vercel/oidc': 3.2.0
# Single @opentelemetry/api instance workspace-wide: with two resolved
# copies (1.9.0 + 1.9.1), the copy that registers the tracer provider and
# the copy a package imports can differ, and the API's global-registration