eyeloupe 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +129 -0
  5. data/Rakefile +8 -0
  6. data/app/assets/builds/eyeloupe.css +1 -0
  7. data/app/assets/config/eyeloupe_manifest.js +3 -0
  8. data/app/assets/images/eyeloupe/logo.png +0 -0
  9. data/app/assets/javascripts/eyeloupe/application.js +5 -0
  10. data/app/assets/javascripts/eyeloupe/controllers/application.js +9 -0
  11. data/app/assets/javascripts/eyeloupe/controllers/eyeloupe/nav_controller.js +13 -0
  12. data/app/assets/javascripts/eyeloupe/controllers/eyeloupe/pause_controller.js +29 -0
  13. data/app/assets/javascripts/eyeloupe/controllers/eyeloupe/refresh_controller.js +53 -0
  14. data/app/assets/javascripts/eyeloupe/controllers/eyeloupe/search_controller.js +11 -0
  15. data/app/assets/javascripts/eyeloupe/controllers/index.js +11 -0
  16. data/app/assets/stylesheets/application.tailwind.css +42 -0
  17. data/app/assets/stylesheets/eyeloupe/application.css +16 -0
  18. data/app/controllers/concerns/eyeloupe/searchable.rb +20 -0
  19. data/app/controllers/eyeloupe/application_controller.rb +17 -0
  20. data/app/controllers/eyeloupe/configs_controller.rb +20 -0
  21. data/app/controllers/eyeloupe/data_controller.rb +15 -0
  22. data/app/controllers/eyeloupe/in_requests_controller.rb +24 -0
  23. data/app/controllers/eyeloupe/out_requests_controller.rb +27 -0
  24. data/app/helpers/eyeloupe/application_helper.rb +5 -0
  25. data/app/jobs/eyeloupe/application_job.rb +4 -0
  26. data/app/mailers/eyeloupe/application_mailer.rb +6 -0
  27. data/app/models/eyeloupe/application_record.rb +5 -0
  28. data/app/models/eyeloupe/in_request.rb +4 -0
  29. data/app/models/eyeloupe/out_request.rb +4 -0
  30. data/app/views/eyeloupe/in_requests/_frame.html.erb +44 -0
  31. data/app/views/eyeloupe/in_requests/index.html.erb +18 -0
  32. data/app/views/eyeloupe/in_requests/show.html.erb +82 -0
  33. data/app/views/eyeloupe/out_requests/_frame.html.erb +46 -0
  34. data/app/views/eyeloupe/out_requests/index.html.erb +18 -0
  35. data/app/views/eyeloupe/out_requests/show.html.erb +69 -0
  36. data/app/views/eyeloupe/shared/_status_code.html.erb +21 -0
  37. data/app/views/eyeloupe/shared/_verb.html.erb +17 -0
  38. data/app/views/layouts/eyeloupe/application.html.erb +203 -0
  39. data/config/importmap.rb +4 -0
  40. data/config/routes.rb +12 -0
  41. data/config/tailwind.config.js +22 -0
  42. data/db/migrate/20230518175305_create_eyeloupe_in_requests.rb +22 -0
  43. data/db/migrate/20230525125352_create_eyeloupe_out_requests.rb +18 -0
  44. data/lib/eyeloupe/configuration.rb +20 -0
  45. data/lib/eyeloupe/engine.rb +21 -0
  46. data/lib/eyeloupe/http.rb +19 -0
  47. data/lib/eyeloupe/processors/in_request.rb +148 -0
  48. data/lib/eyeloupe/processors/out_request.rb +76 -0
  49. data/lib/eyeloupe/request_middleware.rb +72 -0
  50. data/lib/eyeloupe/version.rb +4 -0
  51. data/lib/eyeloupe.rb +21 -0
  52. data/lib/tasks/eyeloupe_tasks.rake +9 -0
  53. metadata +195 -0
@@ -0,0 +1,18 @@
1
+ <div data-controller="eyeloupe--search">
2
+ <div class="sm:flex sm:items-center">
3
+ <div class="sm:flex-auto">
4
+ <h1 class="text-xl font-semibold leading-6 text-gray-900">Requests</h1>
5
+ <p class="mt-2 text-sm text-gray-700">All incoming request to your application</p>
6
+ </div>
7
+ <div>
8
+ <form method="get" data-action="eyeloupe--search#submit">
9
+ <input type="hidden" name="frame" value="frame" />
10
+ <label for="q" class="sr-only">Search</label>
11
+ <input type="text" id="q" name="q" value="<%= params[:q] %>" placeholder="Search for path" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm" />
12
+ </form>
13
+ </div>
14
+ </div>
15
+ <div class="mt-8 flow-root">
16
+ <%= turbo_frame_tag "frame", src: in_requests_path(frame: true), data: {"eyeloupe--refresh-target": "frame", "eyeloupe--search-target": "frame"} do %><% end %>
17
+ </div>
18
+ </div>
@@ -0,0 +1,82 @@
1
+ <div>
2
+ <div class="px-4 sm:px-0">
3
+ <h3 class="text-xl font-semibold leading-7 text-gray-900">Request details</h3>
4
+ </div>
5
+ <div class="mt-6 border-t border-gray-100">
6
+ <dl class="divide-y divide-gray-100">
7
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
8
+ <dt class="text-base font-medium leading-6 text-gray-900">Time</dt>
9
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
10
+ <%= @request.created_at.to_formatted_s(:long) %> (<%= distance_of_time_in_words(@request.created_at, DateTime.now) %>)
11
+ </dd>
12
+ </div>
13
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
14
+ <dt class="text-base font-medium leading-6 text-gray-900">Hostname</dt>
15
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.hostname %></dd>
16
+ </div>
17
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
18
+ <dt class="text-base font-medium leading-6 text-gray-900">Method</dt>
19
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
20
+ <%= render "eyeloupe/shared/verb", verb: @request.verb %>
21
+ </dd>
22
+ </div>
23
+ <% if @request.controller.present? %>
24
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
25
+ <dt class="text-base font-medium leading-6 text-gray-900">Controller</dt>
26
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.controller %></dd>
27
+ </div>
28
+ <% end %>
29
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
30
+ <dt class="text-base font-medium leading-6 text-gray-900">Path</dt>
31
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.path %></dd>
32
+ </div>
33
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
34
+ <dt class="text-base font-medium leading-6 text-gray-900">Status</dt>
35
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
36
+ <%= render "eyeloupe/shared/status_code", code: @request.status %>
37
+ </dd>
38
+ </div>
39
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
40
+ <dt class="text-base font-medium leading-6 text-gray-900">Duration</dt>
41
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
42
+ <%= @request.duration %> ms
43
+ <% if @request.db_duration.present? && @request.view_duration.present? %>
44
+ (ActiveRecord: <%= @request.db_duration %> ms, Views: <%= @request.view_duration %> ms)
45
+ <% end %>
46
+ </dd>
47
+ </div>
48
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
49
+ <dt class="text-base font-medium leading-6 text-gray-900">IP Address</dt>
50
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.ip %></dd>
51
+ </div>
52
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
53
+ <dt class="text-base font-medium leading-6 text-gray-900">Payload</dt>
54
+ <dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
55
+ <% if @request.payload.present? %>
56
+ <pre class="p-2"><%= @request.payload %></pre>
57
+ <% else %>
58
+ <p class="text-gray-400 p-2">No payload</p>
59
+ <% end %>
60
+ </dd>
61
+ </div>
62
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
63
+ <dt class="text-base font-medium leading-6 text-gray-900">Headers</dt>
64
+ <dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
65
+ <pre class="p-2"><%= JSON.pretty_generate(JSON.parse(@request.headers || "{}")) %></pre>
66
+ </dd>
67
+ </div>
68
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
69
+ <dt class="text-base font-medium leading-6 text-gray-900">Session</dt>
70
+ <dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
71
+ <pre class="p-2" ><%= JSON.pretty_generate(JSON.parse(@request.session || "{}")) %></pre>
72
+ </dd>
73
+ </div>
74
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
75
+ <dt class="text-base font-medium leading-6 text-gray-900">Response</dt>
76
+ <dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
77
+ <pre class="p-2"><%= @request.format == "application/json" ? JSON.pretty_generate(JSON.parse(@request.response || "{}")) : @request.response %></pre>
78
+ </dd>
79
+ </div>
80
+ </dl>
81
+ </div>
82
+ </div>
@@ -0,0 +1,46 @@
1
+ <%= turbo_frame_tag "frame" do %>
2
+ <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
3
+ <div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
4
+
5
+ <table class="min-w-full divide-y divide-gray-300">
6
+ <thead>
7
+ <tr>
8
+ <th scope="col" class="py-3 pl-4 pr-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500 sm:pl-0">Verb</th>
9
+ <th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Host</th>
10
+ <th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Path</th>
11
+ <th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Status</th>
12
+ <th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Duration</th>
13
+ <th scope="col" class="px-3 py-3 text-left text-sm font-medium uppercase tracking-wide text-gray-500">Occurred</th>
14
+ <th scope="col" class="relative py-3 pl-3 pr-4 sm:pr-0">
15
+ <span class="sr-only">Details</span>
16
+ </th>
17
+ </tr>
18
+ </thead>
19
+ <tbody class="divide-y divide-gray-200">
20
+ <% @requests.each do |request| %>
21
+ <tr>
22
+ <td class="whitespace-nowrap py-4 pl-4 pr-3 text-base font-medium text-gray-900 sm:pl-0">
23
+ <%= render "eyeloupe/shared/verb", verb: request.verb %>
24
+ </td>
25
+ <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= request.hostname %></td>
26
+ <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= request.path.truncate(50) %></td>
27
+ <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500">
28
+ <%= render "eyeloupe/shared/status_code", code: request.status %>
29
+ </td>
30
+ <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= request.duration %> ms</td>
31
+ <td class="whitespace-nowrap px-3 py-4 text-base text-gray-500"><%= distance_of_time_in_words(request.created_at, DateTime.now) %></td>
32
+ <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-base font-medium sm:pr-0">
33
+ <%= link_to "Details", out_request_path(request), class: "text-gray-600 hover:text-gray-900", data: {"turbo_frame": "_top"} %>
34
+ </td>
35
+ </tr>
36
+ <% end %>
37
+ </tbody>
38
+ </table>
39
+ </div>
40
+ <aside class="mt-4 px-4 py-3 flex items-center justify-center sm:px-6" aria-label="Pagination">
41
+ <div class="flex-1 flex justify-center">
42
+ <%== pagy_nav(@pagy) %>
43
+ </div>
44
+ </aside>
45
+ </div>
46
+ <% end %>
@@ -0,0 +1,18 @@
1
+ <div data-controller="eyeloupe--search">
2
+ <div class="sm:flex sm:items-center">
3
+ <div class="sm:flex-auto">
4
+ <h1 class="text-xl font-semibold leading-6 text-gray-900">HTTP Client</h1>
5
+ <p class="mt-2 text-sm text-gray-700">All outbound HTTP requests made by your application</p>
6
+ </div>
7
+ <div>
8
+ <form method="get" data-action="eyeloupe--search#submit">
9
+ <input type="hidden" name="frame" value="frame" />
10
+ <label for="q" class="sr-only">Search</label>
11
+ <input type="text" id="q" name="q" value="<%= params[:q] %>" placeholder="Search for path" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-red-500 focus:border-red-500 sm:text-sm" />
12
+ </form>
13
+ </div>
14
+ </div>
15
+ <div class="mt-8 flow-root">
16
+ <%= turbo_frame_tag "frame", src: out_requests_path(frame: true), data: {"eyeloupe--refresh-target": "frame", "eyeloupe--search-target": "frame"} do %><% end %>
17
+ </div>
18
+ </div>
@@ -0,0 +1,69 @@
1
+ <div>
2
+ <div class="px-4 sm:px-0">
3
+ <h3 class="text-xl font-semibold leading-7 text-gray-900">HTTP Client Request Details</h3>
4
+ </div>
5
+ <div class="mt-6 border-t border-gray-100">
6
+ <dl class="divide-y divide-gray-100">
7
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
8
+ <dt class="text-base font-medium leading-6 text-gray-900">Time</dt>
9
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
10
+ <%= @request.created_at.to_formatted_s(:long) %> (<%= distance_of_time_in_words(@request.created_at, DateTime.now) %>)
11
+ </dd>
12
+ </div>
13
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
14
+ <dt class="text-base font-medium leading-6 text-gray-900">Hostname</dt>
15
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.hostname %></dd>
16
+ </div>
17
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
18
+ <dt class="text-base font-medium leading-6 text-gray-900">Method</dt>
19
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
20
+ <%= render "eyeloupe/shared/verb", verb: @request.verb %>
21
+ </dd>
22
+ </div>
23
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
24
+ <dt class="text-base font-medium leading-6 text-gray-900">Path</dt>
25
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0"><%= @request.path %></dd>
26
+ </div>
27
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
28
+ <dt class="text-base font-medium leading-6 text-gray-900">Status</dt>
29
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
30
+ <%= render "eyeloupe/shared/status_code", code: @request.status %>
31
+ </dd>
32
+ </div>
33
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
34
+ <dt class="text-base font-medium leading-6 text-gray-900">Duration</dt>
35
+ <dd class="mt-1 text-base leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
36
+ <%= @request.duration %> ms
37
+ </dd>
38
+ </div>
39
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
40
+ <dt class="text-base font-medium leading-6 text-gray-900">Payload</dt>
41
+ <dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
42
+ <% if @request.payload.present? %>
43
+ <pre class="p-2"><%= @request.payload %></pre>
44
+ <% else %>
45
+ <p class="text-gray-400 p-2">No payload</p>
46
+ <% end %>
47
+ </dd>
48
+ </div>
49
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
50
+ <dt class="text-base font-medium leading-6 text-gray-900">Request headers</dt>
51
+ <dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
52
+ <pre class="p-2"><%= JSON.pretty_generate(JSON.parse(@request.req_headers || "{}")) %></pre>
53
+ </dd>
54
+ </div>
55
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
56
+ <dt class="text-base font-medium leading-6 text-gray-900">Response headers</dt>
57
+ <dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
58
+ <pre class="p-2"><%= JSON.pretty_generate(JSON.parse(@request.res_headers || "{}")) %></pre>
59
+ </dd>
60
+ </div>
61
+ <div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
62
+ <dt class="text-base font-medium leading-6 text-gray-900">Response</dt>
63
+ <dd class="mt-1 text-base leading-6 sm:col-span-2 sm:mt-0 rounded-md bg-black text-white overflow-x-auto">
64
+ <pre class="p-2"><%= @request.format == "application/json" ? JSON.pretty_generate(JSON.parse(@request.response || "{}")) : @request.response %></pre>
65
+ </dd>
66
+ </div>
67
+ </dl>
68
+ </div>
69
+ </div>
@@ -0,0 +1,21 @@
1
+ <% if code.to_s[0] == "2" %>
2
+ <span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-base font-medium text-green-700 ring-1 ring-inset ring-green-600/20">
3
+ <%= code %>
4
+ </span>
5
+ <% elsif code.to_s[0] == "3" %>
6
+ <span class="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-base font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
7
+ <%= code %>
8
+ </span>
9
+ <% elsif code.to_s[0] == "4" %>
10
+ <span class="inline-flex items-center rounded-md bg-yellow-50 px-2 py-1 text-base font-medium text-yellow-800 ring-1 ring-inset ring-yellow-600/20">
11
+ <%= code %>
12
+ </span>
13
+ <% elsif code.to_s[0] == "5" %>
14
+ <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-base font-medium text-red-700 ring-1 ring-inset ring-red-600/10">
15
+ <%= code %>
16
+ </span>
17
+ <% else %>
18
+ <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-base font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">
19
+ <%= code %>
20
+ </span>
21
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <% if verb.downcase == "post" || verb.downcase == "patch" || verb.downcase == "put" %>
2
+ <span class="inline-flex items-center rounded-md bg-blue-50 px-2 py-1 text-base font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10">
3
+ <%= verb %>
4
+ </span>
5
+ <% elsif verb.downcase == "get" || verb.downcase == "options" %>
6
+ <span class="inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-base font-medium text-gray-600 ring-1 ring-inset ring-gray-500">
7
+ <%= verb %>
8
+ </span>
9
+ <% elsif verb.downcase == "delete" %>
10
+ <span class="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-base font-medium text-red-700 ring-1 ring-inset ring-red-600/10">
11
+ <%= verb %>
12
+ </span>
13
+ <% else %>
14
+ <span class="inline-flex items-center rounded-md bg-purple-50 px-2 py-1 text-base font-medium text-purple-700 ring-1 ring-inset ring-purple-700/10">
15
+ <%= verb %>
16
+ </span>
17
+ <% end %>
@@ -0,0 +1,203 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Eyeloupe</title>
5
+ <%= csrf_meta_tags %>
6
+ <%= csp_meta_tag %>
7
+ <%= javascript_importmap_tags "eyeloupe/application" %>
8
+
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Fira+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
12
+
13
+ <%= stylesheet_link_tag "eyeloupe", media: "all" %>
14
+ </head>
15
+ <body class="bg-gray-50">
16
+
17
+ <div data-controller="eyeloupe--nav">
18
+ <div data-controller="eyeloupe--refresh">
19
+ <!-- Off-canvas menu for mobile, show/hide based on off-canvas menu state. -->
20
+ <div class="relative z-50 lg:hidden hidden" role="dialog" aria-modal="true" data-eyeloupe--nav-target="content">
21
+ <div class="fixed inset-0 bg-gray-900/80"></div>
22
+
23
+ <div class="fixed inset-0 flex">
24
+ <div class="relative mr-16 flex w-full max-w-xs flex-1">
25
+ <div class="absolute left-full top-0 flex w-16 justify-center pt-5">
26
+ <button data-action="eyeloupe--nav#close" type="button" class="-m-2.5 p-2.5">
27
+ <span class="sr-only">Close sidebar</span>
28
+ <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
29
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
30
+ </svg>
31
+ </button>
32
+ </div>
33
+
34
+
35
+ <div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-50 px-6 pb-2">
36
+ <%= link_to root_url, class:"flex h-16 shrink-0 items-center" do %>
37
+ <%= image_tag "eyeloupe/logo.png", class: "h-7 w-auto" %>
38
+ <h1 class="ml-2 text-xl font-semibold text-gray-700">Eyeloupe</h1>
39
+ <% end %>
40
+ <nav class="flex flex-1 flex-col">
41
+ <ul role="list" class="flex flex-1 flex-col gap-y-7">
42
+ <li>
43
+ <ul role="list" class="-mx-2 space-y-1">
44
+ <li>
45
+ <%= link_to in_requests_path, class:"#{request.path.include?('/in_requests') ? 'bg-gray-200 text-red-500' : ''} hover:bg-gray-200 text-gray-500 group flex gap-x-3 rounded-md p-2 text-base leading-6 font-medium" do %>
46
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
47
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
48
+ <path d="M21 17l-18 0"></path>
49
+ <path d="M6 10l-3 -3l3 -3"></path>
50
+ <path d="M3 7l18 0"></path>
51
+ <path d="M18 20l3 -3l-3 -3"></path>
52
+ </svg>
53
+ Requests
54
+ <% end %>
55
+ </li>
56
+ <li>
57
+ <%= link_to out_requests_path, class:"#{request.path.include?('/out_requests') ? 'bg-gray-200 text-red-500' : ''} hover:bg-gray-200 text-gray-500 group flex gap-x-3 rounded-md p-2 text-base leading-6 font-medium" do %>
58
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
59
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
60
+ <path d="M21 12a9 9 0 1 0 -9 9"></path>
61
+ <path d="M3.6 9h16.8"></path>
62
+ <path d="M3.6 15h8.4"></path>
63
+ <path d="M11.578 3a17 17 0 0 0 0 18"></path>
64
+ <path d="M12.5 3c1.719 2.755 2.5 5.876 2.5 9"></path>
65
+ <path d="M18 21v-7m3 3l-3 -3l-3 3"></path>
66
+ </svg>
67
+ HTTP Client
68
+ <% end %>
69
+ </li>
70
+ </ul>
71
+ </li>
72
+ </ul>
73
+ </nav>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <!-- Static sidebar for desktop -->
80
+ <div class="mx-auto">
81
+ <div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
82
+ <div class="flex grow flex-col gap-y-5 overflow-y-auto bg-gray-50 px-6">
83
+ <%= link_to root_path, class:"flex h-16 shrink-0 items-center" do %>
84
+ <%= image_tag "eyeloupe/logo.png", class: "h-12 w-auto" %>
85
+ <h1 class="ml-2 text-2xl font-semibold text-gray-700">Eyeloupe</h1>
86
+ <% end %>
87
+ <nav class="flex flex-1 flex-col">
88
+ <ul role="list" class="flex flex-1 flex-col gap-y-7">
89
+ <li>
90
+ <ul role="list" class="-mx-2 space-y-1">
91
+ <li>
92
+ <%= link_to in_requests_path, class:"#{request.path.include?('/in_requests') ? 'bg-gray-200 text-red-500' : ''} hover:bg-gray-200 text-gray-500 group flex gap-x-3 rounded-md p-2 text-base leading-6 font-medium" do %>
93
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
94
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
95
+ <path d="M21 17l-18 0"></path>
96
+ <path d="M6 10l-3 -3l3 -3"></path>
97
+ <path d="M3 7l18 0"></path>
98
+ <path d="M18 20l3 -3l-3 -3"></path>
99
+ </svg>
100
+ Requests
101
+ <% end %>
102
+ </li>
103
+ <li>
104
+ <%= link_to out_requests_path, class:"#{request.path.include?('/out_requests') ? 'bg-gray-200 text-red-500' : ''} hover:bg-gray-200 text-gray-500 group flex gap-x-3 rounded-md p-2 text-base leading-6 font-medium" do %>
105
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
106
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
107
+ <path d="M21 12a9 9 0 1 0 -9 9"></path>
108
+ <path d="M3.6 9h16.8"></path>
109
+ <path d="M3.6 15h8.4"></path>
110
+ <path d="M11.578 3a17 17 0 0 0 0 18"></path>
111
+ <path d="M12.5 3c1.719 2.755 2.5 5.876 2.5 9"></path>
112
+ <path d="M18 21v-7m3 3l-3 -3l-3 3"></path>
113
+ </svg>
114
+ HTTP Client
115
+ <% end %>
116
+ </li>
117
+ </ul>
118
+ </li>
119
+ </ul>
120
+ </nav>
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ <div class="hidden lg:flex py-4 justify-end items-end px-4 gap-x-2">
126
+ <%= link_to configs_path(value: "#{!@eyeloupe_capture}"), title: "Pause data collection", data: { "turbo_method": "put" }, class: "#{@eyeloupe_capture ? "text-gray-500 hover:bg-gray-300 bg-gray-200" : "bg-red-500 text-white hover:bg-red-600"} rounded-md p-1 transition duration-500 inline-block hidden" do %>
127
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
128
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
129
+ <path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
130
+ <path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
131
+ </svg>
132
+ <% end %>
133
+ <button title="Auto refresh data" data-action="eyeloupe--refresh#toggle" data-eyeloupe--refresh-target="btn" class="rounded-md text-gray-500 hover:bg-gray-300 bg-gray-200 p-1 transition duration-500 inline-block">
134
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
135
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
136
+ <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
137
+ <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
138
+ </svg>
139
+ </button>
140
+ <%= link_to data_path, title: "Delete all Eyeloupe data", data: { "turbo_method": "delete", "turbo_confirm": "Are you sure to delete all Eyeloupe data?" }, class: "rounded-md text-gray-500 hover:bg-gray-300 bg-gray-200 p-1 transition duration-500 inline-block" do %>
141
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
142
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
143
+ <path d="M4 7l16 0"></path>
144
+ <path d="M10 11l0 6"></path>
145
+ <path d="M14 11l0 6"></path>
146
+ <path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path>
147
+ <path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path>
148
+ </svg>
149
+ <% end %>
150
+ </div>
151
+
152
+ <div class="sticky top-0 z-40 flex justify-between px-4 gap-x-6 bg-white py-4 shadow-sm sm:px-6 lg:hidden">
153
+ <div class="flex items-center gap-x-6">
154
+ <button data-action="eyeloupe--nav#open" type="button" class="-m-2.5 p-2.5 text-red-500 lg:hidden">
155
+ <span class="sr-only">Open sidebar</span>
156
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
157
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
158
+ </svg>
159
+ </button>
160
+ <%= link_to root_url, class: "flex gap-x-1" do %>
161
+ <%= image_tag "eyeloupe/logo.png", class: "h-7 w-auto" %>
162
+ <h1 class="ml-2 text-xl font-semibold text-gray-700">Eyeloupe</h1>
163
+ <% end %>
164
+ </div>
165
+ <div class="py-4 flex justify-end items-end px-4 gap-x-2">
166
+ <%= link_to configs_path(value: "#{!@eyeloupe_capture}"), title: "Pause data collection", data: { "turbo_method": "put" }, class: "#{@eyeloupe_capture ? "text-gray-500 hover:bg-gray-300 bg-gray-200" : "bg-red-500 text-white hover:bg-red-600"} rounded-md p-1 transition duration-500 inline-block hidden" do %>
167
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
168
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
169
+ <path d="M6 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
170
+ <path d="M14 5m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v12a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path>
171
+ </svg>
172
+ <% end %>
173
+ <button title="Auto refresh data" data-action="eyeloupe--refresh#toggle" data-eyeloupe--refresh-target="btn" class="rounded-md text-gray-500 hover:bg-gray-300 bg-gray-200 p-1 transition duration-500 inline-block">
174
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
175
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
176
+ <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
177
+ <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
178
+ </svg>
179
+ </button>
180
+ <%= link_to data_path, title: "Delete all Eyeloupe data", data: { "turbo_method": "delete", "turbo_confirm": "Are you sure to delete all Eyeloupe data?" }, class: "rounded-md text-gray-500 hover:bg-gray-300 bg-gray-200 p-1 transition duration-500 inline-block" do %>
181
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
182
+ <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
183
+ <path d="M4 7l16 0"></path>
184
+ <path d="M10 11l0 6"></path>
185
+ <path d="M14 11l0 6"></path>
186
+ <path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"></path>
187
+ <path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"></path>
188
+ </svg>
189
+ <% end %>
190
+ </div>
191
+ </div>
192
+
193
+ <main class="py-10 lg:pl-72 px-5 mx-auto">
194
+
195
+ <div class="px-4 sm:px-6 lg:px-8 bg-white rounded-md shadow-md py-5">
196
+ <%= yield %>
197
+ </div>
198
+ </main>
199
+ </div>
200
+ </div>
201
+
202
+ </body>
203
+ </html>
@@ -0,0 +1,4 @@
1
+ pin_all_from File.expand_path("../app/assets/javascripts", __dir__)
2
+ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
3
+ pin "@hotwired/stimulus", to: "https://ga.jspm.io/npm:@hotwired/stimulus@3.0.1/dist/stimulus.js"
4
+ pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
data/config/routes.rb ADDED
@@ -0,0 +1,12 @@
1
+ Eyeloupe::Engine.routes.draw do
2
+
3
+ root to: "application#root"
4
+
5
+ resources :in_requests, only: [:index, :show]
6
+ resources :out_requests, only: [:index, :show]
7
+
8
+ resource :data, only: [:destroy]
9
+
10
+ resource :configs, only: [:update]
11
+
12
+ end
@@ -0,0 +1,22 @@
1
+ const defaultTheme = require('tailwindcss/defaultTheme')
2
+
3
+ module.exports = {
4
+ content: [
5
+ './public/*.html',
6
+ './app/helpers/**/*.rb',
7
+ './app/assets/javascripts/**/*.js',
8
+ './app/views/**/*.{erb,haml,html,slim}'
9
+ ],
10
+ theme: {
11
+ extend: {
12
+ fontFamily: {
13
+ sans: ['Fira Sans', 'sans-serif', ...defaultTheme.fontFamily.sans],
14
+ },
15
+ },
16
+ },
17
+ plugins: [
18
+ require('@tailwindcss/forms'),
19
+ require('@tailwindcss/aspect-ratio'),
20
+ require('@tailwindcss/typography'),
21
+ ]
22
+ }
@@ -0,0 +1,22 @@
1
+ class CreateEyeloupeInRequests < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :eyeloupe_in_requests do |t|
4
+ t.string :verb
5
+ t.string :hostname
6
+ t.string :controller
7
+ t.string :path
8
+ t.string :format
9
+ t.integer :status
10
+ t.integer :duration
11
+ t.integer :db_duration
12
+ t.integer :view_duration
13
+ t.string :ip
14
+ t.text :payload
15
+ t.text :headers
16
+ t.text :session
17
+ t.text :response
18
+
19
+ t.timestamps
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ class CreateEyeloupeOutRequests < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :eyeloupe_out_requests do |t|
4
+ t.string :verb
5
+ t.string :hostname
6
+ t.string :path
7
+ t.string :format
8
+ t.integer :status
9
+ t.integer :duration
10
+ t.text :payload
11
+ t.text :req_headers
12
+ t.text :res_headers
13
+ t.text :response
14
+
15
+ t.timestamps
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ require 'singleton'
3
+
4
+ module Eyeloupe
5
+ class Configuration
6
+ include Singleton
7
+
8
+ # @return [Array<String>]
9
+ attr_accessor :excluded_paths
10
+
11
+ # @return [Boolean]
12
+ attr_accessor :capture
13
+
14
+ def initialize
15
+ @excluded_paths = %w[]
16
+ @capture = true
17
+ end
18
+ end
19
+
20
+ end
@@ -0,0 +1,21 @@
1
+ require 'importmap-rails'
2
+
3
+ module Eyeloupe
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Eyeloupe
6
+
7
+ initializer "eyeloupe.assets" do |app|
8
+ app.config.assets.precompile += %w[ eyeloupe_manifest ]
9
+ end
10
+
11
+ initializer 'eyeloupe.add_middleware' do |app|
12
+ app.config.middleware.insert(0, Eyeloupe::RequestMiddleware)
13
+ end
14
+
15
+ initializer "eyeloupe.importmap", :before => "importmap" do |app|
16
+ app.config.importmap.paths << root.join("config/importmap.rb")
17
+ # https://github.com/rails/importmap-rails#sweeping-the-cache-in-development-and-test
18
+ app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+ require 'net/http'
3
+ module Net
4
+ class HTTP
5
+ alias original_request request
6
+
7
+ def request(req, body = nil, &block)
8
+ if Eyeloupe.configuration.capture
9
+ Eyeloupe::Processors::OutRequest.instance.init(req, body)
10
+ res = original_request(req, body, &block)
11
+ Eyeloupe::Processors::OutRequest.instance.process(res)
12
+ else
13
+ res = original_request(req, body, &block)
14
+ end
15
+ res
16
+ end
17
+
18
+ end
19
+ end