Repository: streamlit/streamlit
Pull request: Send initialization data in the NewReport message
Removes `ForwardMsg.initialize`. All initialization data is now carried in every `ForwardMsg.new_report` message.

This lets us remove the state from the server that stores whether a given client has already received the initialize message. This flag had a race condition - it was possible for the flag to be set to true, but the client not to actually have received the message, if a script used the `st.experimental_rerun` function towards the top of a script.

- (Python) `report_session. _maybe_enqueue_initialize_message` (and its associated flag) is gone.
- (frontend) `App.handleNewReport` now conditionally calls through to a new `App.handleOneTimeInitialization` function for the first new report message. There's logic that detects and errors if the initialize data unexpectedly changes from message to message.
- (frontend) Several new `App.test.tsx` tests that make sure initialization/newReport handling is correct.

Fixes #2226

Problem statement:
Script rerun event can cause `SessionInfo not initialized` error on the frontend
If a script is rerun very quickly after being started, it's possible to trigger an error on the frontend ("Tried to use SessionInfo before it was initialized").

An easy repro is our `streamlit run e2e/scripts/st_experimental_rerun.py` test script.

It _seems like_ what is happening is:
- `ReportSession._on_scriptrunner_event`receives a `SCRIPT_STARTED` event
- It enqueues two messages on its `self._report` object: "initialize" and "new_report"
- _Before those messages are processed by the Report_, the rerun request is handled, `ReportSession` gets a _second_ `SCRIPT_STARTED` event, it clears the Report queue, and _does not re-enqueue_ the "initialize" event (because it's already been enqueued once for the session).

Should we possibly always enqueue the initialize message whenever a run starts? It's a bit of extra overhead, but it's probably the right thing to do. Right now we're mistakenly assuming state on the client that doesn't always exist.

Hints from issue discussion:
I don't know if we can send two Initialize messages in a row. I think a lot of our code expects Initialize to only be sent once per session. But if we can change this, that would be great!

Another option: don't set the boolean `self._sent_initialize_message = True` until the message is _actually_ sent. But this would take a little refactoring too...

Commit message: Don't error if SessionInfo data changes

This is totally legit - it happens when the Streamlit server is killed and restarted, and the client reconnects.

Test SessionInfo.fromInitializeMessage

Patch to review:
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index b2c55495dd53..c00f1a25d012 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -17,7 +17,7 @@
 
 import React from "react"
 import { shallow, mount, ReactWrapper } from "enzyme"
-import { ForwardMsg } from "autogen/proto"
+import { ForwardMsg, NewReport } from "autogen/proto"
 import { IMenuItem } from "hocs/withS4ACommunication/types"
 import { MetricsManager } from "./lib/MetricsManager"
 import { getMetricsManagerForTest } from "./lib/MetricsManagerTestUtils"
@@ -28,17 +28,10 @@ import MainMenu from "./components/core/MainMenu"
 
 const getProps = (extend?: Partial<Props>): Props => ({
   screenCast: {
+    currentState: "OFF",
     toggleRecordAudio: jest.fn(),
     startRecording: jest.fn(),
     stopRecording: jest.fn(),
-    fileName: "",
-    recording: false,
-    recordAudio: false,
-    countdown: -1,
-    startAnimation: false,
-    showRecordedDialog: false,
-    showScreencastDialog: false,
-    showUnsupportedDialog: false,
   },
   s4aCommunication: {
     connect: jest.fn(),
@@ -84,7 +77,8 @@ describe("App", () => {
   })
 
   afterEach(() => {
-    SessionInfo.singleton = undefined
+    const UnsafeSessionInfo = SessionInfo as any
+    UnsafeSessionInfo.singleton = undefined
   })
 
   it("renders without crashing", () => {
@@ -93,7 +87,7 @@ describe("App", () => {
     expect(wrapper.html()).not.toBeNull()
   })
 
-  it("should reload when streamlit server version changes", () => {
+  it("reloads when streamlit server version changes", () => {
     const props = getProps()
     const wrapper = shallow(<App {...props} />)
 
@@ -101,14 +95,16 @@ describe("App", () => {
 
     const fwMessage = new ForwardMsg()
 
-    fwMessage.initialize = {
-      environmentInfo: {
-        streamlitVersion: "svv",
+    fwMessage.newReport = {
+      initialize: {
+        environmentInfo: {
+          streamlitVersion: "svv",
+        },
+        sessionId: "sessionId",
+        userInfo: {},
+        config: {},
+        sessionState: {},
       },
-      sessionId: "sessionId",
-      userInfo: {},
-      config: {},
-      sessionState: {},
     }
 
     // @ts-ignore
@@ -117,7 +113,7 @@ describe("App", () => {
     expect(window.location.reload).toHaveBeenCalled()
   })
 
-  it("should start screencast recording when the MainMenu is clicked", () => {
+  it("starts screencast recording when the MainMenu is clicked", () => {
     const props = getProps()
     const wrapper = shallow(<App {...props} />)
 
@@ -135,7 +131,7 @@ describe("App", () => {
     )
   })
 
-  it("should stop screencast when esc is pressed", () => {
+  it("stops screencast when esc is pressed", () => {
     const props = getProps()
     const wrapper = shallow(<App {...props} />)
 
@@ -145,7 +141,7 @@ describe("App", () => {
     expect(props.screenCast.stopRecording).toBeCalled()
   })
 
-  it("should show s4aMenuItems", () => {
+  it("shows s4aMenuItems", () => {
     const props = getProps({
       s4aCommunication: {
         connect: jest.fn(),
@@ -167,3 +163,82 @@ describe("App", () => {
     ])
   })
 })
+
+describe("App.handleNewReport", () => {
+  const NEW_REPORT = new NewReport({
+    initialize: {
+      userInfo: {
+        installationId: "installationId",
+        installationIdV1: "installationIdV1",
+        installationIdV2: "installationIdV2",
+        email: "email",
+      },
+      config: {
+        sharingEnabled: false,
+        gatherUsageStats: false,
+        maxCachedMessageAge: 0,
+        mapboxToken: "mapboxToken",
+        allowRunOnSave: false,
+      },
+      environmentInfo: {
+        streamlitVersion: "streamlitVersion",
+        pythonVersion: "pythonVersion",
+      },
+      sessionState: {
+        runOnSave: false,
+        reportIsRunning: false,
+      },
+      sessionId: "sessionId",
+      commandLine: "commandLine",
+    },
+  })
+
+  afterEach(() => {
+    const UnsafeSessionInfo = SessionInfo as any
+    UnsafeSessionInfo.singleton = undefined
+  })
+
+  it("performs one-time initialization", () => {
+    const wrapper = shallow(<App {...getProps()} />)
+    const app = wrapper.instance()
+
+    const oneTimeInitialization = jest.spyOn(
+      app,
+      // @ts-ignore
+      "handleOneTimeInitialization"
+    )
+
+    expect(SessionInfo.isSet()).toBe(false)
+
+    // @ts-ignore
+    app.handleNewReport(NEW_REPORT)
+
+    expect(oneTimeInitialization).toHaveBeenCalledTimes(1)
+    expect(SessionInfo.isSet()).toBe(true)
+  })
+
+  it("performs one-time initialization only once", () => {
+    const wrapper = shallow(<App {...getProps()} />)
+    const app = wrapper.instance()
+
+    const oneTimeInitialization = jest.spyOn(
+      app,
+      // @ts-ignore
+      "handleOneTimeInitialization"
+    )
+
+    expect(SessionInfo.isSet()).toBe(false)
+
+    // @ts-ignore
+    app.handleNewReport(NEW_REPORT)
+    // @ts-ignore
+    app.handleNewReport(NEW_REPORT)
+    // @ts-ignore
+    app.handleNewReport(NEW_REPORT)
+
+    // Multiple NEW_REPORT messages should not result in one-time
+    // initialization being performed more than once.
+    expect(oneTimeInitialization).toHaveBeenCalledTimes(1)
+    expect(SessionInfo.isSet()).toBe(true)
+  })
+})
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index d3a12e604ae8..9146c33e2bef 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -54,6 +54,7 @@ import {
   SessionEvent,
   WidgetStates,
   SessionState,
+  Config,
 } from "autogen/proto"
 
 import { RERUN_PROMPT_MODAL_DIALOG } from "lib/baseconsts"
@@ -287,14 +288,12 @@ export class App extends PureComponent<Props, State> {
 
     try {
       dispatchProto(msgProto, "type", {
-        initialize: (initializeMsg: Initialize) =>
-          this.handleInitialize(initializeMsg),
+        newReport: (newReportMsg: NewReport) =>
+          this.handleNewReport(newReportMsg),
         sessionStateChanged: (msg: SessionState) =>
           this.handleSessionStateChanged(msg),
         sessionEvent: (evtMsg: SessionEvent) =>
           this.handleSessionEvent(evtMsg),
-        newReport: (newReportMsg: NewReport) =>
-          this.handleNewReport(newReportMsg),
         delta: (deltaMsg: Delta) =>
           this.handleDeltaMsg(
             deltaMsg,
@@ -378,64 +377,6 @@ export class App extends PureComponent<Props, State> {
     })
   }
 
-  /**
-   * Handler for ForwardMsg.initialize messages
-   * @param initializeMsg an Initialize protobuf
-   */
-  handleInitialize = (initializeMsg: Initialize): void => {
-    const {
-      sessionId,
-      environmentInfo,
-      userInfo,
-      config,
-      sessionState,
-    } = initializeMsg
-
-    if (
-      sessionId == null ||
-      !environmentInfo ||
-      !userInfo ||
-      !config ||
-      !sessionState
-    ) {
-      throw new Error("InitializeMsg is missing a required field")
-    }
-
-    if (App.hasStreamlitVersionChanged(initializeMsg)) {
-      window.location.reload()
-      return
-    }
-
-    SessionInfo.current = new SessionInfo({
-      sessionId,
-      streamlitVersion: environmentInfo.streamlitVersion,
-      pythonVersion: environmentInfo.pythonVersion,
-      installationId: userInfo.installationId,
-      installationIdV1: userInfo.installationIdV1,
-      installationIdV2: userInfo.installationIdV2,
-      authorEmail: userInfo.email,
-      maxCachedMessageAge: config.maxCachedMessageAge,
-      commandLine: initializeMsg.commandLine,
-      userMapboxToken: config.mapboxToken,
-    })
-
-    MetricsManager.current.initialize({
-      gatherUsageStats: Boolean(config.gatherUsageStats),
-    })
-
-    MetricsManager.current.enqueue("createReport", {
-      pythonVersion: SessionInfo.current.pythonVersion,
-    })
-
-    this.setState({
-      sharingEnabled: Boolean(config.sharingEnabled),
-      allowRunOnSave: Boolean(config.allowRunOnSave),
-    })
-
-    this.props.s4aCommunication.connect()
-    this.handleSessionStateChanged(sessionState)
-  }
-
   /**
    * Handler for ForwardMsg.sessionStateChanged messages
    * @param stateChangeProto a SessionState protobuf
@@ -530,9 +471,24 @@ export class App extends PureComponent<Props, State> {
    * @param newReportProto a NewReport protobuf
    */
   handleNewReport = (newReportProto: NewReport): void => {
+    const initialize = newReportProto.initialize as Initialize
+
+    if (App.hasStreamlitVersionChanged(initialize)) {
+      window.location.reload()
+      return
+    }
+
+    // First, handle initialization logic. Each NewReport message has
+    // initialization data. If this is the _first_ time we're receiving
+    // the NewReport message, we perform some one-time initialization.
+    if (!SessionInfo.isSet()) {
+      // We're not initialized. Perform one-time initialization.
+      this.handleOneTimeInitialization(initialize)
+    }
+
     const { reportHash } = this.state
     const {
-      id: reportId,
+      reportId,
       name: reportName,
       scriptPath,
       deployParams,
@@ -561,6 +517,31 @@ export class App extends PureComponent<Props, State> {
     }
   }
 
+  /**
+   * Performs one-time initialization. This is called from `handleNewReport`.
+   */
+  handleOneTimeInitialization = (initialize: Initialize): void => {
+    SessionInfo.current = SessionInfo.fromInitializeMessage(initialize)
+
+    const config = initialize.config as Config
+
+    MetricsManager.current.initialize({
+      gatherUsageStats: config.gatherUsageStats,
+    })
+
+    MetricsManager.current.enqueue("createReport", {
+      pythonVersion: SessionInfo.current.pythonVersion,
+    })
+
+    this.setState({
+      sharingEnabled: config.sharingEnabled,
+      allowRunOnSave: config.allowRunOnSave,
+    })
+
+    this.props.s4aCommunication.connect()
+    this.handleSessionStateChanged(initialize.sessionState)
+  }
+
   /**
    * Handler for ForwardMsg.reportFinished messages
    * @param status the ReportFinishedStatus that the report finished with
diff --git a/frontend/src/lib/SessionInfo.test.ts b/frontend/src/lib/SessionInfo.test.ts
index 0aba31cd1293..fda973e1a4cd 100644
--- a/frontend/src/lib/SessionInfo.test.ts
+++ b/frontend/src/lib/SessionInfo.test.ts
@@ -16,7 +16,47 @@
  */
 
 import { SessionInfo } from "lib/SessionInfo"
+import { Initialize } from "../autogen/proto"
 
 test("Throws an error when used before initialization", () => {
   expect(() => SessionInfo.current).toThrow()
 })
+
+test("Can be initialized from a protobuf", () => {
+  const MESSAGE = new Initialize({
+    userInfo: {
+      installationId: "installationId",
+      installationIdV1: "installationIdV1",
+      installationIdV2: "installationIdV2",
+      email: "email",
+    },
+    config: {
+      sharingEnabled: false,
+      gatherUsageStats: false,
+      maxCachedMessageAge: 31,
+      mapboxToken: "mapboxToken",
+      allowRunOnSave: false,
+    },
+    environmentInfo: {
+      streamlitVersion: "streamlitVersion",
+      pythonVersion: "pythonVersion",
+    },
+    sessionState: {
+      runOnSave: false,
+      reportIsRunning: false,
+    },
+    sessionId: "sessionId",
+    commandLine: "commandLine",
+  })
+
+  const si = SessionInfo.fromInitializeMessage(MESSAGE)
+  expect(si.sessionId).toEqual("sessionId")
+  expect(si.streamlitVersion).toEqual("streamlitVersion")
+  expect(si.pythonVersion).toEqual("pythonVersion")
+  expect(si.installationId).toEqual("installationId")
+  expect(si.installationIdV1).toEqual("installationIdV1")
+  expect(si.installationIdV2).toEqual("installationIdV2")
+  expect(si.authorEmail).toEqual("email")
+  expect(si.maxCachedMessageAge).toEqual(31)
+  expect(si.commandLine).toEqual("commandLine")
+})
diff --git a/frontend/src/lib/SessionInfo.ts b/frontend/src/lib/SessionInfo.ts
index 1e683421c88e..c5b8fb7a8482 100644
--- a/frontend/src/lib/SessionInfo.ts
+++ b/frontend/src/lib/SessionInfo.ts
@@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {
+  Config,
+  EnvironmentInfo,
+  Initialize,
+  UserInfo,
+} from "../autogen/proto"
 
 export interface Args {
   sessionId: string
@@ -84,7 +90,27 @@ export class SessionInfo {
     return this.current.commandLine === "streamlit hello"
   }
 
-  constructor({
+  /** Create a SessionInfo from the relevant bits of an initialize message. */
+  public static fromInitializeMessage(initialize: Initialize): SessionInfo {
+    const environmentInfo = initialize.environmentInfo as EnvironmentInfo
+    const userInfo = initialize.userInfo as UserInfo
+    const config = initialize.config as Config
+
+    return new SessionInfo({
+      sessionId: initialize.sessionId,
+      streamlitVersion: environmentInfo.streamlitVersion,
+      pythonVersion: environmentInfo.pythonVersion,
+      installationId: userInfo.installationId,
+      installationIdV1: userInfo.installationIdV1,
+      installationIdV2: userInfo.installationIdV2,
+      authorEmail: userInfo.email,
+      maxCachedMessageAge: config.maxCachedMessageAge,
+      commandLine: initialize.commandLine,
+      userMapboxToken: config.mapboxToken,
+    })
+  }
+
+  public constructor({
     sessionId,
     streamlitVersion,
     pythonVersion,
diff --git a/lib/streamlit/report_session.py b/lib/streamlit/report_session.py
index 5fb1037b5da7..75fdf9112260 100644
--- a/lib/streamlit/report_session.py
+++ b/lib/streamlit/report_session.py
@@ -96,7 +96,6 @@ def __init__(self, ioloop, script_path, command_line, uploaded_file_manager):
         self._local_sources_watcher = LocalSourcesWatcher(
             self._report, self._on_source_file_changed
         )
-        self._sent_initialize_message = False
         self._storage = None
         self._maybe_reuse_previous_run = False
         self._run_on_save = config.get_option("server.runOnSave")
@@ -264,7 +263,6 @@ def _on_scriptrunner_event(self, event, exception=None, client_state=None):
                 self._ioloop.spawn_callback(self._save_running_report)
 
             self._clear_queue()
-            self._maybe_enqueue_initialize_message()
             self._enqueue_new_report_message()
 
         elif (
@@ -348,15 +346,39 @@ def _enqueue_file_change_message(self):
         msg.session_event.report_changed_on_disk = True
         self.enqueue(msg)
 
-    def _maybe_enqueue_initialize_message(self):
-        if self._sent_initialize_message:
-            return
+    def get_deploy_params(self):
+        try:
+            from streamlit.git_util import GitRepo
 
-        self._sent_initialize_message = True
+            self._repo = GitRepo(self._report.script_path)
+            return self._repo.get_repo_info()
+        except:
+            # Issues can arise based on the git structure
+            # (e.g. if branch is in DETACHED HEAD state,
+            # git is not installed, etc)
+            # In this case, catch any errors
+            return None
 
+    def _enqueue_new_report_message(self):
+        self._report.generate_new_id()
         msg = ForwardMsg()
-        imsg = msg.initialize
+        msg.new_report.report_id = self._report.report_id
+        msg.new_report.name = self._report.name
+        msg.new_report.script_path = self._report.script_path
 
+        # git deploy params
+        deploy_params = self.get_deploy_params()
+        if deploy_params is not None:
+            repo, branch, module = deploy_params
+            msg.new_report.deploy_params.repository = repo
+            msg.new_report.deploy_params.branch = branch
+            msg.new_report.deploy_params.module = module
+
+        # Immutable session data. We send this every time a new report is
+        # started, to avoid having to track whether the client has already
+        # received it. It does not change from run to run; it's up to the
+        # to perform one-time initialization only once.
+        imsg = msg.new_report.initialize
         imsg.config.sharing_enabled = config.get_option("global.sharingMode") != "off"
 
         imsg.config.gather_usage_stats = config.get_option("browser.gatherUsageStats")
@@ -369,16 +391,6 @@ def _maybe_enqueue_initialize_message(self):
 
         imsg.config.allow_run_on_save = config.get_option("server.allowRunOnSave")
 
-        LOGGER.debug(
-            "New browser connection: "
-            "gather_usage_stats=%s, "
-            "sharing_enabled=%s, "
-            "max_cached_message_age=%s",
-            imsg.config.gather_usage_stats,
-            imsg.config.sharing_enabled,
-            imsg.config.max_cached_message_age,
-        )
-
         imsg.environment_info.streamlit_version = __version__
         imsg.environment_info.python_version = ".".join(map(str, sys.version_info))
 
@@ -400,34 +412,6 @@ def _maybe_enqueue_initialize_message(self):
 
         self.enqueue(msg)
 
-    def get_deploy_params(self):
-        try:
-            from streamlit.git_util import GitRepo
-
-            self._repo = GitRepo(self._report.script_path)
-            return self._repo.get_repo_info()
-        except:
-            # Issues can arise based on the git structure
-            # (e.g. if branch is in DETACHED HEAD state,
-            # git is not installed, etc)
-            # In this case, catch any errors
-            return None
-
-    def _enqueue_new_report_message(self):
-        self._report.generate_new_id()
-        msg = ForwardMsg()
-        msg.new_report.id = self._report.report_id
-        msg.new_report.name = self._report.name
-        msg.new_report.script_path = self._report.script_path
-
-        deploy_params = self.get_deploy_params()
-        if deploy_params is not None:
-            repo, branch, module = deploy_params
-            msg.new_report.deploy_params.repository = repo
-            msg.new_report.deploy_params.branch = branch
-            msg.new_report.deploy_params.module = module
-        self.enqueue(msg)
-
     def _enqueue_report_finished_message(self, status):
         """Enqueue a report_finished ForwardMsg.
 
diff --git a/lib/tests/streamlit/report_queue_test.py b/lib/tests/streamlit/report_queue_test.py
index 97ba8c4868e0..91eafdb07c63 100644
--- a/lib/tests/streamlit/report_queue_test.py
+++ b/lib/tests/streamlit/report_queue_test.py
@@ -27,9 +27,9 @@
 # For the messages below, we don't really care about their contents so much as
 # their general type.
 
-INIT_MSG = ForwardMsg()
-INIT_MSG.initialize.config.sharing_enabled = True
-INIT_MSG.initialize.config.allow_run_on_save = True
+NEW_REPORT_MSG = ForwardMsg()
+NEW_REPORT_MSG.new_report.initialize.config.sharing_enabled = True
+NEW_REPORT_MSG.new_report.initialize.config.allow_run_on_save = True
 
 TEXT_DELTA_MSG1 = ForwardMsg()
 TEXT_DELTA_MSG1.delta.new_element.text.body = "text1"
@@ -57,20 +57,20 @@ def test_simple_enqueue(self):
         rq = ReportQueue()
         self.assertTrue(rq.is_empty())
 
-        rq.enqueue(INIT_MSG)
+        rq.enqueue(NEW_REPORT_MSG)
 
         self.assertFalse(rq.is_empty())
         queue = rq.flush()
         self.assertTrue(rq.is_empty())
         self.assertEqual(len(queue), 1)
-        self.assertTrue(queue[0].initialize.config.sharing_enabled)
-        self.assertTrue(queue[0].initialize.config.allow_run_on_save)
+        self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled)
+        self.assertTrue(queue[0].new_report.initialize.config.allow_run_on_save)
 
     def test_enqueue_two(self):
         rq = ReportQueue()
         self.assertTrue(rq.is_empty())
 
-        rq.enqueue(INIT_MSG)
+        rq.enqueue(NEW_REPORT_MSG)
 
         TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path(
             RootContainer.MAIN, (), 0
@@ -79,7 +79,7 @@ def test_enqueue_two(self):
 
         queue = rq.flush()
         self.assertEqual(len(queue), 2)
-        self.assertTrue(queue[0].initialize.config.sharing_enabled)
+        self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled)
         self.assertEqual(
             make_delta_path(RootContainer.MAIN, (), 0), queue[1].metadata.delta_path
         )
@@ -89,7 +89,7 @@ def test_enqueue_three(self):
         rq = ReportQueue()
         self.assertTrue(rq.is_empty())
 
-        rq.enqueue(INIT_MSG)
+        rq.enqueue(NEW_REPORT_MSG)
 
         TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path(
             RootContainer.MAIN, (), 0
@@ -103,7 +103,7 @@ def test_enqueue_three(self):
 
         queue = rq.flush()
         self.assertEqual(len(queue), 3)
-        self.assertTrue(queue[0].initialize.config.sharing_enabled)
+        self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled)
         self.assertEqual(
             make_delta_path(RootContainer.MAIN, (), 0), queue[1].metadata.delta_path
         )
@@ -117,7 +117,7 @@ def test_replace_element(self):
         rq = ReportQueue()
         self.assertTrue(rq.is_empty())
 
-        rq.enqueue(INIT_MSG)
+        rq.enqueue(NEW_REPORT_MSG)
 
         TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path(
             RootContainer.MAIN, (), 0
@@ -131,7 +131,7 @@ def test_replace_element(self):
 
         queue = rq.flush()
         self.assertEqual(len(queue), 2)
-        self.assertTrue(queue[0].initialize.config.sharing_enabled)
+        self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled)
         self.assertEqual(
             make_delta_path(RootContainer.MAIN, (), 0), queue[1].metadata.delta_path
         )
@@ -141,7 +141,7 @@ def test_simple_add_rows(self):
         rq = ReportQueue()
         self.assertTrue(rq.is_empty())
 
-        rq.enqueue(INIT_MSG)
+        rq.enqueue(NEW_REPORT_MSG)
 
         TEXT_DELTA_MSG1.metadata.delta_path[:] = make_delta_path(
             RootContainer.MAIN, (), 0
@@ -156,7 +156,7 @@ def test_simple_add_rows(self):
 
         queue = rq.flush()
         self.assertEqual(len(queue), 3)
-        self.assertTrue(queue[0].initialize.config.sharing_enabled)
+        self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled)
         self.assertEqual(
             make_delta_path(RootContainer.MAIN, (), 0), queue[1].metadata.delta_path
         )
@@ -173,7 +173,7 @@ def test_add_rows_rerun(self):
         rq = ReportQueue()
         self.assertTrue(rq.is_empty())
 
-        rq.enqueue(INIT_MSG)
+        rq.enqueue(NEW_REPORT_MSG)
 
         # Simulate rerun
         for i in range(2):
@@ -194,7 +194,7 @@ def test_add_rows_rerun(self):
 
         queue = rq.flush()
         self.assertEqual(len(queue), 3)
-        self.assertTrue(queue[0].initialize.config.sharing_enabled)
+        self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled)
         self.assertEqual(
             make_delta_path(RootContainer.MAIN, (), 0), queue[1].metadata.delta_path
         )
@@ -212,7 +212,7 @@ def test_multiple_containers(self):
         rq = ReportQueue()
         self.assertTrue(rq.is_empty())
 
-        rq.enqueue(INIT_MSG)
+        rq.enqueue(NEW_REPORT_MSG)
 
         def enqueue_deltas(container: RootContainer, path: Tuple[int, ...]):
             # We deep-copy the protos because we mutate each one
@@ -248,7 +248,7 @@ def assert_deltas(container: RootContainer, path: Tuple[int, ...], idx: int):
 
         queue = rq.flush()
         self.assertEqual(5, len(queue))
-        self.assertTrue(queue[0].initialize.config.sharing_enabled)
+        self.assertTrue(queue[0].new_report.initialize.config.sharing_enabled)
 
         assert_deltas(RootContainer.MAIN, (), 1)
         assert_deltas(RootContainer.SIDEBAR, (0, 0, 1), 3)
diff --git a/lib/tests/streamlit/report_test.py b/lib/tests/streamlit/report_test.py
index 0bd9197726a4..898722523f38 100644
--- a/lib/tests/streamlit/report_test.py
+++ b/lib/tests/streamlit/report_test.py
@@ -27,9 +27,9 @@
 from streamlit.proto.StaticManifest_pb2 import StaticManifest
 from tests import testutil
 
-INIT_MSG = ForwardMsg()
-INIT_MSG.initialize.config.sharing_enabled = True
-INIT_MSG.metadata.delta_path[:] = make_delta_path(RootContainer.MAIN, (), 0)
+NEW_REPORT_MSG = ForwardMsg()
+NEW_REPORT_MSG.new_report.initialize.config.sharing_enabled = True
+NEW_REPORT_MSG.metadata.delta_path[:] = make_delta_path(RootContainer.MAIN, (), 0)
 
 TEXT_DELTA_MSG = ForwardMsg()
 TEXT_DELTA_MSG.delta.new_element.text.body = "text1"
@@ -56,7 +56,7 @@ def _parse_msg(msg_string):
 class ReportTest(unittest.TestCase):
     def test_serialize_final_report(self):
         report = Report("/not/a/script.py", "")
-        _enqueue(report, INIT_MSG)
+        _enqueue(report, NEW_REPORT_MSG)
         _enqueue(report, TEXT_DELTA_MSG)
         _enqueue(report, EMPTY_DELTA_MSG)
 
@@ -65,7 +65,7 @@ def test_serialize_final_report(self):
         # Validate our messages.
         messages = [_parse_msg(msg_string) for _, msg_string in files[:-1]]
         self.assertEqual(3, len(messages))
-        self.assertEqual("initialize", messages[0].WhichOneof("type"))
+        self.assertEqual("new_report", messages[0].WhichOneof("type"))
         self.assertEqual("text1", messages[1].delta.new_element.text.body)
         self.assertEqual("empty", messages[2].delta.new_element.WhichOneof("type"))
 
@@ -82,7 +82,7 @@ def test_serialize_final_report(self):
 
     def test_serialize_running_report(self):
         report = Report("/not/a/script.py", "")
-        _enqueue(report, INIT_MSG)
+        _enqueue(report, NEW_REPORT_MSG)
         _enqueue(report, EMPTY_DELTA_MSG)
         _enqueue(report, TEXT_DELTA_MSG)
         _enqueue(report, EMPTY_DELTA_MSG)
diff --git a/proto/streamlit/proto/ForwardMsg.proto b/proto/streamlit/proto/ForwardMsg.proto
index 6e607cac94f5..76e0381a431a 100644
--- a/proto/streamlit/proto/ForwardMsg.proto
+++ b/proto/streamlit/proto/ForwardMsg.proto
@@ -17,7 +17,6 @@
 syntax = "proto3";
 
 import "streamlit/proto/Delta.proto";
-import "streamlit/proto/Initialize.proto";
 import "streamlit/proto/NewReport.proto";
 import "streamlit/proto/PageConfig.proto";
 import "streamlit/proto/PageInfo.proto";
@@ -45,7 +44,6 @@ message ForwardMsg {
   oneof type {
     // Report lifecycle messages.
 
-    Initialize initialize = 3;
     NewReport new_report = 4;
     Delta delta = 5;
     PageInfo page_info_changed = 12;
diff --git a/proto/streamlit/proto/Initialize.proto b/proto/streamlit/proto/Initialize.proto
deleted file mode 100644
index d136599f1e8a..000000000000
--- a/proto/streamlit/proto/Initialize.proto
+++ /dev/null
@@ -1,68 +0,0 @@
-/**
- * Copyright 2018-2020 Streamlit Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
-*/
-
-syntax = "proto3";
-
-import "streamlit/proto/SessionState.proto";
-
-// This is the first message that is sent when the connection is opened.
-// It contains streamlit configuration data, and the session state
-// that existed at the time of connection.
-message Initialize {
-  UserInfo user_info = 1;
-  Config config = 2;
-  EnvironmentInfo environment_info = 3;
-
-  // The session state at the time the connection was established
-  SessionState session_state = 4;
-
-  // the actual command line as a string
-  string command_line = 5;
-
-  // The ReportSession.id for this connection's ReportSession.
-  // This is used to associate uploaded files with the client that uploaded
-  // them.
-  string session_id = 6;
-}
-
-message Config {
-  // True if saving is properly configured.
-  bool sharing_enabled = 1;
-
-  // See config option "browser.gatherUsageStats".
-  bool gather_usage_stats = 2;
-
-  // See config option "global.maxCachedMessageAge".
-  int32 max_cached_message_age = 3;
-
-  // See config option "mapbox.token".
-  string mapbox_token = 4;
-
-  // See config option "server.allowRunOnSave"
-  bool allow_run_on_save = 5;
-}
-
-message UserInfo {
-  string installation_id = 1;
-  string installation_id_v1 = 3;
-  string installation_id_v2 = 4;
-  string email = 2;
-}
-
-message EnvironmentInfo {
-  string streamlit_version = 1;
-  string python_version = 2;
-}
diff --git a/proto/streamlit/proto/NewReport.proto b/proto/streamlit/proto/NewReport.proto
index 61695a4a9ddb..2258148fc03b 100644
--- a/proto/streamlit/proto/NewReport.proto
+++ b/proto/streamlit/proto/NewReport.proto
@@ -16,21 +16,85 @@
 
 syntax = "proto3";
 
-// This is the first message that is sent when a new report is run
+import "streamlit/proto/SessionState.proto";
+
+// This is the first message that is sent when a new report is run.
 message NewReport {
+  // Initialization data. This data does *not* change from rerun to rerun,
+  // but we send it every time so that we don't have to track whether the
+  // client has already received it. The client is responsible for
+  // performing initialization logic only once.
+  Initialize initialize = 1;
+
   // The report ID
-  string id = 1;
+  string report_id = 2;
 
   // The basename of the script that launched this report. Example: "foo.py"
-  string name = 2;
+  string name = 3;
 
   // The full path of the script that launched this report. Example:
   // "/foo/bar/foo.py"
-  string script_path = 3;
+  string script_path = 4;
 
   DeployParams deploy_params = 5;
 }
 
+// Contains Streamlit configuration data, and the session state that existed
+// at the time the user connected.
+message Initialize {
+  UserInfo user_info = 1;
+  Config config = 2;
+  EnvironmentInfo environment_info = 3;
+
+  // The session state at the time the connection was established
+  SessionState session_state = 4;
+
+  // the actual command line as a string
+  string command_line = 5;
+
+  // The ReportSession.id for this connection's ReportSession.
+  // This is used to associate uploaded files with the client that uploaded
+  // them.
+  string session_id = 6;
+}
+
+// App configuration options, initialized mainly from the
+// .streamlit/config.toml file. Does not change over the app's lifetime.
+message Config {
+  // True if saving is properly configured.
+  bool sharing_enabled = 1;
+
+  // See config option "browser.gatherUsageStats".
+  bool gather_usage_stats = 2;
+
+  // See config option "global.maxCachedMessageAge".
+  int32 max_cached_message_age = 3;
+
+  // See config option "mapbox.token".
+  string mapbox_token = 4;
+
+  // See config option "server.allowRunOnSave"
+  bool allow_run_on_save = 5;
+}
+
+// Data that identifies the Streamlit app creator.
+// Does not change over the app's lifetime.
+message UserInfo {
+  string installation_id = 1;
+  string installation_id_v1 = 3;
+  string installation_id_v2 = 4;
+  string email = 2;
+}
+
+// Data that identifies the Streamlit app's environment.
+// Does not change over the app lifetime.
+message EnvironmentInfo {
+  string streamlit_version = 1;
+  string python_version = 2;
+}
+
+// Data about the git repository this app is deployed from (if any).
+// Does not change over the app's lifetime.
 message DeployParams {
   string repository = 1;
   string branch = 2;


Code review comments:
diff_hunk: @@ -14,6 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {
+  Config,
+  EnvironmentInfo,
+  Initialize,
+  UserInfo,
+} from "../autogen/proto"
path: frontend/src/lib/SessionInfo.ts
text: @user1:
Same here. No need for the `../`

diff_hunk: @@ -16,7 +16,47 @@
  */
 
 import { SessionInfo } from "lib/SessionInfo"
+import { Initialize } from "../autogen/proto"
path: frontend/src/lib/SessionInfo.test.ts
text: @user1:
Not a big deal, but this doesn't need to be relative, right?

`import { Initialize } from "autogen/proto"`

It's not a big deal, but just what we do in other places.