Repository: phoenixframework/phoenix
Pull request #3816 (2020-05-01 00:41:01)

Tiny grammar fix
This reads a lot better


Patch:
diff --git a/guides/directory_structure.md b/guides/directory_structure.md
index 4599e739c..cf34fe4a6 100644
--- a/guides/directory_structure.md
+++ b/guides/directory_structure.md
@@ -15,7 +15,7 @@ When we use `mix phx.new` to generates a new Phoenix application, it builds a to
 │   └── hello_web
 │   └── hello_web.ex
 ├── priv
-├── test
+└── test
 ```
 
 We will go over those directories one by one:
@@ -41,7 +41,7 @@ The `lib/hello` directory hosts all of your business domain. Since our project d
 ```console
 lib/hello
 ├── application.ex
-├── repo.ex
+└── repo.ex
 ```
 
 The `lib/hello/application.ex` file defines an Elixir application named `Hello.Application`. That's because at the end of the day Phoenix applications are simply Elixir applications. The `Hello.Application` module defines which services are part of our application:
@@ -50,6 +50,8 @@ The `lib/hello/application.ex` file defines an Elixir application named `Hello.A
 children = [
   # Start the Ecto repository
   Hello.Repo,
+  # Start the Telemetry supervisor
+  HelloWeb.Telemetry,
   # Start the PubSub system
   {Phoenix.PubSub, name: Hello.PubSub},
   # Start the Endpoint (http/https)
@@ -90,7 +92,7 @@ lib/hello_web
 │   │   └── app.html.eex
 │   └── page
 │       └── index.html.eex
-└── views
+├── views
 │   ├── error_helpers.ex
 │   ├── error_view.ex
 │   ├── layout_view.ex
@@ -98,6 +100,7 @@ lib/hello_web
 ├── endpoint.ex
 ├── gettext.ex
 ├── router.ex
+└── telemetry.ex
 ```
 
 All of the files which are currently in the `controllers`, `templates`, and `views` directories are there to create the "Welcome to Phoenix!" page we saw in the "Up and running" guide. 
@@ -105,6 +108,8 @@ The `channels` directory is where we will add code related to building real-time
 
 By looking at `templates` and `views` directories, we can see Phoenix provides features for handling layouts and error pages out of the box.
 
-Besides the directories mentioned, `lib/hello_web` has three files at its root. `lib/hello_web/endpoint.ex` is the entry-point for HTTP requests. Once the browser accesses `http://localhost:4000`, the endpoint starts processing the data, eventually leading to the router, which is defined in `lib/hello_web/router.ex`. The router defines the rules to dispatch requests to "controllers", which then uses "views" and "templates" to render HTML pages back to clients. We explore these layers in length in other guides, starting with the "Request life-cycle" guide coming next.
+Besides the directories mentioned, `lib/hello_web` has four files at its root. `lib/hello_web/endpoint.ex` is the entry-point for HTTP requests. Once the browser accesses `http://localhost:4000`, the endpoint starts processing the data, eventually leading to the router, which is defined in `lib/hello_web/router.ex`. The router defines the rules to dispatch requests to "controllers", which then uses "views" and "templates" to render HTML pages back to clients. We explore these layers in length in other guides, starting with the "Request life-cycle" guide coming next.
+
+Through _telemetry_, Phoenix is able to collect metrics and send monitoring events of your application. The `lib/hello_web/telemetry.ex` file defines the supervisor responsible for managing the telemetry processes. You can find more information on this topic in the [Telemetry guide](telemetry.html).
 
 Finally, there is a `lib/hello_web/gettext.ex` file which provides internationalization through [Gettext](https://hexdocs.pm/gettext/Gettext.html). If you are not worried about internationalization, you can safely skip this file and its contents.
diff --git a/guides/introduction/community.md b/guides/introduction/community.md
index 0757cc0a3..7c01a2698 100644
--- a/guides/introduction/community.md
+++ b/guides/introduction/community.md
@@ -32,6 +32,8 @@ There are a number of places to connect with community members at all experience
 
 ## Screencasts/Courses
 
+  * [Phoenix LiveView Free Course (The Pragmatic Studio - May 2020)](https://pragmaticstudio.com/courses/phoenix-liveview)
+
   * [The Complete Elixir and Phoenix Bootcamp Master Functional Programming Techniques with Elixir and Phoenix while Learning to Build Compelling Web Applications (Udemy - Jun 2017)](https://www.udemy.com/the-complete-elixir-and-phoenix-bootcamp-and-tutorial/)
 
   * [Discover Elixir & Phoenix (Mar 2017)](https://www.ludu.co/course/discover-elixir-phoenix)
diff --git a/guides/plug.md b/guides/plug.md
index 091b668bc..d044fc59f 100644
--- a/guides/plug.md
+++ b/guides/plug.md
@@ -38,7 +38,7 @@ Pretty simple, right? Let's see this function in action by adding it to our endp
 defmodule HelloWeb.Endpoint do
   ...
 
-  plug :instrospect
+  plug :introspect
   plug HelloWeb.Router
 
   def introspect(conn, _opts) do
@@ -134,7 +134,7 @@ Endpoints organize all the plugs common to every request, and apply them before
 defmodule HelloWeb.Endpoint do
   ...
 
-  plug :instrospect
+  plug :introspect
   plug HelloWeb.Router
 ```
 
diff --git a/guides/routing.md b/guides/routing.md
index 2db728c84..b418489a9 100644
--- a/guides/routing.md
+++ b/guides/routing.md
@@ -291,7 +291,7 @@ scope "/admin", HelloWeb.Admin do
 end
 ```
 
-We define a new scope where all routes a prefixed with "/admin" and all controllers are under the `HelloWeb.Admin` namespace.
+We define a new scope where all routes are prefixed with "/admin" and all controllers are under the `HelloWeb.Admin` namespace.
 
 Running `mix phx.routes` again, in addition to the previous set of routes we get the following:
 
diff --git a/lib/phoenix/controller/pipeline.ex b/lib/phoenix/controller/pipeline.ex
index 9d9b5ffbc..7e4161cfb 100644
--- a/lib/phoenix/controller/pipeline.ex
+++ b/lib/phoenix/controller/pipeline.ex
@@ -19,15 +19,12 @@ defmodule Phoenix.Controller.Pipeline do
 
       @doc false
       def call(conn, action) when is_atom(action) do
-        conn =
-          update_in(
-            conn.private,
-            &(&1
-              |> Map.put(:phoenix_controller, __MODULE__)
-              |> Map.put(:phoenix_action, action))
-          )
-
-        phoenix_controller_pipeline(conn, action)
+        conn
+        |> merge_private(
+          phoenix_controller: __MODULE__,
+          phoenix_action: action
+        )
+        |> phoenix_controller_pipeline(action)
       end
 
       @doc false
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 5565af5ba..89960f438 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -768,8 +768,13 @@ defmodule Phoenix.Endpoint do
       The valid keys are:
 
         * `:peer_data` - the result of `Plug.Conn.get_peer_data/1`
+
         * `:x_headers` - all request headers that have an "x-" prefix
+
         * `:uri` - a `%URI{}` with information from the conn
+
+        * `:user_agent` - the value of the "user-agent" request header
+
         * `{:session, session_config}` - the session information from `Plug.Conn`.
           The `session_config` is an exact copy of the arguments given to `Plug.Session`.
           This requires the "_csrf_token" to be given as request parameter with
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index 727dedd0c..ddd72b00b 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -450,10 +450,13 @@ defmodule Phoenix.Router do
       def __helpers__, do: __MODULE__.Helpers
 
       defp prepare(conn) do
-        update_in conn.private,
-          &(&1
-            |> Map.put(:phoenix_router, __MODULE__)
-            |> Map.put(__MODULE__, {conn.script_name, @phoenix_forwards}))
+        merge_private(
+          conn,
+          [
+            {:phoenix_router, __MODULE__},
+            {__MODULE__, {conn.script_name, @phoenix_forwards}}
+          ]
+        )
       end
 
       unquote(matches)
diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex
index 14b835258..45a63e2f6 100644
--- a/lib/phoenix/socket/transport.ex
+++ b/lib/phoenix/socket/transport.ex
@@ -248,7 +248,7 @@ defmodule Phoenix.Socket.Transport do
 
     connect_info =
       Enum.map(connect_info, fn
-        key when key in [:peer_data, :uri, :x_headers] ->
+        key when key in [:peer_data, :uri, :user_agent, :x_headers] ->
           key
 
         {:session, session} ->
@@ -431,9 +431,13 @@ defmodule Phoenix.Socket.Transport do
   The supported keys are:
 
     * `:peer_data` - the result of `Plug.Conn.get_peer_data/1`
+
     * `:x_headers` - a list of all request headers that have an "x-" prefix
+
     * `:uri` - a `%URI{}` derived from the conn
 
+    * `:user_agent` - the value of the "user-agent" request header
+
   """
   def connect_info(conn, endpoint, keys) do
     for key <- keys, into: %{} do
@@ -447,6 +451,9 @@ defmodule Phoenix.Socket.Transport do
         :uri ->
           {:uri, fetch_uri(conn)}
 
+        :user_agent ->
+          {:user_agent, fetch_user_agent(conn)}
+
         {:session, session} ->
           {:session, connect_session(conn, endpoint, session)}
 
@@ -519,17 +526,23 @@ defmodule Phoenix.Socket.Transport do
         do: pair
   end
 
-  defp fetch_uri(%{host: host, scheme: scheme, query_string: query_string, port: port, request_path: request_path}) do
+  defp fetch_uri(conn) do
     %URI{
-      scheme: to_string(scheme),
-      query: query_string,
-      port: port,
-      host: host,
-      authority: host,
-      path: request_path,
+      scheme: to_string(conn.scheme),
+      query: conn.query_string,
+      port: conn.port,
+      host: conn.host,
+      authority: conn.host,
+      path: conn.request_path
     }
   end
 
+  defp fetch_user_agent(conn) do
+    with {_, value} <- List.keyfind(conn.req_headers, "user-agent", 0) do
+      value
+    end
+  end
+
   defp check_origin_config(handler, endpoint, opts) do
     Phoenix.Config.cache(endpoint, {:check_origin, handler}, fn _ ->
       check_origin =


Test patch:
diff --git a/test/phoenix/integration/websocket_channels_test.exs b/test/phoenix/integration/websocket_channels_test.exs
index add5f9aa2..217f142f9 100644
--- a/test/phoenix/integration/websocket_channels_test.exs
+++ b/test/phoenix/integration/websocket_channels_test.exs
@@ -165,7 +165,14 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do
       websocket: [
         check_origin: ["//example.com"],
         timeout: 200,
-        connect_info: [:x_headers, :peer_data, :uri, session: @session_config, signing_salt: "salt"]
+        connect_info: [
+          :x_headers,
+          :peer_data,
+          :uri,
+          :user_agent,
+          session: @session_config,
+          signing_salt: "salt"
+        ]
       ]
 
     plug Plug.Session, @session_config
@@ -296,6 +303,28 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do
                                       "port" => 80}}}}
       end
 
+      test "transport user agent is extracted to the socket connect_info" do
+        extra_headers = [{"user-agent", "foo/1.0"}]
+
+        {:ok, sock} =
+          WebsocketClient.start_link(
+            self(),
+            "ws://127.0.0.1:#{@port}/ws/connect_info/websocket?vsn=#{@vsn}",
+            @serializer,
+            extra_headers
+          )
+        WebsocketClient.join(sock, lobby(), %{})
+
+        assert_receive %Message{
+          event: "joined",
+          payload: %{
+            "connect_info" => %{
+              "user_agent" => "foo/1.0"
+            }
+          }
+        }
+      end
+
       test "transport session is extracted to the socket connect_info" do
         import Phoenix.ConnTest
         path = "ws://127.0.0.1:#{@port}/ws/connect_info/websocket?vsn=#{@vsn}"