Yehuda Katz
Inside Rails: The Lifecycle of a Request
See all speakers
See all 4 speakers

Summarized using AI

Inside Rails: The Lifecycle of a Request

Yehuda Katz, Vaidehi Joshi, Godfrey Chan, and Krystan HuffMenne • May 28, 2019 • Minneapolis, MN

The video "Inside Rails: The Lifecycle of a Request" from RailsConf 2019 features speakers Yehuda Katz, Vaidehi Joshi, Godfrey Chan, and Krystan HuffMenne. This talk dives deep into the intricacies of how a web request is processed in a Ruby on Rails application, emphasizing the foundational concept that everything is interconnected via the Rack protocol.

  • Introduction to the Lifecycle: The session opens with an acknowledgment of the audience’s experience as Ruby developers and presents a fundamental question: how does a request transition from a browser input to an actionable method in Rails?

  • Understanding DNS and Server Connection: The initial steps involve translating a user-friendly domain (like skylight.io) into a machine-readable IP address through the Domain Name System (DNS) and establishing a connection using HTTP. The video details how this interaction functions at a high level, using the analogy of a phone call to illustrate communication protocols.

  • The Role of HTTP: HTTP (Hypertext Transfer Protocol) serves as the language for communication between browsers and web servers. The talk explains the essential structure of HTTP requests and responses, including how they are formatted and what elements they contain, thus reinforcing the protocol’s importance in web communication.

  • Request Handling by Web Servers: Various web servers, including Apache and Nginx, handle these requests, with a specific focus on how they manage to serve static and dynamic content, often routing requests to Rails applications when needed. The Rack protocol is positioned as a simple and uniform method for web servers to communicate with Ruby frameworks like Rails.

  • Middleware Concept: The speakers introduce the concept of middleware, a design pattern used in Rack and Rails that allows for the enhancement and processing of incoming requests and outgoing responses through reusable components. This segment highlights the importance of conventions in Rails that facilitate easier progress in web development tasks.

  • Routing in Rails: The talk explains the routing process in Rails, where incoming request URLs are matched to controller actions based on predefined rules. It elaborates on how Rails dynamically routes requests through its routes.rb configuration.

  • Integration of Skylight: The final part of the presentation ties everything together by discussing how Skylight, their performance monitoring tool for Rails applications, utilizes the established conventions and middleware to provide insights into request processing, emphasizing the value of community conventions in developing tools that streamline the developer experience.

The session concludes with a call for attendees to explore career opportunities with Skylight, reinforcing the collaborative spirit within the Rails community.

Inside Rails: The Lifecycle of a Request
Yehuda Katz, Vaidehi Joshi, Godfrey Chan, and Krystan HuffMenne • May 28, 2019 • Minneapolis, MN

RailsConf 2019 - Inside Rails: The Lifecycle of a Request by Yehuda Katz, Vaidehi Joshi, Godfrey Chan, & Krystan HuffMenne
_______________________________________________________________________________________________
Cloud 66 - Pain Free Rails Deployments
Cloud 66 for Rails acts like your in-house DevOps team to build, deploy and maintain your Rails applications on any cloud or server.

Get $100 Cloud 66 Free Credits with the code: RailsConf-19
($100 Cloud 66 Free Credits, for the new user only, valid till 31st December 2019)

Link to the website: https://cloud66.com/rails?utm_source=-&utm_medium=-&utm_campaign=RailsConf19
Link to sign up: https://app.cloud66.com/users/sign_in?utm_source=-&utm_medium=-&utm_campaign=RailsConf19
_______________________________________________________________________________________________
This is a sponsored talk by Skylight.

This breathtaking documentary series combines rare action, unimaginable scale, impossible locations and intimate moments captured from the depths of Rails' deepest internals. Together we will follow the lives of Rails' best loved, wildest and most elusive components. From the towering peaks of Rack to the lush green of Action Dispatch and the dry-sculpted crescents of Action Controller, our world is truly spectacular. Join the Skylight team on this incredible Journey to unearth the lifecycle of a Rails request.

RailsConf 2019

00:00:22.020 Well, I guess we didn't still see a staff; a little bit more people in there. I’m just checking, is everyone in the right room? In case you're not sure, this is a sponsored session. It means that we work for a company that sponsored the conference. That's why we did indeed have a call for papers process, so the talk might not be very good. Anyway, just kidding! It's always very flattering and I really appreciate it when people come out to support us.
00:00:40.080 We work for a company called Skylight. We make a Rails performance monitoring product called Skylight. You can find it at skylight.io, and if you can remember that URL, there's a promo code that you can use to get fifty bucks off, which is a pretty good deal. If you can’t remember that URL, no problem! We have a booth at the exhibit hall tomorrow and the day after, so you can just come talk to us or visit our booth for discounts and everything.
00:01:05.700 In case you haven’t seen Skylight before, this is what it looks like. But again, I’m not going to waste your time here; we’re just going to dive right into our talk. Okay, so who here has written a Rails app before? Just checking... cool! So most of the room here are Ruby programmers and Rails developers, right? You've probably written some code similar to this at some point, where you have a controller and methods like index or show. Inside these methods, it's just Ruby! We know how Ruby works. Maybe you put `Post.find` there, but we can understand that `find` is a class method, so `Post.find` causes time, effort, blah blah; it's Ruby through and through.
00:01:48.450 But have you ever wondered how we got here in the first place? Like, when you type `skylight.io/hello` in your browser, how does it make it all the way to your index method? Who calls that? How does a web request get turned into something inside Ruby? That's what this talk is going to be about. To tell you more about that, let's say you're meeting somebody for lunch. You may tell your co-workers at a certain place, but it probably won’t work for your out-of-town friend who's not familiar with the area. Instead, you can give them the street address so they can give it to their cab driver or ask anyone for directions.
00:02:31.309 Computers work much the same way. `skylight.io` is easier for us to remember, but it doesn't help your computer find the server. The first order of business is to connect to the server and figure out how to get there. They need to translate that name into an address. For computer networks, this is the IP address. You’ve probably come across it at some point. With this kind of address, computers can navigate the interconnected networks of the Internet and find their way to their destinations.
00:03:00.840 DNS, which stands for Domain Name System, helps our computers translate the domain name into the IP address they use to find the correct server. You can try it out for yourself with a command line tool called `dig` on your computer. The output looks a little intimidating, but the main thing to look at here is that it translates `skylight.io` into `234.194.84.73`. The DNS is a registry of domain names mapped to IP addresses. When you buy your own domain, you are in charge of setting up and maintaining this mapping, otherwise your customers can’t find you. Once we have the IP address of the server, our browser can connect to it.
00:04:18.509 The way the browser connects to the server is actually pretty interesting. You can think of opening a connection between the two like picking up a phone and dialing someone's number. In fact, there's another program on your computer called `telnet` that lets you open a raw connection to any server. Here we are connecting to the server we found earlier, on port 80, which is the default HTTP port. Once we have connected, we have to say something, but what do we say? Hi? Unfortunately, that doesn't get you very far. What do you do when you pick up the phone and it sounds like a spam call? You hang up.
00:05:04.360 So, let's try this again, but this time doing it properly. Browsers and servers have to agree on a language for communicating so that they can understand what one another is asking for. This is where HTTP comes in; it stands for Hypertext Transfer Protocol. This is the language that browsers and web servers understand to make the request for `skylight.io/hello`. Here is the simplest request we could make: it specifies that it is a GET request for the path `/hello`, using the HTTP protocol version 1.1 and is for the host `skylight.io`.
00:06:42.430 A simple response from the server looks like this: it specifies that the request was successful and includes a bunch of header information. Finally, we get 'hello world,' the text we’ve rendered from the controller. HTTP is a plaintext protocol as opposed to a binary protocol, which makes it easy for humans to learn, understand, and debug. It provides a structured way for the browser to ask for web pages and assets, submit forms, handle caching, compression, and other mundane details.
00:08:01.110 By the way, here's a side note: just like your phone line, the connection between the browser and the server is unencrypted. The request goes through many places to get to the other side, including the conference Wi-Fi, the convention center's routers, our internet provider, the hosting company, and many other intermediate networks. Between them, they help forward the request along to the right place. This means a lot of parties along the way have the opportunity to eavesdrop on the conversation. But maybe you don’t want others to know what kind of conversation you’re having.
00:09:06.590 No problem! You can just encrypt the contents of the conversation, and it will still pass through all of the same parties. They will still be able to see that you are sending each other messages, but those messages won’t make sense to them because only your browser and the server have the keys to decrypt them. This is known as HTTPS. The 's' makes it secure. But it's not a different protocol from HTTP; you're still speaking the same plaintext protocol that we saw earlier.
00:10:25.420 Before the browser sends the message out, it encrypts it, and before the server interprets the message, it decrypts it. The encryption and decryption are done by using a secret key that both the browser and server have agreed upon, which no one else knows about. But how did the browser and server pick what keys to use for encryption and decryption without giving them away while all the other parties are listening in? Well, that’s a topic for another talk. So, by now, your browser has successfully connected to the server and asked it for a specific web page. How did it generate the response? To tell you about that, let’s push to the stack!
00:12:13.160 Thank you, Zack! Alright, so that is a good question: how did it generate this response? First of all, we have to figure out what kind of server this is. Well, it's a web server, which really just means that it speaks HTTP, as we saw earlier. There are quite a few different examples of web servers: we have Apache, Nginx, Passenger, Lighttpd, Unicorn, Puma, and even Webrick. Some of them are written in Ruby, while others are written in C, and their job is mainly to parse, which just means understand what the request is.
00:12:48.940 They have to make a decision about how to service that request. For simple requests, like serving static assets, you can easily configure a web server to do that for you. For example, let’s say that we want to tell the web server that whenever a browser requests anything in assets, then the web server should try to find that file in my app's public assets folder. If it exists, it will serve that with compression; otherwise, it should return a 404 Not Found page.
00:13:50.700 Depending on which web server you're using, there are specific configuration syntaxes and languages that you might want to rely upon. For example, if you're using Nginx, you would write something that looks like this in your config file. However, for more complicated things, it gets trickier. For example, we might want to tell our web server that whenever a browser goes to `/blog`, we need to go to the database, find the 10 most recent blog posts, format them nicely, show some comments, and maybe add a header, footer, some JavaScript, and some CSS. Easy, right? Well, this is maybe a little too complicated to express in the web server's configuration language, but that's what we have Rails for.
00:15:25.980 What we want to do is tell our web server that it needs to hand these types of requests off to Rails for further processing. But how is the web server going to communicate with Rails? In Ruby, there are lots of different ways to communicate this kind of information. Rails could potentially register a block with the server, or the server could call a method on Rails, passing request information along as request arguments, environment variables, and maybe even a global variable.
00:16:52.060 If we go down this path, we have to consider what kind of object these would look like. And there's another question: how will Rails communicate back to the web server? Ultimately, all of these options actually work, but what’s more important is not which option you pick, but rather that everyone agrees on the same option. It's important that we all agree on the same conventions, which is why Rack was born. Rack presents a unified API for web servers to communicate with Ruby web frameworks and vice versa.
00:17:53.900 By implementing the Rack protocol, all web servers only need to implement a single adapter for Ruby, and any Ruby framework that conforms to this convention will work seamlessly with these web servers. So, Rack is just a simple Ruby protocol, which essentially means it's a convention.
00:19:04.420 Rack does a few different things: first, the web server needs to tell the web framework, 'Hey, I have a request for you to handle, and by the way, here are the details for the request,' including the path, the HTTP verb, and the headers. Then the framework needs to tell the server, 'It’s cool, I handled it!' and also provide the result, including the status code, headers, and the body.
00:20:17.250 To remain lightweight and framework-agnostic, Rack picked the simplest possible way to do this in Ruby. It notifies the web framework using a method call and communicates the details as method arguments. The web framework, in turn, communicates back by responding with some return value from that method call.
00:21:34.760 In code, it looks a little bit like this: we see the web server preparing a hash, which is conveniently called the env hash. The env hash contains all the headers and a path. For example, we’ll see that request method contains the HTTP verb (like GET or POST), path info contains the path, and HTTP underscore star has the corresponding header value.
00:23:14.110 An important thing here is that your app has to implement a `call` method, and we see convention at work here because the server is going to expect that `call` method to exist. The server invokes the app's `call` method with the env hash as the argument, and the app processes the request based on the information in the env hash. It will return an array with exactly three things in it. Pro tip: another way to refer to this is a tuple of three.
00:24:42.440 So what are the three things in this tuple of three? First, you have the HTTP status code, which is just a number; second, you have the headers hash; and finally, you have the body. Now, intuitively, we might think that the body should just be a string, but surprisingly, it’s actually an enumerable object. It just needs to implement the `each` method and yield strings in a very simple case. You can expect it to be returned as an array with a single string in it.
00:26:00.360 To guide us through the Rack API, I'm going to call on Godfrey. The goal of the talk is to understand Rails, but before we dive into Rails, let's try using this Rack API that we learned by itself. So let's say we write a simple Rack app. This is probably one of the simplest Rack apps you can write. You can see that as an object, it has a `call` method on it that takes an env hash. It checks what is in the env hash, specifically looking at the path of the request. If the path is `/hello`, it’s going to render a 200 response.
00:28:06.670 That's the status code; 200 stands for a successful request. As we said, we need to return an array with three things: the first is 200, the second is a hash of headers, in this case, telling the browser this is plain text, so don't bother trying to interpret it as HTML or anything like that. Finally, the third thing is an array of the body, in which case we’re returning plain text 'hello world.' If the path is anything other than `/hello`, we will do a 404 response. I'm sure you have encountered that in the past: '404' stands for 'not found,' right?
00:29:40.270 Now that we have written our app, how do we use it? How do we call it and make it do things? Rack is really just a convention—a protocol. You may have noticed that this didn't have to subclass from anything specific; it can just be any object that responds to `call`. To hook that up to an HTTP request, you need a web server again, as we talked about earlier. We can hook this up with Nginx or Apache. Fortunately for this simple demonstration, Rack came with a pre-built script, which is like a web server that natively understands the Rack protocol, called `rackup`.
00:30:58.410 To configure the web server, you can use a `config.ru` file—basically a Ruby file with some extra DSL. The first thing we do in this `config.ru` file is require the file that we have up there, and the only other thing in there is the command to run `HelloWorld.new.` The `run` is the keyword in the DSL that tells Rackup, 'Hey, this is the app'—the thing on the right-hand side, which is `HelloWorld.new`, an object with a `call` method on it. To run it, just execute the command `rackup` from the same directory where `config.ru` is.
00:32:20.890 Under the hood, Rackup just wraps whatever Ruby web server you have. Sometimes it uses Thin; sometimes it uses Webrick—that's just implementation details. As far as we’re concerned, Rackup is a web server that understands `config.ru`, which understands Rack apps. So now if we open the browser and go to `localhost:9292/hello`, we are going to get the 'hello world' response. If we go to anything else (like `/notfound`), we’re going to get our not found.
00:33:26.200 Great! We’ve written a Rack app! Now let’s make it do a little bit more. Let’s say we want that when you go to `localhost:9292`, you get a not found. This is because `/` is not `/hello`, so why don’t we make `/` redirect to `/hello`? To do that, you would modify your `rackup` to check if the path info is `/`. If it is, we return a 301 response code, which is the response code for a permanent redirect, and in the headers, we return the location of where to redirect to: `/hello`.
00:34:24.450 This works great! If you open the browser and go to `/`, it will redirect you to `/hello`. However, as you can imagine, if you keep adding things here, this `if` statement is going to get pretty big. As you add more pages, you’ll have maybe a hundred lines of `if` statements. Redirecting is a pretty common task that you might want to use in different parts of your app, so it would be great if we could extract this into a reusable and composable piece of code.
00:35:47.690 We can achieve this with a new class we can call `Redirect`, which is only responsible for handling the redirect aspect of the app. In the `call` method, we first check if the path matches what we want to redirect from. If it does, we intercept the request, return a response, and tell the browser to redirect. If it doesn’t match, we'll delegate to the app that is passed in to do further processing. In the configuration, instead of passing `HelloWorld.new` to `run`, we’re going to pass `Redirect.new` but pass `HelloWorld.new` to that `Redirect` app.
00:36:45.130 This is great, and it works! In fact, we just made our first Rack middleware. You might have heard the term before; this is exactly what it is. Middleware is not necessarily a concrete concept in Rack’s back; it’s just a very useful convention. As far as the web server is concerned, there’s no middleware; there’s just one app with a `call` method on it. What you choose to do inside your `call` is up to you. In the case of middleware, it just happened to choose to sometimes forward it to a different app.
00:37:40.380 The Rackup config DSL has a special syntax for middleware because of how useful this convention is. The only problem is that the nesting can get a bit cumbersome. Imagine if we want a lot of middleware; the right-hand side is going to look like a very deeply nested thing. The Rackup DSL has a way for you to flatten it. Instead of `run`, you can use `use` to specify a list of middlewares before you specify the app. This is pretty powerful! The Rack gem itself ships with a bunch of very useful middleware that you can add to your app.
00:39:15.540 So, for example, with something like this, you can add compression to your app, HTTP caching, and handle HEAD responses. That’s the power of middleware convention! To recap: this is the whole Rack concept we’ve covered, and now it’s time to return to Rails. The point of this detour into Rack is to show that web servers use the Rack protocol to communicate with Ruby web frameworks. Therefore, it must mean that Rails implements the Rack protocol; otherwise, what’s the point of this detour?
00:40:39.230 And it does indeed! If you look at your Rails app, you will see that it generates a `config.ru` file for you. You can check it out in your work directory later, but it probably looks something like this: it has one thing required, and has one single line that says `run Rails.application`. So we know that the convention for how the Rackup DSL works is the thing it has to run must be a Rack app. So it must mean that `Rails.application` is a Rack app!
00:42:41.350 So let’s try it! You can do this by running the Rails console and simply running `Rails.application.call`. But the problem is, to call a Rack app, we obviously have to pass it the env hash. We can just build this hash by hand, but that’s a lot of work and quite error-prone. You would need to read the Rack specification carefully to ensure you’re passing all the expected elements in there. Fortunately, Rack has a utility for this purpose called `Rack::MockRequest`. You can pass it a URL, and it will build the corresponding env hash for you, which is very useful for testing and other tasks.
00:43:58.170 So now that we have the env hash, we can try calling `Rails.application.call` again. If we do that, you will see that it does what you expect. It might be a little bit surprising, but it actually works, right? When you call `Rails.application`, it actually runs the entire Rails app, giving you the logging output that you’re probably familiar with. It returns an array of three things; if you're not familiar with that syntax, it's how you can conveniently destructure the array from the return value.
00:45:12.040 If you check the status, you’ll find it’s 200. The headers contain lots of things, including the content type and several other headers that Rails has added for you. If you look at the body, again, it's an array, so it takes a little more work to examine, but if you print it out, you'll see that it’s the HTML response that you expect. So that's pretty cool! We found the Rails app-Rack app integration point, but if you look at `config.ru`, you might remember that earlier we said there are two things you can include in a configuration file: you can put `run`, and you also should add your middlewares there.
00:46:41.350 You will see there’s no `use` keyword, so Rails does not use any middleware in the same way. However, if you want to look at the Rails middleware, you can run `bin/rails middleware` to show you all the middleware in the proper syntax. You can observe that Rails implements some of its internal features in middleware.
00:48:12.100 This is quite beneficial because if you don't need some of those features, you can remove them. For example, if you're writing an API server, you might not be using cookies, so it doesn't mean you're stuck with it. You can simply delete it from your configuration. In `config/application.rb`, you might use the `config.middleware.delete` method, and you will have removed the part of Rails that concerns cookies. So this seems great. You can, of course, also add your own middleware as well.
00:49:24.100 The last part is, we’ve looked at all the middleware, but what about how it gets to the controller action? For that, we have Krystan to explain. So we know that `use` is for middleware and `run` is for the app, but what is `Rails.application.routes`? We know it should be a Rack app, so let’s try it. Yep, it’s a Rack app! As you can see, we call `Blorg.application.routes` instead of `Rails.application.call`, but otherwise, it’s the exact same example as before.
00:50:31.760 So what does this Rack app do? It looks at the URL and matches it against a bunch of routing rules to find the right controller action to call. It’s generated from your `config/routes.rb` file. I hope that looks familiar! The resources DSL is a shorthand for defining routes at once; ultimately, it expands into these seven routes. For example, when you make a GET request to `/posts`, it will call the `PostController#index` method. If you make a PUT request to `/posts/:id`, it will go to the `PostController#update` method instead.
00:52:26.720 So, what is this `posts#index`? We know it stands for the index action on the post controller. If you follow that code, you will see that it eventually expands into `PostController.action(:index)`. What is that? Here’s a much simplified version of the action controller code. First of all, we have the action method; it returns a lambda. The lambda takes an argument called `params`. What’s that? Surprise! It’s a hash. What does the lambda return? An array. And by the way, surprise! Lambdas respond to `call`. Yep, it’s a Rack app! Everything is a Rack app!
00:53:36.920 Finally, putting everything together, you can imagine that the routes app is a Rack app that matches the given request path and HTTP verb against the rules defined in your routes config. It then delegates the request to the appropriate controller's action. Good thing you don’t have to write this by hand! Thanks, Rails! Now, you might be wondering how Rails generates routes efficiently from your routes config. Well, there’s a talk for that! Check out Vaidehi’s talk from last year at RailsConf for more information.
00:54:48.910 Now we know that 'everything is a Rack app,' and we can mix and match things. Here's some pro tips: did you know that you can route a part of your Rails app to a Rack app just like that? In fact, now that we learned about lambdas, you can even write that inline. You may think it's a terrible idea, but you've probably used this functionality before. How do you think `redirect_to` works? Surprise! It's a Rack app. You can even mount a Sinatra app inside a Rails app. You may not know this, but the Sidekiq web UI is written in Sinatra, so you may already have a Sinatra app running inside your Rails app.
00:56:17.120 Of course, you can also go the other way around and mount a Rails app inside your Sinatra app. But we’ll leave that to your imagination! Remember that `controller.action` method? I wouldn’t actually recommend doing this in your Rails app because it bypasses autoload and some performance optimizations, but it’s really cool to know how everything fits together!
00:57:39.940 Okay, after all that, we've finally made it back to the controller action we started with. To summarize, everything is a Rack app. Oh wait, how does that `render plain` thing work? Well, maybe come to a talk next year; we’re running out of time! Speaking of talks, here’s a chance to talk about how Skylight fits into this story!
00:58:58.780 So, as we learned so far, frameworks aren't magic; they are just a layer of conventions that live on top of a nice primitive under the hood that works really well. Conventions can help you learn and can help you collaborate with your team. We all love conventions, or we wouldn't be here! More than that, conventions give the community the ability to write tools that everyone can share.
01:00:45.080 For example, Skylight needs to wrap around our entire request so we know how long it took. Well, if we’ve been paying attention, we know how to do that: we use middleware. What better way than a middleware? Convention over configuration is more about how you build Rails apps; it also allows Skylight to give you a lot of detailed information without having to configure anything.
01:02:29.580 In a lot of ecosystems, tools like Skylight would require you to do a bunch of work to get things running, but because Rails provides such good conventions, we can leverage those to make your life easier. Earlier, we saw how Rails dispatches actions, but we skipped over this part at the bottom.
01:04:06.180 You may or may not know this, but Rails has a built-in instrumentation system, and we can get detailed information about what’s going on inside our requests without hacking into private APIs. For example, the section over here is how we get the name of your endpoints without you having to tell us anything about what they're called.
01:05:23.360 Now Skylight does way more than just give you the average of the response time for your requests. We provide detailed aggregate traces of your entire request. We leverage Active Support notifications and Rails conventions to provide information for Rails and for common gems without any configuration by default. We also show you important parts of the request so you can focus on speeding up what matters.
01:06:32.570 If you look at this example over here, that big, I don’t know if you can see the color but I think it’s purple, you should focus on the app serializer because that’s taking up so much of the time. If you want to speed up this endpoint, look at it! We still collect more information than what we show by default,
01:08:05.380 So if you uncheck the condensed trace checkbox at the top, every middleware we saw before is here in the expanded trace. As you can see from this screen, everything is a Rack app. There's way too much to tell you about in this talk, like the fact that we do a ton of our work in Rust. And by the way, we have official background job support this year. But come to our booth to hear more about that.
01:09:56.600 Okay, I will do this part. That’s the lifecycle of a request! I hope that was interesting for you! And like Krystan said, maybe next year we’ll do a lifecycle of a response or something.
01:10:16.490 Yes, we're hiring! If digging deep into Rails and integrating with Rails to measure people's apps interests you, we are hiring! We have great perks! You can come meet us at the booth!
01:11:39.905 We are a pretty small company; we have seven people in total. The downside of that is when one or two people can’t make it, it's like 20 or 40 percent of your company, but that’s also the fun part. You get to know each other really well and work closely together.
01:12:25.000 I look forward to seeing you at the booth tomorrow and signing up for Skylight. Thank you very much!
Explore all talks recorded at RailsConf 2019
+102