Skip to content

Commit 52cf906

Browse files
Merge pull request #6106 from decentraland/release/release-20240226
release: release 20240226
2 parents e506293 + d22010f commit 52cf906

30 files changed

+360
-90
lines changed

browser-interface/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ packages/**/*.js.map
8181
test/**/*.js
8282
test/**/*.js.map
8383
dist
84+
!packages/shared/world/runtime-7/sourcemap/source-map@0.7.4.js
8485

8586
# ============== #
8687
# Local mappings #

browser-interface/package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

browser-interface/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"@dcl/legacy-ecs": "^6.11.11",
6666
"@dcl/protocol": "1.0.0-7716486147.commit-7433b10",
6767
"@dcl/rpc": "^1.1.1",
68-
"@dcl/scene-runtime": "7.0.6-20231206162622.commit-9ff48a9",
68+
"@dcl/scene-runtime": "7.0.6-20240220184109.commit-cf1e4e2",
6969
"@dcl/schemas": "^9.1.1",
7070
"@dcl/single-sign-on-client": "^0.1.0",
7171
"@dcl/urn-resolver": "^2.2.0",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import future from 'fp-future'
2+
3+
export async function injectScript(url: string) {
4+
const theFuture = future<Event>()
5+
const theScript = document.createElement('script')
6+
const persistMessage =
7+
'If this error persists, please try emptying the cache of your browser and reloading this page.'
8+
theScript.src = url
9+
theScript.async = true
10+
theScript.type = 'application/javascript'
11+
theScript.crossOrigin = 'anonymous'
12+
theScript.addEventListener('load', theFuture.resolve)
13+
theScript.addEventListener('error', (e) =>
14+
theFuture.reject(e.error || new Error(`The script ${url} failed to load.\n${persistMessage}`))
15+
)
16+
theScript.addEventListener('abort', () =>
17+
theFuture.reject(
18+
new Error(
19+
`Script loading aborted: ${url}.\nThis may be caused because you manually stopped the loading or because of a network error.\n${persistMessage}`
20+
)
21+
)
22+
)
23+
document.body.appendChild(theScript)
24+
return theFuture
25+
}

browser-interface/packages/shared/apis/host/DevTools.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ export function registerDevToolsServiceServerImplementation(port: RpcServerPort<
2121
const [payload] = params as ProtocolMapping.Events['Runtime.exceptionThrown']
2222

2323
if (payload.exceptionDetails.exception) {
24+
// If we have the sourcemaps loaded for the scene, then use it for gettin the correct stack trace.
25+
if (context.sourcemap && payload.exceptionDetails.exception.value) {
26+
try {
27+
const error = JSON.parse(payload.exceptionDetails.exception.value)
28+
if (error && error.stack) {
29+
const sourcemapError = context.sourcemap.parseError(error)
30+
context.logger.error(sourcemapError)
31+
break
32+
}
33+
} catch (_e) {}
34+
}
35+
2436
context.logger.error(
2537
payload.exceptionDetails.text,
2638
payload.exceptionDetails.exception.value || payload.exceptionDetails.exception.unserializableValue

browser-interface/packages/shared/apis/host/context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { RpcSceneControllerServiceDefinition } from 'shared/protocol/decent
77
import type { RpcClientModule } from '@dcl/rpc/dist/codegen'
88
import { EntityAction } from 'shared/protocol/decentraland/sdk/ecs6/engine_interface_ecs6.gen'
99
import { IInternalEngine } from '../../world/runtime-7/engine'
10+
import { Sourcemap } from '../../world/runtime-7/sourcemap/types'
1011

1112
type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }
1213

@@ -38,4 +39,7 @@ export type PortContext = {
3839

3940
// Internal engine used to store the user avatar's info
4041
internalEngine: IInternalEngine | undefined
42+
43+
// Parse sdk7 errors.
44+
sourcemap: Sourcemap | undefined
4145
}

browser-interface/packages/shared/world/SceneWorker.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getAssetBundlesBaseUrl,
1313
PIPE_SCENE_CONSOLE,
1414
playerHeight,
15+
PREVIEW,
1516
WSS_ENABLED
1617
} from 'config'
1718
import { gridToWorld } from 'lib/decentraland/parcels/gridToWorld'
@@ -42,6 +43,7 @@ import { joinBuffers } from 'lib/javascript/uint8arrays'
4243
import { nativeMsgBridge } from 'unity-interface/nativeMessagesBridge'
4344
import { _INTERNAL_WEB_TRANSPORT_ALLOC_SIZE } from 'renderer-protocol/transports/webTransport'
4445
import { createInternalEngine } from './runtime-7/engine'
46+
import { initSourcemap } from './runtime-7/sourcemap'
4547
import { forceStopScene } from './parcelSceneManager'
4648

4749
export enum SceneWorkerReadyState {
@@ -194,7 +196,8 @@ export class SceneWorker {
194196
readFile: this.readFile.bind(this),
195197
initialEntitiesTick0: Uint8Array.of(),
196198
hasMainCrdt: false,
197-
internalEngine: undefined
199+
internalEngine: undefined,
200+
sourcemap: undefined
198201
}
199202

200203
// if the scene metadata has a base parcel, then we set it as the position
@@ -230,7 +233,27 @@ export class SceneWorker {
230233
}
231234
}
232235

233-
async readFile(fileName: string) {
236+
async loadSourcemap() {
237+
try {
238+
// Only sdk7 scenes
239+
if (!this.rpcContext.sdk7) return
240+
241+
// Only preview or production scenes with the DEBUG_MOD or DEBUG_SCENE_LOG param
242+
if (!PREVIEW && !DEBUG_SCENE_LOG) return
243+
244+
const mainFile = PREVIEW
245+
? this.loadableScene.entity.metadata.main
246+
: `${this.loadableScene.entity.metadata.main}.map`
247+
const file = await this.readFile(mainFile, 'text')
248+
if (!file?.content) return
249+
return (await initSourcemap(file.content, PREVIEW)) ?? undefined
250+
} catch (_) {}
251+
}
252+
253+
async readFile<T extends 'text' | 'arraybuffer' = 'arraybuffer'>(
254+
fileName: string,
255+
type?: T
256+
): Promise<T extends 'text' ? { hash: string; content: string } : { hash: string; content: Uint8Array }> {
234257
// filenames are lower cased as per https://adr.decentraland.org/adr/ADR-80
235258
const normalized = fileName.toLowerCase()
236259

@@ -245,7 +268,11 @@ export class SceneWorker {
245268
const response = await fetch(url)
246269

247270
if (!response.ok) throw new Error(`Error fetching file ${file} from ${url}`)
248-
return { hash, content: new Uint8Array(await response.arrayBuffer()) }
271+
if (!type || type === 'arraybuffer') {
272+
return { hash, content: new Uint8Array(await response.arrayBuffer()) } as any
273+
}
274+
275+
return { hash, content: await response.text() } as any
249276
}
250277
}
251278

@@ -406,6 +433,7 @@ export class SceneWorker {
406433
this.metadata.scene.parcels,
407434
showAsPortableExperience
408435
)
436+
this.rpcContext.sourcemap = await this.loadSourcemap()
409437
}
410438
sceneEvents.emit(SCENE_LOAD, signalSceneLoad(this.loadableScene))
411439
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { SourceMapConsumerConstructor, Sourcemap } from './types'
2+
3+
import('./source-map@0.7.4.js')
4+
5+
let initialized = false
6+
declare const globalThis: {
7+
sourceMap: { SourceMapConsumer: SourceMapConsumerConstructor }
8+
}
9+
10+
async function getSourcemap() {
11+
if (initialized) return globalThis.sourceMap
12+
initialized = true
13+
14+
if (!global.sourceMap) {
15+
return
16+
}
17+
18+
globalThis.sourceMap.SourceMapConsumer.initialize({
19+
'lib/mappings.wasm': 'https://unpkg.com/source-map@0.7.4/lib/mappings.wasm'
20+
})
21+
22+
return globalThis.sourceMap
23+
}
24+
25+
export async function initSourcemap(code: string, inlineSourcemaps: boolean = true): Promise<Sourcemap | void> {
26+
const sourceMap = await getSourcemap()
27+
if (!sourceMap) return
28+
function decodeSourcemap(): any {
29+
// External source-maps .js.map file
30+
if (!inlineSourcemaps) {
31+
return code
32+
}
33+
// Inline sourcemap, find the source-map inside the file encoded in a base64
34+
const inlineSourceMapComment = code.match(/\/\/# sourceMappingURL=data:application\/json;base64,(.*)/)
35+
if (!inlineSourceMapComment || !inlineSourceMapComment[1]) return
36+
const decodedSourceMap = Buffer.from(inlineSourceMapComment[1], 'base64').toString('utf-8')
37+
return decodedSourceMap
38+
}
39+
const sourcemapCode = decodeSourcemap()
40+
if (!sourcemapCode) return
41+
const sourcemapConsumer = await new sourceMap.SourceMapConsumer(sourcemapCode)
42+
43+
/**
44+
* Because the scene-runtime uses an eval with a Function, it generates an offset of :2
45+
* in every error. So we need to fix that in the error code stack.
46+
*/
47+
function adjustStackTrace(stackTrace: string) {
48+
const lines = stackTrace.split('\n')
49+
const adjustedLines = lines.map((line) => {
50+
// Check if the line contains a line number
51+
const match = line.match(/:(\d+):(\d+)\)$/)
52+
if (match) {
53+
const lineNumber = parseInt(match[1], 10)
54+
const adjustedLineNumber = lineNumber - 2 // Add 2 to each line number
55+
return line.replace(`:${lineNumber}:`, `:${adjustedLineNumber}:`)
56+
}
57+
return line
58+
})
59+
return adjustedLines.join('\n')
60+
}
61+
62+
function parseError(error: Error) {
63+
if (!error.stack || !sourcemapConsumer) {
64+
return error
65+
}
66+
const stack = adjustStackTrace(error.stack.toString()).split('\n')
67+
const mappedStackTrace = stack.map((frame, index) => {
68+
// Show the error message
69+
if (index === 0) return frame
70+
71+
// Fix all anonymous errors
72+
const match = frame.match(/<anonymous>:(\d+):(\d+)/)
73+
if (match) {
74+
const [, line, column] = match
75+
const originalPosition = sourcemapConsumer.originalPositionFor({
76+
line: parseInt(line, 10),
77+
column: parseInt(column, 10)
78+
})
79+
if (originalPosition.source) {
80+
const fileName = ` (${originalPosition.source}:${originalPosition.line}:${originalPosition.column})`
81+
return frame.replace(/ \(eval.*$/, fileName)
82+
}
83+
}
84+
return ''
85+
})
86+
return mappedStackTrace.join('\n')
87+
}
88+
return { parseError }
89+
}

browser-interface/packages/shared/world/runtime-7/sourcemap/source-map@0.7.4.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export type RawSourceMap = {
2+
version: number
3+
sources: string[]
4+
names: string[]
5+
sourceRoot?: string
6+
sourcesContent?: string[]
7+
mappings: string
8+
file: string
9+
}
10+
export type BasicSourceMapConsumer = SourceMapConsumer & {
11+
file: string
12+
sourceRoot: string
13+
sources: string[]
14+
sourcesContent: string[]
15+
}
16+
17+
export type SourceMapConsumerConstructor = {
18+
prototype: SourceMapConsumer
19+
initialize(keys: { [key: string]: string }): void
20+
new (rawSourceMap: RawSourceMap, sourceMapUrl?: string): Promise<BasicSourceMapConsumer>
21+
}
22+
23+
export type SourceMapConsumer = {
24+
originalPositionFor(generatedPosition: { line: number; column: number }): {
25+
source: string | null
26+
line: number | null
27+
column: number | null
28+
name: string | null
29+
}
30+
}
31+
32+
export type Sourcemap = {
33+
parseError(error: Error): string | Error
34+
}

0 commit comments

Comments
 (0)