Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ steps:
exit "$$PREP_STATUS"
fi
make build REFRESH_JS_BUILD=1
# Vite emits `.map` files into `dist/` for Sentry symbolication.
# Zip them (preserving the `assets/` layout the host uploads under),
# then strip them from `dist/` BEFORE tarring: the source maps must
# never ship inside the app bundles, and some downstream steps (e.g.
# the Android publish) copy `dist/` directly rather than through the
# map-stripping Makefile targets. Stripping here makes `dist.tar.gz`
# map-free for every consumer.
echo "--- :sentry: Packaging source maps"
# Include both the minified `.js` and their `.map` files: Sentry's
# Debug ID symbolication needs the minified source alongside the map
# (uploading only maps yields `js_no_source`/`missing_source`). Both
# carry the same injected Debug ID.
( cd dist && zip -r -q ../GutenbergKitSourceMaps.zip . -i '*.js' '*.map' )
buildkite-agent artifact upload GutenbergKitSourceMaps.zip
find dist -name '*.map' -delete
tar -czf dist.tar.gz dist/
buildkite-agent artifact upload dist.tar.gz
plugins: &plugins
Expand Down Expand Up @@ -149,6 +164,9 @@ steps:
command: |
buildkite-agent artifact download '*.xcframework.zip' .
buildkite-agent artifact download '*.xcframework.zip.checksum.txt' .
# Source maps are optional: `publish_to_s3` skips the upload when the
# zip is absent, so don't fail the step if the artifact isn't found.
buildkite-agent artifact download GutenbergKitSourceMaps.zip . || true
install_gems
bundle exec fastlane publish_to_s3 version:$BUILDKITE_COMMIT
plugins: *plugins
Expand Down
5 changes: 5 additions & 0 deletions .buildkite/publish-pr-xcframework.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ echo '--- :arrow_down: Downloading XCFramework artifacts'
buildkite-agent artifact download '*.xcframework.zip' . --step "build-xcframework"
buildkite-agent artifact download '*.xcframework.zip.checksum.txt' . --step "build-xcframework"

# Source maps are optional: `publish_to_s3` skips the upload when the zip is
# absent, so tolerate a missing artifact (e.g. a build that emitted no maps).
echo '--- :arrow_down: Downloading source maps'
buildkite-agent artifact download GutenbergKitSourceMaps.zip . --step "build-react" || true

echo '--- :rubygems: Setting up Gems'
install_gems

Expand Down
5 changes: 5 additions & 0 deletions .buildkite/release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ echo '--- :arrow_down: Downloading XCFramework artifacts'
buildkite-agent artifact download '*.xcframework.zip' . --step "build-xcframework"
buildkite-agent artifact download '*.xcframework.zip.checksum.txt' . --step "build-xcframework"

# Source maps are optional: `publish_to_s3` skips the upload when the zip is
# absent, so tolerate a missing artifact (e.g. a build that emitted no maps).
echo '--- :arrow_down: Downloading source maps'
buildkite-agent artifact download GutenbergKitSourceMaps.zip . --step "build-react" || true

echo '--- :rubygems: Setting up Gems'
install_gems

Expand Down
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -132,24 +132,50 @@ build: prep-translations ## Build the project for all platforms (iOS, Android, w
$(MAKE) _RECURSIVE_INVOKE=1 npm-dependencies && \
echo "--- :node: Building Gutenberg" && \
npm run build && \
$(MAKE) inject-sourcemap-debug-ids && \
echo "--- :open_file_folder: Copying Build Products into place" && \
$(MAKE) copy-dist-ios && \
$(MAKE) copy-dist-android; \
else \
echo "--- :white_check_mark: Skipping JS build (dist already exists). Use REFRESH_JS_BUILD=1 to force refresh."; \
fi

# Inject Sentry Debug IDs into the built JS and its source maps. This runs
# BETWEEN `npm run build` and the `copy-dist-*` targets so the `//# debugId=`
# comment lands in `dist/` and is copied into the shipped bundles, while the
# matching `debug_id` is written into the `.map` files that get uploaded to
# Sentry. Debug IDs let Sentry match maps to events regardless of the (unstable,
# per-install) on-device file paths — the only reliable approach for GutenbergKit's
# WebView, where path-based matching fails.
#
# `@sentry/cli sourcemaps inject` only rewrites files with adjacent maps; it needs
# no auth token (uploading, which does, stays host-side). The injected UUIDs are
# deterministic (content-hashed), so builds remain reproducible.
.PHONY: inject-sourcemap-debug-ids
inject-sourcemap-debug-ids:
@echo "--- :sentry: Injecting source map Debug IDs"
@npx sentry-cli sourcemaps inject dist

# The `find ... -delete` after each copy strips source maps from the native
# bundles. Maps are emitted into `dist/` (see `vite.config.js`) so build tooling
# can deliver them to Sentry, but they must NOT ship inside the app: they'd bloat
# the binary and expose de-minified source. `dist/` itself keeps its maps for the
# upload step. Stripping here (rather than only in CI) covers every consumer of
# these targets — local `make build`, CI, and `bin/release.sh` (which commits the
# iOS bundle via `git add .`).
.PHONY: copy-dist-ios
copy-dist-ios:
@rm -rf ./ios/Sources/GutenbergKitResources/Gutenberg/
@mkdir -p ./ios/Sources/GutenbergKitResources/Gutenberg
@cp -r ./dist/. ./ios/Sources/GutenbergKitResources/Gutenberg/
@find ./ios/Sources/GutenbergKitResources/Gutenberg -name '*.map' -delete
@touch ./ios/Sources/GutenbergKitResources/Gutenberg/.gitkeep

.PHONY: copy-dist-android
copy-dist-android:
@rm -rf ./android/Gutenberg/src/main/assets/
@cp -r ./dist/. ./android/Gutenberg/src/main/assets
@find ./android/Gutenberg/src/main/assets -name '*.map' -delete

.PHONY: build-swift-package
build-swift-package: build ## Build the Swift package for iOS
Expand Down
28 changes: 28 additions & 0 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ lane :publish_to_s3 do |options|
auto_prefix: false,
if_exists: :replace
)

# Publish the JS source maps alongside the XCFramework so host apps can fetch
# them (by GutenbergKit version) and upload them to Sentry for exception
# symbolication. The maps are NOT bundled inside the XCFramework — SwiftPM
# binary-target consumers don't surface sibling files to the host build — so
# they ship as a separate object under the same versioned key prefix.
# Optional: older builds (or a build that emitted no maps) simply skip this.
sourcemaps = sourcemaps_file_path
if sourcemaps
upload_to_s3(
bucket: 'a8c-apps-public-artifacts',
key: File.join('gutenbergkit', version, 'GutenbergKitSourceMaps.zip'),
file: sourcemaps,
auto_prefix: false,
if_exists: :replace
)
else
UI.important('No GutenbergKitSourceMaps.zip found; skipping source-map upload.')
end
end

lane :publish_pr_xcframework do
Expand Down Expand Up @@ -224,6 +243,15 @@ def xcframework_file_path
)
end

# Locate the source-map bundle produced by the `build-react` CI step. Returns
# nil when absent (rather than failing) so `publish_to_s3` can treat source-map
# publishing as optional — a build that emitted no maps still publishes the
# XCFramework.
def sourcemaps_file_path
path = File.join(PROJECT_ROOT, 'GutenbergKitSourceMaps.zip')
File.exist?(path) ? path : nil
end

def require_single_glob_match!(pattern, not_found_message)
matches = Dir.glob(pattern)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ public struct GutenbergJSException {
public let tags: [String: String]
public let isHandled: Bool
public let handledBy: String
/// Source-map Debug IDs for the files in the stack trace, used to symbolicate
/// the exception in Sentry regardless of the on-device file paths.
public let debugImages: [DebugImage]

public struct StacktraceLine {
public let filename: String?
Expand All @@ -120,6 +123,22 @@ public struct GutenbergJSException {
}
}

/// Pairs a minified file with the Debug ID that identifies its source map.
public struct DebugImage {
public let codeFile: String
public let debugID: String

init?(from dict: [AnyHashable: Any]) {
guard let codeFile = dict["code_file"] as? String,
let debugID = dict["debug_id"] as? String
else {
return nil
}
self.codeFile = codeFile
self.debugID = debugID
}
}

init?(from dict: [AnyHashable: Any]) {
guard let type = dict["type"] as? String,
let message = dict["message"] as? String,
Expand All @@ -139,6 +158,9 @@ public struct GutenbergJSException {
self.tags = tags
self.isHandled = isHandled
self.handledBy = handledBy
// Optional: absent in payloads from builds without Debug IDs.
let rawDebugImages = dict["debug_images"] as? [[AnyHashable: Any]] ?? []
self.debugImages = rawDebugImages.compactMap { DebugImage(from: $0) }
}
}

Expand Down
60 changes: 60 additions & 0 deletions ios/Tests/GutenbergKitTests/GutenbergJSExceptionTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation
import Testing

@testable import GutenbergKit

@Suite
struct GutenbergJSExceptionTests {

private func baseException(
debugImages: [[String: Any]]? = nil
) -> [String: Any] {
var dict: [String: Any] = [
"type": "TypeError",
"message": "boom",
"stacktrace": [
["function": "fn", "filename": "~/assets/editor-abc.js", "lineno": 22, "colno": 247]
],
"context": [:],
"tags": [:],
"isHandled": false,
"handledBy": "window.error"
]
if let debugImages {
dict["debug_images"] = debugImages
}
return dict
}

@Test("Decodes debug images when present")
func decodesDebugImages() throws {
let dict = baseException(debugImages: [
["code_file": "~/assets/editor-abc.js", "debug_id": "11111111-1111-1111-1111-111111111111"]
])

let exception = try #require(GutenbergJSException(from: dict))

#expect(exception.debugImages.count == 1)
#expect(exception.debugImages.first?.codeFile == "~/assets/editor-abc.js")
#expect(exception.debugImages.first?.debugID == "11111111-1111-1111-1111-111111111111")
}

@Test("Defaults to no debug images when the field is absent")
func defaultsToEmptyWhenAbsent() throws {
let exception = try #require(GutenbergJSException(from: baseException()))
#expect(exception.debugImages.isEmpty)
}

@Test("Skips malformed debug images")
func skipsMalformedDebugImages() throws {
let dict = baseException(debugImages: [
["code_file": "~/assets/editor-abc.js"], // missing debug_id
["code_file": "~/assets/index-def.js", "debug_id": "22222222-2222-2222-2222-222222222222"]
])

let exception = try #require(GutenbergJSException(from: dict))

#expect(exception.debugImages.count == 1)
#expect(exception.debugImages.first?.debugID == "22222222-2222-2222-2222-222222222222")
}
}
Loading
Loading