@glasstrace/sdk 0.4.2 → 0.7.0
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.
- package/dist/{chunk-TVOYTJ7I.js → chunk-QW6W4CSA.js} +48 -2
- package/dist/{chunk-TVOYTJ7I.js.map → chunk-QW6W4CSA.js.map} +1 -1
- package/dist/{chunk-67H2JEI4.js → chunk-SALPGSWK.js} +2 -2
- package/dist/cli/init.cjs +46 -2
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.js +2 -2
- package/dist/cli/mcp-add.cjs +45 -1
- package/dist/cli/mcp-add.cjs.map +1 -1
- package/dist/cli/mcp-add.js +1 -1
- package/dist/index.cjs +243 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -2
- package/dist/index.d.ts +67 -2
- package/dist/index.js +199 -16
- package/dist/index.js.map +1 -1
- package/package.json +7 -2
- /package/dist/{chunk-67H2JEI4.js.map → chunk-SALPGSWK.js.map} +0 -0
package/dist/cli/mcp-add.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -1617,6 +1617,7 @@ var src_exports = {};
|
|
|
1617
1617
|
__export(src_exports, {
|
|
1618
1618
|
GlasstraceExporter: () => GlasstraceExporter,
|
|
1619
1619
|
GlasstraceSpanProcessor: () => GlasstraceSpanProcessor,
|
|
1620
|
+
PRESIGNED_THRESHOLD_BYTES: () => PRESIGNED_THRESHOLD_BYTES,
|
|
1620
1621
|
SdkError: () => SdkError,
|
|
1621
1622
|
SessionManager: () => SessionManager,
|
|
1622
1623
|
buildImportGraph: () => buildImportGraph,
|
|
@@ -1644,6 +1645,8 @@ __export(src_exports, {
|
|
|
1644
1645
|
saveCachedConfig: () => saveCachedConfig,
|
|
1645
1646
|
sendInitRequest: () => sendInitRequest,
|
|
1646
1647
|
uploadSourceMaps: () => uploadSourceMaps,
|
|
1648
|
+
uploadSourceMapsAuto: () => uploadSourceMapsAuto,
|
|
1649
|
+
uploadSourceMapsPresigned: () => uploadSourceMapsPresigned,
|
|
1647
1650
|
withGlasstraceConfig: () => withGlasstraceConfig
|
|
1648
1651
|
});
|
|
1649
1652
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -15556,7 +15559,12 @@ var SdkInitResponseSchema = external_exports.object({
|
|
|
15556
15559
|
linkedAccountId: external_exports.string().uuid().optional(),
|
|
15557
15560
|
minimumSdkVersion: external_exports.string(),
|
|
15558
15561
|
apiVersion: external_exports.string(),
|
|
15559
|
-
tierLimits: TierLimitsSchema
|
|
15562
|
+
tierLimits: TierLimitsSchema,
|
|
15563
|
+
claimResult: external_exports.object({
|
|
15564
|
+
newApiKey: DevApiKeySchema,
|
|
15565
|
+
accountId: external_exports.string().uuid(),
|
|
15566
|
+
graceExpiresAt: external_exports.number().int().positive()
|
|
15567
|
+
}).optional()
|
|
15560
15568
|
});
|
|
15561
15569
|
var DiscoveryResponseSchema = external_exports.object({
|
|
15562
15570
|
key: AnonApiKeySchema,
|
|
@@ -15568,6 +15576,45 @@ var SourceMapUploadResponseSchema = external_exports.object({
|
|
|
15568
15576
|
fileCount: external_exports.number().int().nonnegative(),
|
|
15569
15577
|
totalSizeBytes: external_exports.number().int().nonnegative()
|
|
15570
15578
|
});
|
|
15579
|
+
var PresignedUploadRequestSchema = external_exports.object({
|
|
15580
|
+
buildHash: BuildHashSchema,
|
|
15581
|
+
files: external_exports.array(
|
|
15582
|
+
external_exports.object({
|
|
15583
|
+
filePath: external_exports.string().min(1),
|
|
15584
|
+
sizeBytes: external_exports.number().int().positive()
|
|
15585
|
+
})
|
|
15586
|
+
).min(1).max(100)
|
|
15587
|
+
});
|
|
15588
|
+
var PresignedUploadResponseSchema = external_exports.object({
|
|
15589
|
+
uploadId: external_exports.string().uuid(),
|
|
15590
|
+
expiresAt: external_exports.number().int().positive(),
|
|
15591
|
+
files: external_exports.array(
|
|
15592
|
+
external_exports.object({
|
|
15593
|
+
filePath: external_exports.string().min(1),
|
|
15594
|
+
clientToken: external_exports.string().min(1),
|
|
15595
|
+
pathname: external_exports.string().min(1),
|
|
15596
|
+
maxBytes: external_exports.number().int().positive()
|
|
15597
|
+
})
|
|
15598
|
+
).min(1).max(100)
|
|
15599
|
+
});
|
|
15600
|
+
var SourceMapManifestRequestSchema = external_exports.object({
|
|
15601
|
+
uploadId: external_exports.string().uuid(),
|
|
15602
|
+
buildHash: BuildHashSchema,
|
|
15603
|
+
files: external_exports.array(
|
|
15604
|
+
external_exports.object({
|
|
15605
|
+
filePath: external_exports.string().min(1),
|
|
15606
|
+
sizeBytes: external_exports.number().int().positive(),
|
|
15607
|
+
blobUrl: external_exports.string().url()
|
|
15608
|
+
})
|
|
15609
|
+
).min(1).max(100)
|
|
15610
|
+
});
|
|
15611
|
+
var SourceMapManifestResponseSchema = external_exports.object({
|
|
15612
|
+
success: external_exports.literal(true),
|
|
15613
|
+
buildHash: BuildHashSchema,
|
|
15614
|
+
fileCount: external_exports.number().int().nonnegative(),
|
|
15615
|
+
totalSizeBytes: external_exports.number().int().nonnegative(),
|
|
15616
|
+
activatedAt: external_exports.number().int().positive()
|
|
15617
|
+
});
|
|
15571
15618
|
var GLASSTRACE_ATTRIBUTE_NAMES = {
|
|
15572
15619
|
// Server-side attributes
|
|
15573
15620
|
TRACE_TYPE: "glasstrace.trace.type",
|
|
@@ -15864,13 +15911,13 @@ async function sendInitRequest(config2, anonKey, sdkVersion, importGraph, health
|
|
|
15864
15911
|
async function performInit(config2, anonKey, sdkVersion) {
|
|
15865
15912
|
if (rateLimitBackoff) {
|
|
15866
15913
|
rateLimitBackoff = false;
|
|
15867
|
-
return;
|
|
15914
|
+
return null;
|
|
15868
15915
|
}
|
|
15869
15916
|
try {
|
|
15870
15917
|
const effectiveKey = config2.apiKey || anonKey;
|
|
15871
15918
|
if (!effectiveKey) {
|
|
15872
15919
|
console.warn("[glasstrace] No API key available for init request.");
|
|
15873
|
-
return;
|
|
15920
|
+
return null;
|
|
15874
15921
|
}
|
|
15875
15922
|
const controller = new AbortController();
|
|
15876
15923
|
const timeoutId = setTimeout(() => controller.abort(), INIT_TIMEOUT_MS);
|
|
@@ -15887,45 +15934,61 @@ async function performInit(config2, anonKey, sdkVersion) {
|
|
|
15887
15934
|
clearTimeout(timeoutId);
|
|
15888
15935
|
currentConfig = result;
|
|
15889
15936
|
await saveCachedConfig(result);
|
|
15937
|
+
if (result.claimResult) {
|
|
15938
|
+
try {
|
|
15939
|
+
process.stderr.write(
|
|
15940
|
+
`[glasstrace] Account claimed! Update GLASSTRACE_API_KEY=${result.claimResult.newApiKey} in your .env file.
|
|
15941
|
+
`
|
|
15942
|
+
);
|
|
15943
|
+
} catch (logErr) {
|
|
15944
|
+
console.warn(
|
|
15945
|
+
`[glasstrace] Failed to write claim migration message: ${logErr instanceof Error ? logErr.message : String(logErr)}`
|
|
15946
|
+
);
|
|
15947
|
+
}
|
|
15948
|
+
return { claimResult: result.claimResult };
|
|
15949
|
+
}
|
|
15950
|
+
return null;
|
|
15890
15951
|
} catch (err) {
|
|
15891
15952
|
clearTimeout(timeoutId);
|
|
15892
15953
|
if (err instanceof DOMException && err.name === "AbortError") {
|
|
15893
15954
|
console.warn("[glasstrace] ingestion_unreachable: Init request timed out.");
|
|
15894
|
-
return;
|
|
15955
|
+
return null;
|
|
15895
15956
|
}
|
|
15896
15957
|
const status = err.status;
|
|
15897
15958
|
if (status === 401) {
|
|
15898
15959
|
console.warn(
|
|
15899
15960
|
"[glasstrace] ingestion_auth_failed: Check your GLASSTRACE_API_KEY."
|
|
15900
15961
|
);
|
|
15901
|
-
return;
|
|
15962
|
+
return null;
|
|
15902
15963
|
}
|
|
15903
15964
|
if (status === 429) {
|
|
15904
15965
|
console.warn("[glasstrace] ingestion_rate_limited: Backing off.");
|
|
15905
15966
|
rateLimitBackoff = true;
|
|
15906
|
-
return;
|
|
15967
|
+
return null;
|
|
15907
15968
|
}
|
|
15908
15969
|
if (typeof status === "number" && status >= 400) {
|
|
15909
15970
|
console.warn(
|
|
15910
15971
|
`[glasstrace] Init request failed with status ${status}. Using cached config.`
|
|
15911
15972
|
);
|
|
15912
|
-
return;
|
|
15973
|
+
return null;
|
|
15913
15974
|
}
|
|
15914
15975
|
if (err instanceof Error && err.name === "ZodError") {
|
|
15915
15976
|
console.warn(
|
|
15916
15977
|
"[glasstrace] Init response failed validation (schema version mismatch?). Using cached config."
|
|
15917
15978
|
);
|
|
15918
|
-
return;
|
|
15979
|
+
return null;
|
|
15919
15980
|
}
|
|
15920
15981
|
console.warn(
|
|
15921
15982
|
`[glasstrace] ingestion_unreachable: ${err instanceof Error ? err.message : String(err)}`
|
|
15922
15983
|
);
|
|
15984
|
+
return null;
|
|
15923
15985
|
}
|
|
15924
15986
|
} catch (err) {
|
|
15925
15987
|
console.warn(
|
|
15926
15988
|
`[glasstrace] Unexpected init error: ${err instanceof Error ? err.message : String(err)}`
|
|
15927
15989
|
);
|
|
15928
15990
|
}
|
|
15991
|
+
return null;
|
|
15929
15992
|
}
|
|
15930
15993
|
function getActiveConfig() {
|
|
15931
15994
|
if (currentConfig) {
|
|
@@ -19066,6 +19129,14 @@ async function installConsoleCapture() {
|
|
|
19066
19129
|
}
|
|
19067
19130
|
};
|
|
19068
19131
|
}
|
|
19132
|
+
function sdkLog(level, message) {
|
|
19133
|
+
isGlasstraceLog = true;
|
|
19134
|
+
try {
|
|
19135
|
+
console[level](message);
|
|
19136
|
+
} finally {
|
|
19137
|
+
isGlasstraceLog = false;
|
|
19138
|
+
}
|
|
19139
|
+
}
|
|
19069
19140
|
|
|
19070
19141
|
// src/register.ts
|
|
19071
19142
|
var consoleCaptureInstalled = false;
|
|
@@ -19155,7 +19226,11 @@ function registerGlasstrace(options) {
|
|
|
19155
19226
|
if (config2.verbose) {
|
|
19156
19227
|
console.info("[glasstrace] Background init firing.");
|
|
19157
19228
|
}
|
|
19158
|
-
await performInit(config2, anonKey, "0.
|
|
19229
|
+
const initResult = await performInit(config2, anonKey, "0.7.0");
|
|
19230
|
+
if (initResult?.claimResult) {
|
|
19231
|
+
setResolvedApiKey(initResult.claimResult.newApiKey);
|
|
19232
|
+
notifyApiKeyResolved();
|
|
19233
|
+
}
|
|
19159
19234
|
maybeInstallConsoleCapture();
|
|
19160
19235
|
} catch (err) {
|
|
19161
19236
|
console.warn(
|
|
@@ -19175,7 +19250,11 @@ function registerGlasstrace(options) {
|
|
|
19175
19250
|
if (config2.verbose) {
|
|
19176
19251
|
console.info("[glasstrace] Background init firing.");
|
|
19177
19252
|
}
|
|
19178
|
-
await performInit(config2, anonKey, "0.
|
|
19253
|
+
const initResult = await performInit(config2, anonKey, "0.7.0");
|
|
19254
|
+
if (initResult?.claimResult) {
|
|
19255
|
+
setResolvedApiKey(initResult.claimResult.newApiKey);
|
|
19256
|
+
notifyApiKeyResolved();
|
|
19257
|
+
}
|
|
19179
19258
|
maybeInstallConsoleCapture();
|
|
19180
19259
|
} catch (err) {
|
|
19181
19260
|
console.warn(
|
|
@@ -19197,7 +19276,7 @@ function registerGlasstrace(options) {
|
|
|
19197
19276
|
if (config2.verbose) {
|
|
19198
19277
|
console.info("[glasstrace] Background init firing.");
|
|
19199
19278
|
}
|
|
19200
|
-
await performInit(config2, anonKeyForInit, "0.
|
|
19279
|
+
await performInit(config2, anonKeyForInit, "0.7.0");
|
|
19201
19280
|
maybeInstallConsoleCapture();
|
|
19202
19281
|
} catch (err) {
|
|
19203
19282
|
console.warn(
|
|
@@ -19296,10 +19375,7 @@ async function uploadSourceMaps(apiKey, endpoint, buildHash, maps) {
|
|
|
19296
19375
|
sourceMap: m.content
|
|
19297
19376
|
}))
|
|
19298
19377
|
};
|
|
19299
|
-
|
|
19300
|
-
while (baseUrl.endsWith("/")) {
|
|
19301
|
-
baseUrl = baseUrl.slice(0, -1);
|
|
19302
|
-
}
|
|
19378
|
+
const baseUrl = stripTrailingSlashes(endpoint);
|
|
19303
19379
|
const response = await fetch(`${baseUrl}/v1/source-maps`, {
|
|
19304
19380
|
method: "POST",
|
|
19305
19381
|
headers: {
|
|
@@ -19320,6 +19396,155 @@ async function uploadSourceMaps(apiKey, endpoint, buildHash, maps) {
|
|
|
19320
19396
|
const json2 = await response.json();
|
|
19321
19397
|
return SourceMapUploadResponseSchema.parse(json2);
|
|
19322
19398
|
}
|
|
19399
|
+
var PRESIGNED_THRESHOLD_BYTES = 45e5;
|
|
19400
|
+
function stripTrailingSlashes(url2) {
|
|
19401
|
+
let result = url2;
|
|
19402
|
+
while (result.endsWith("/")) {
|
|
19403
|
+
result = result.slice(0, -1);
|
|
19404
|
+
}
|
|
19405
|
+
return result;
|
|
19406
|
+
}
|
|
19407
|
+
async function requestPresignedTokens(apiKey, endpoint, buildHash, files) {
|
|
19408
|
+
const baseUrl = stripTrailingSlashes(endpoint);
|
|
19409
|
+
const response = await fetch(`${baseUrl}/v1/source-maps/presign`, {
|
|
19410
|
+
method: "POST",
|
|
19411
|
+
headers: {
|
|
19412
|
+
"Content-Type": "application/json",
|
|
19413
|
+
Authorization: `Bearer ${apiKey}`
|
|
19414
|
+
},
|
|
19415
|
+
body: JSON.stringify({ buildHash, files })
|
|
19416
|
+
});
|
|
19417
|
+
if (!response.ok) {
|
|
19418
|
+
try {
|
|
19419
|
+
await response.text();
|
|
19420
|
+
} catch {
|
|
19421
|
+
}
|
|
19422
|
+
throw new Error(
|
|
19423
|
+
`Presigned token request failed: ${String(response.status)} ${response.statusText}`
|
|
19424
|
+
);
|
|
19425
|
+
}
|
|
19426
|
+
const json2 = await response.json();
|
|
19427
|
+
return PresignedUploadResponseSchema.parse(json2);
|
|
19428
|
+
}
|
|
19429
|
+
async function uploadToBlob(clientToken, pathname, content) {
|
|
19430
|
+
let mod;
|
|
19431
|
+
try {
|
|
19432
|
+
mod = await import("@vercel/blob/client");
|
|
19433
|
+
} catch (err) {
|
|
19434
|
+
const code = err.code;
|
|
19435
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
19436
|
+
throw new Error(
|
|
19437
|
+
"Presigned upload requires @vercel/blob. Install it: npm install @vercel/blob"
|
|
19438
|
+
);
|
|
19439
|
+
}
|
|
19440
|
+
throw err;
|
|
19441
|
+
}
|
|
19442
|
+
const result = await mod.put(pathname, new Blob([content]), {
|
|
19443
|
+
access: "public",
|
|
19444
|
+
token: clientToken
|
|
19445
|
+
});
|
|
19446
|
+
return { url: result.url, size: Buffer.byteLength(content, "utf-8") };
|
|
19447
|
+
}
|
|
19448
|
+
async function submitManifest(apiKey, endpoint, uploadId, buildHash, files) {
|
|
19449
|
+
const baseUrl = stripTrailingSlashes(endpoint);
|
|
19450
|
+
const response = await fetch(`${baseUrl}/v1/source-maps/manifest`, {
|
|
19451
|
+
method: "POST",
|
|
19452
|
+
headers: {
|
|
19453
|
+
"Content-Type": "application/json",
|
|
19454
|
+
Authorization: `Bearer ${apiKey}`
|
|
19455
|
+
},
|
|
19456
|
+
body: JSON.stringify({ uploadId, buildHash, files })
|
|
19457
|
+
});
|
|
19458
|
+
if (!response.ok) {
|
|
19459
|
+
try {
|
|
19460
|
+
await response.text();
|
|
19461
|
+
} catch {
|
|
19462
|
+
}
|
|
19463
|
+
throw new Error(
|
|
19464
|
+
`Source map manifest submission failed: ${String(response.status)} ${response.statusText}`
|
|
19465
|
+
);
|
|
19466
|
+
}
|
|
19467
|
+
const json2 = await response.json();
|
|
19468
|
+
return SourceMapManifestResponseSchema.parse(json2);
|
|
19469
|
+
}
|
|
19470
|
+
async function uploadSourceMapsPresigned(apiKey, endpoint, buildHash, maps, blobUploader = uploadToBlob) {
|
|
19471
|
+
if (maps.length === 0) {
|
|
19472
|
+
throw new Error("No source maps to upload");
|
|
19473
|
+
}
|
|
19474
|
+
const presigned = await requestPresignedTokens(
|
|
19475
|
+
apiKey,
|
|
19476
|
+
endpoint,
|
|
19477
|
+
buildHash,
|
|
19478
|
+
maps.map((m) => ({
|
|
19479
|
+
filePath: m.filePath,
|
|
19480
|
+
sizeBytes: Buffer.byteLength(m.content, "utf-8")
|
|
19481
|
+
}))
|
|
19482
|
+
);
|
|
19483
|
+
const mapsByPath = new Map(maps.map((m) => [m.filePath, m]));
|
|
19484
|
+
if (mapsByPath.size !== maps.length) {
|
|
19485
|
+
throw new Error("Duplicate filePath entries in source maps");
|
|
19486
|
+
}
|
|
19487
|
+
for (const token of presigned.files) {
|
|
19488
|
+
if (!mapsByPath.has(token.filePath)) {
|
|
19489
|
+
throw new Error(
|
|
19490
|
+
`Presigned token for "${token.filePath}" has no matching source map entry`
|
|
19491
|
+
);
|
|
19492
|
+
}
|
|
19493
|
+
}
|
|
19494
|
+
const CONCURRENCY = 5;
|
|
19495
|
+
const uploadResults = [];
|
|
19496
|
+
for (let i = 0; i < presigned.files.length; i += CONCURRENCY) {
|
|
19497
|
+
const chunk = presigned.files.slice(i, i + CONCURRENCY);
|
|
19498
|
+
const chunkResults = await Promise.all(
|
|
19499
|
+
chunk.map(async (token) => {
|
|
19500
|
+
const entry = mapsByPath.get(token.filePath);
|
|
19501
|
+
const result = await blobUploader(token.clientToken, token.pathname, entry.content);
|
|
19502
|
+
return {
|
|
19503
|
+
filePath: token.filePath,
|
|
19504
|
+
sizeBytes: result.size,
|
|
19505
|
+
blobUrl: result.url
|
|
19506
|
+
};
|
|
19507
|
+
})
|
|
19508
|
+
);
|
|
19509
|
+
uploadResults.push(...chunkResults);
|
|
19510
|
+
}
|
|
19511
|
+
return submitManifest(apiKey, endpoint, presigned.uploadId, buildHash, uploadResults);
|
|
19512
|
+
}
|
|
19513
|
+
async function uploadSourceMapsAuto(apiKey, endpoint, buildHash, maps, options) {
|
|
19514
|
+
if (maps.length === 0) {
|
|
19515
|
+
throw new Error("No source maps to upload");
|
|
19516
|
+
}
|
|
19517
|
+
const totalBytes = maps.reduce(
|
|
19518
|
+
(sum, m) => sum + Buffer.byteLength(m.content, "utf-8"),
|
|
19519
|
+
0
|
|
19520
|
+
);
|
|
19521
|
+
if (totalBytes < PRESIGNED_THRESHOLD_BYTES) {
|
|
19522
|
+
return uploadSourceMaps(apiKey, endpoint, buildHash, maps);
|
|
19523
|
+
}
|
|
19524
|
+
const checkAvailable = options?.checkBlobAvailable ?? (async () => {
|
|
19525
|
+
try {
|
|
19526
|
+
await import("@vercel/blob/client");
|
|
19527
|
+
return true;
|
|
19528
|
+
} catch {
|
|
19529
|
+
return false;
|
|
19530
|
+
}
|
|
19531
|
+
});
|
|
19532
|
+
const blobAvailable = await checkAvailable();
|
|
19533
|
+
if (blobAvailable) {
|
|
19534
|
+
return uploadSourceMapsPresigned(
|
|
19535
|
+
apiKey,
|
|
19536
|
+
endpoint,
|
|
19537
|
+
buildHash,
|
|
19538
|
+
maps,
|
|
19539
|
+
options?.blobUploader
|
|
19540
|
+
);
|
|
19541
|
+
}
|
|
19542
|
+
sdkLog(
|
|
19543
|
+
"warn",
|
|
19544
|
+
`[glasstrace] Build exceeds 4.5MB (${totalBytes} bytes). Install @vercel/blob for presigned uploads to avoid serverless body size limits. Falling back to legacy upload.`
|
|
19545
|
+
);
|
|
19546
|
+
return uploadSourceMaps(apiKey, endpoint, buildHash, maps);
|
|
19547
|
+
}
|
|
19323
19548
|
|
|
19324
19549
|
// src/config-wrapper.ts
|
|
19325
19550
|
function withGlasstraceConfig(nextConfig) {
|
|
@@ -19609,6 +19834,7 @@ async function buildImportGraph(projectRoot) {
|
|
|
19609
19834
|
0 && (module.exports = {
|
|
19610
19835
|
GlasstraceExporter,
|
|
19611
19836
|
GlasstraceSpanProcessor,
|
|
19837
|
+
PRESIGNED_THRESHOLD_BYTES,
|
|
19612
19838
|
SdkError,
|
|
19613
19839
|
SessionManager,
|
|
19614
19840
|
buildImportGraph,
|
|
@@ -19636,6 +19862,8 @@ async function buildImportGraph(projectRoot) {
|
|
|
19636
19862
|
saveCachedConfig,
|
|
19637
19863
|
sendInitRequest,
|
|
19638
19864
|
uploadSourceMaps,
|
|
19865
|
+
uploadSourceMapsAuto,
|
|
19866
|
+
uploadSourceMapsPresigned,
|
|
19639
19867
|
withGlasstraceConfig
|
|
19640
19868
|
});
|
|
19641
19869
|
//# sourceMappingURL=index.cjs.map
|