Save debug information in `chrome.storage` after a Dart app loads  (#1791)

diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart
index 60d2477..b749136 100644
--- a/dwds/debug_extension_mv3/web/background.dart
+++ b/dwds/debug_extension_mv3/web/background.dart
@@ -53,11 +53,19 @@
         final currentTab = await _getTab();
         final currentUrl = currentTab?.url ?? '';
         final appUrl = debugInfo.appUrl ?? '';
-        if (currentUrl.isEmpty || appUrl.isEmpty || currentUrl != appUrl) {
+        if (currentTab == null ||
+            currentUrl.isEmpty ||
+            appUrl.isEmpty ||
+            currentUrl != appUrl) {
           console.warn(
               'Dart app detected at $appUrl but current tab is $currentUrl.');
           return;
         }
+        // Save the debug info for the Dart app in storage:
+        await setStorageObject<DebugInfo>(
+            type: StorageObject.debugInfo,
+            value: debugInfo,
+            tabId: currentTab.id);
         // Update the icon to show that a Dart app has been detected:
         chrome.action.setIcon(IconInfo(path: 'dart.png'), /*callback*/ null);
       });
diff --git a/dwds/debug_extension_mv3/web/data_serializers.dart b/dwds/debug_extension_mv3/web/data_serializers.dart
index 3692027..20552e6 100644
--- a/dwds/debug_extension_mv3/web/data_serializers.dart
+++ b/dwds/debug_extension_mv3/web/data_serializers.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:built_value/serializer.dart';
+import 'package:dwds/data/debug_info.dart';
 
 import 'data_types.dart';
 
@@ -10,6 +11,7 @@
 
 /// Serializers for all the data types used in the Dart Debug Extension.
 @SerializersFor([
+  DebugInfo,
   DevToolsOpener,
 ])
 final Serializers serializers = _$serializers;
diff --git a/dwds/debug_extension_mv3/web/data_serializers.g.dart b/dwds/debug_extension_mv3/web/data_serializers.g.dart
index 0b4f692..2d11b95 100644
--- a/dwds/debug_extension_mv3/web/data_serializers.g.dart
+++ b/dwds/debug_extension_mv3/web/data_serializers.g.dart
@@ -6,7 +6,9 @@
 // BuiltValueGenerator
 // **************************************************************************
 
-Serializers _$serializers =
-    (new Serializers().toBuilder()..add(DevToolsOpener.serializer)).build();
+Serializers _$serializers = (new Serializers().toBuilder()
+      ..add(DebugInfo.serializer)
+      ..add(DevToolsOpener.serializer))
+    .build();
 
 // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas
diff --git a/dwds/debug_extension_mv3/web/storage.dart b/dwds/debug_extension_mv3/web/storage.dart
index 7e8aa5f..a989e63 100644
--- a/dwds/debug_extension_mv3/web/storage.dart
+++ b/dwds/debug_extension_mv3/web/storage.dart
@@ -13,12 +13,19 @@
 
 import 'chrome_api.dart';
 import 'data_serializers.dart';
+import 'web_api.dart';
+
+/// Switch to true for debug logging.
+bool enableDebugLogging = true;
 
 enum StorageObject {
+  debugInfo,
   devToolsOpener;
 
   String get keyName {
     switch (this) {
+      case StorageObject.debugInfo:
+        return 'debugInfo';
       case StorageObject.devToolsOpener:
         return 'devToolsOpener';
     }
@@ -28,9 +35,10 @@
 Future<bool> setStorageObject<T>({
   required StorageObject type,
   required T value,
+  int? tabId,
   void Function()? callback,
 }) {
-  final storageKey = type.keyName;
+  final storageKey = _createStorageKey(type, tabId);
   final json = jsonEncode(serializers.serialize(value));
   final storageObj = <String, String>{storageKey: json};
   final completer = Completer<bool>();
@@ -38,22 +46,42 @@
     if (callback != null) {
       callback();
     }
+    _debugLog(storageKey, 'Set: $json');
     completer.complete(true);
   }));
   return completer.future;
 }
 
-Future<T?> fetchStorageObject<T>({required StorageObject type}) {
-  final storageKey = type.keyName;
+Future<T?> fetchStorageObject<T>({required StorageObject type, int? tabId}) {
+  final storageKey = _createStorageKey(type, tabId);
   final completer = Completer<T?>();
   chrome.storage.local.get([storageKey], allowInterop((Object storageObj) {
     final json = getProperty(storageObj, storageKey) as String?;
     if (json == null) {
+      _debugWarn(storageKey, 'Does not exist.');
       completer.complete(null);
     } else {
       final value = serializers.deserialize(jsonDecode(json)) as T;
+      _debugLog(storageKey, 'Fetched: $json');
       completer.complete(value);
     }
   }));
   return completer.future;
 }
+
+String _createStorageKey(StorageObject type, int? tabId) {
+  if (tabId == null) return type.keyName;
+  return '$tabId-${type.keyName}';
+}
+
+void _debugLog(String storageKey, String msg) {
+  if (enableDebugLogging) {
+    console.log('[$storageKey] $msg');
+  }
+}
+
+void _debugWarn(String storageKey, String msg) {
+  if (enableDebugLogging) {
+    console.warn('[$storageKey] $msg');
+  }
+}
diff --git a/dwds/lib/src/injected/client.js b/dwds/lib/src/injected/client.js
index 794e091..777c1d6 100644
--- a/dwds/lib/src/injected/client.js
+++ b/dwds/lib/src/injected/client.js
@@ -25172,11 +25172,10 @@
   A.main__closure8.prototype = {
     call$1(b) {
       var t2, t3,
-        _s17_ = "$dartExtensionUri",
         t1 = A._asStringQ(self.$dartEntrypointPath);
       b.get$_$this()._appEntrypointPath = t1;
       t1 = this.windowContext;
-      t2 = A._asStringQ(t1.$index(0, _s17_));
+      t2 = A._asStringQ(t1.$index(0, "$dartAppId"));
       b.get$_$this()._appId = t2;
       t2 = A._asStringQ(self.$dartAppInstanceId);
       b.get$_$this()._appInstanceId = t2;
@@ -25186,7 +25185,7 @@
       t2 = t2._as(window.location).href;
       t2.toString;
       b.get$_$this()._appUrl = t2;
-      t2 = A._asStringQ(t1.$index(0, _s17_));
+      t2 = A._asStringQ(t1.$index(0, "$dartExtensionUri"));
       b.get$_$this()._extensionUrl = t2;
       t1 = A._asBoolQ(t1.$index(0, "$isInternalDartBuild"));
       b.get$_$this()._isInternalBuild = t1;
diff --git a/dwds/test/puppeteer/extension_test.dart b/dwds/test/puppeteer/extension_test.dart
index 016f8dd..78ec470 100644
--- a/dwds/test/puppeteer/extension_test.dart
+++ b/dwds/test/puppeteer/extension_test.dart
@@ -10,7 +10,10 @@
 })
 @Timeout(Duration(seconds: 60))
 import 'dart:async';
+import 'dart:convert';
 
+import 'package:dwds/data/debug_info.dart';
+import 'package:dwds/data/serializers.dart';
 import 'package:puppeteer/puppeteer.dart';
 import 'package:test/test.dart';
 
@@ -19,6 +22,10 @@
 
 final context = TestContext();
 
+// Note: The following delay is required to reduce flakiness. It makes
+// sure the service worker execution context is ready.
+const executionContextDelay = 1;
+
 void main() async {
   late Target serviceWorkerTarget;
   late Browser browser;
@@ -53,6 +60,31 @@
           await browser.close();
         });
 
+        test('the debug info for a Dart app is saved in the extension storage',
+            () async {
+          final appUrl = context.appUrl;
+          // Navigate to the Dart app:
+          final appTab =
+              await navigateToPage(browser, url: appUrl, isNew: true);
+          final worker = (await serviceWorkerTarget.worker)!;
+          await Future.delayed(Duration(seconds: executionContextDelay));
+          // Verify that we have debug info for the Dart app:
+          final tabIdForAppJs = _tabIdForTabJs(appUrl);
+          final appTabId = (await worker.evaluate(tabIdForAppJs)) as int;
+          final debugInfoKey = '$appTabId-debugInfo';
+          final storageObj =
+              await worker.evaluate(_fetchStorageObjJs(debugInfoKey));
+          final json = storageObj[debugInfoKey];
+          final debugInfo =
+              serializers.deserialize(jsonDecode(json)) as DebugInfo;
+          expect(debugInfo.appId, isNotNull);
+          expect(debugInfo.appEntrypointPath, isNotNull);
+          expect(debugInfo.appInstanceId, isNotNull);
+          expect(debugInfo.appOrigin, isNotNull);
+          expect(debugInfo.appUrl, isNotNull);
+          await appTab.close();
+        });
+
         test(
             'can configure opening DevTools in a tab/window with extension settings',
             () async {
@@ -62,12 +94,11 @@
           final windowIdForAppJs = _windowIdForTabJs(appUrl);
           final windowIdForDevToolsJs = _windowIdForTabJs(devToolsUrl);
           // Navigate to the Dart app:
-          await navigateToPage(browser, url: appUrl, isNew: true);
+          final appTab =
+              await navigateToPage(browser, url: appUrl, isNew: true);
           // Click on the Dart Debug Extension icon:
           final worker = (await serviceWorkerTarget.worker)!;
-          // Note: The following delay is required to reduce flakiness (it makes
-          // sure the execution context is ready):
-          await Future.delayed(Duration(seconds: 1));
+          await Future.delayed(Duration(seconds: executionContextDelay));
           await worker.evaluate(clickIconJs);
           // Verify the extension opened the Dart docs in the same window:
           var devToolsTabTarget = await browser
@@ -107,12 +138,23 @@
           // Close the DevTools tab:
           devToolsTab = await devToolsTabTarget.page;
           await devToolsTab.close();
+          await appTab.close();
         });
       });
     }
   });
 }
 
+String _tabIdForTabJs(String tabUrl) {
+  return '''
+    async () => {
+      const matchingTabs = await chrome.tabs.query({ url: "$tabUrl" });
+      const tab = matchingTabs[0];
+      return tab.id;
+    }
+''';
+}
+
 String _windowIdForTabJs(String tabUrl) {
   return '''
     async () => {
@@ -122,3 +164,20 @@
     }
 ''';
 }
+
+String _fetchStorageObjJs(String storageKey) {
+  return '''
+    async () => {
+      const storageKey = "$storageKey";
+      return new Promise((resolve, reject) => {
+        chrome.storage.local.get(storageKey, (storageObj) => {
+          if (storageObj != null) {
+            resolve(storageObj);
+          } else {
+            resolve(null);
+          }
+        });
+      });
+    }
+''';
+}
diff --git a/dwds/web/client.dart b/dwds/web/client.dart
index 79da73e..ac430dc 100644
--- a/dwds/web/client.dart
+++ b/dwds/web/client.dart
@@ -176,7 +176,7 @@
     final windowContext = JsObject.fromBrowserObject(window);
     final debugInfoJson = jsonEncode(serializers.serialize(DebugInfo((b) => b
       ..appEntrypointPath = dartEntrypointPath
-      ..appId = windowContext['\$dartExtensionUri']
+      ..appId = windowContext['\$dartAppId']
       ..appInstanceId = dartAppInstanceId
       ..appOrigin = window.location.origin
       ..appUrl = window.location.href