brut 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (403) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/Gemfile.lock +21 -21
  4. data/assets/YouTubeThumb.pxd +0 -0
  5. data/bin/new-version +3 -3
  6. data/brut-css/package-lock.json +94 -97
  7. data/brut-css/package.json +2 -2
  8. data/brut-js/package-lock.json +3 -3
  9. data/brut-js/package.json +6 -3
  10. data/brut-js/specs/AjaxSubmit.spec.js +62 -6
  11. data/brut-js/src/AjaxSubmit.js +26 -6
  12. data/brutrb.com/forms.md +1 -0
  13. data/brutrb.com/getting-started.md +3 -0
  14. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser-element-styled.png +0 -0
  15. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser-element.png +0 -0
  16. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser.png +0 -0
  17. data/brutrb.com/images/tutorial/02-confirmation-flow.graffle +0 -0
  18. data/brutrb.com/images/tutorial/02-confirmation-flow.png +0 -0
  19. data/brutrb.com/instrumentation.md +142 -3
  20. data/brutrb.com/recipes/authentication.md +1 -0
  21. data/brutrb.com/tutorial.md +29 -1627
  22. data/brutrb.com/tutorials/01-intro.md +1654 -0
  23. data/brutrb.com/tutorials/02-dialog.md +569 -0
  24. data/docs/404.html +2 -2
  25. data/docs/adrs.html +4 -4
  26. data/docs/ai.html +4 -4
  27. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  28. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  29. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  30. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  31. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  32. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  33. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  34. data/docs/api/Brut/BackEnd.html +1 -1
  35. data/docs/api/Brut/CLI/App.html +1 -1
  36. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  37. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
  38. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
  39. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
  40. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
  41. data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
  42. data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
  43. data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
  44. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
  45. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
  46. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
  47. data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
  48. data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
  49. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  50. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
  51. data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
  52. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
  53. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
  54. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  55. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
  56. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +1 -1
  57. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
  58. data/docs/api/Brut/CLI/Apps/Scaffold/DbModel.html +1 -1
  59. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
  60. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
  61. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  62. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +1 -1
  63. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
  64. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
  65. data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
  66. data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
  67. data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
  68. data/docs/api/Brut/CLI/Apps/Test/JS.html +1 -1
  69. data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
  70. data/docs/api/Brut/CLI/Apps/Test.html +1 -1
  71. data/docs/api/Brut/CLI/Apps.html +1 -1
  72. data/docs/api/Brut/CLI/Command.html +1 -1
  73. data/docs/api/Brut/CLI/Error.html +1 -1
  74. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  75. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  76. data/docs/api/Brut/CLI/Executor.html +1 -1
  77. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  78. data/docs/api/Brut/CLI/Options.html +1 -1
  79. data/docs/api/Brut/CLI/Output.html +1 -1
  80. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  81. data/docs/api/Brut/CLI.html +1 -1
  82. data/docs/api/Brut/FactoryBot.html +1 -1
  83. data/docs/api/Brut/Framework/App.html +1 -1
  84. data/docs/api/Brut/Framework/Config.html +1 -1
  85. data/docs/api/Brut/Framework/Container.html +1 -1
  86. data/docs/api/Brut/Framework/Error.html +1 -1
  87. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
  88. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  89. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  90. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  91. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  92. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  93. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  94. data/docs/api/Brut/Framework/Errors.html +1 -1
  95. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +2 -2
  96. data/docs/api/Brut/Framework/MCP.html +1 -1
  97. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  98. data/docs/api/Brut/Framework.html +1 -1
  99. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  100. data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
  101. data/docs/api/Brut/FrontEnd/Component.html +1 -1
  102. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +48 -27
  103. data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
  104. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
  105. data/docs/api/Brut/FrontEnd/Components/Input.html +2 -2
  106. data/docs/api/Brut/FrontEnd/Components/Inputs/ButtonTag.html +443 -0
  107. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  108. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +14 -9
  109. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
  110. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +8 -9
  111. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +10 -11
  112. data/docs/api/Brut/FrontEnd/Components/Inputs.html +2 -2
  113. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  114. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
  115. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  116. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  117. data/docs/api/Brut/FrontEnd/Components.html +1 -1
  118. data/docs/api/Brut/FrontEnd/CsrfProtector.html +1 -1
  119. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  120. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  121. data/docs/api/Brut/FrontEnd/Form.html +39 -39
  122. data/docs/api/Brut/FrontEnd/Forms/Button.html +331 -0
  123. data/docs/api/Brut/FrontEnd/Forms/ButtonInputDefinition.html +544 -0
  124. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
  125. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +1 -1
  126. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +1 -1
  127. data/docs/api/Brut/FrontEnd/Forms/Input.html +6 -2
  128. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +111 -23
  129. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +20 -12
  130. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +1 -1
  131. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  132. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +1 -1
  133. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  134. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
  135. data/docs/api/Brut/FrontEnd/Forms.html +2 -2
  136. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  137. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  138. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  139. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  140. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  141. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  142. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +2 -2
  143. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  144. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  145. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  146. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  147. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  148. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  149. data/docs/api/Brut/FrontEnd/Layout.html +1 -1
  150. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  151. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  152. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  153. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  154. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
  155. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  156. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  157. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
  158. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  159. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  160. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  161. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  162. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  163. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  164. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  165. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  166. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  167. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  168. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  169. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  170. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  171. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  172. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  173. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  174. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  175. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  176. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  177. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  178. data/docs/api/Brut/FrontEnd.html +1 -1
  179. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  180. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  181. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  182. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  183. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  184. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  185. data/docs/api/Brut/I18n.html +1 -1
  186. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  187. data/docs/api/Brut/Instrumentation/Methods/ClassMethods.html +596 -0
  188. data/docs/api/Brut/Instrumentation/Methods.html +173 -0
  189. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +7 -7
  190. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +7 -7
  191. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +19 -17
  192. data/docs/api/Brut/Instrumentation.html +3 -1
  193. data/docs/api/Brut/RubocopConfig.html +1 -1
  194. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  195. data/docs/api/Brut/SinatraHelpers.html +1 -1
  196. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  197. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  198. data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
  199. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  200. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  201. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  202. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  203. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  204. data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
  205. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
  206. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
  207. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
  208. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
  209. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
  210. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
  211. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
  212. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
  213. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
  214. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
  215. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
  216. data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
  217. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
  218. data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
  219. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  220. data/docs/api/Brut/SpecSupport.html +1 -1
  221. data/docs/api/Brut.html +1 -1
  222. data/docs/api/Clock.html +1 -1
  223. data/docs/api/ModuleName.html +1 -1
  224. data/docs/api/RichString.html +1 -1
  225. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  226. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
  227. data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
  228. data/docs/api/Sequel/Extensions.html +1 -1
  229. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  230. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  231. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  232. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  233. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  234. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  235. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  236. data/docs/api/Sequel/Plugins.html +1 -1
  237. data/docs/api/Sequel.html +1 -1
  238. data/docs/api/_index.html +36 -1
  239. data/docs/api/class_list.html +1 -1
  240. data/docs/api/file.README.html +1 -1
  241. data/docs/api/index.html +1 -1
  242. data/docs/api/method_list.html +364 -252
  243. data/docs/api/top-level-namespace.html +1 -1
  244. data/docs/assets/02-confirmation-dialog-browser-element-styled.3NEGM20-.png +0 -0
  245. data/docs/assets/02-confirmation-dialog-browser-element.DPsf0xUW.png +0 -0
  246. data/docs/assets/02-confirmation-dialog-browser.DH8ALFO4.png +0 -0
  247. data/docs/assets/02-confirmation-flow.D9gZ0S5U.png +0 -0
  248. data/docs/assets/{app.vjGWMSnJ.js → app.V2GOcOrg.js} +1 -1
  249. data/docs/assets/chunks/@localSearchIndexroot.3Lsq4QTb.js +1 -0
  250. data/docs/assets/chunks/{VPLocalSearchBox.C-ymMW2k.js → VPLocalSearchBox.CZTadcAy.js} +1 -1
  251. data/docs/assets/chunks/{theme.pJUosGlI.js → theme.FeuYNxyp.js} +2 -2
  252. data/docs/assets/{components.md.B543a3Lm.js → components.md.f4cdTyvV.js} +3 -3
  253. data/docs/assets/{configuration.md.-foE_iVv.js → configuration.md.Bs4-rxnS.js} +1 -1
  254. data/docs/assets/{form-constraints.md.DK5adCgM.js → form-constraints.md.KTv5cdR4.js} +6 -6
  255. data/docs/assets/{forms.md.D5-2rgHh.js → forms.md.Sys-XxVf.js} +3 -3
  256. data/docs/assets/{forms.md.D5-2rgHh.lean.js → forms.md.Sys-XxVf.lean.js} +1 -1
  257. data/docs/assets/{getting-started.md.Cd4XSZb_.js → getting-started.md.CFIW0bcE.js} +6 -3
  258. data/docs/assets/{getting-started.md.Cd4XSZb_.lean.js → getting-started.md.CFIW0bcE.lean.js} +1 -1
  259. data/docs/assets/instrumentation.md._lNSriEZ.js +90 -0
  260. data/docs/assets/instrumentation.md._lNSriEZ.lean.js +1 -0
  261. data/docs/assets/{recipes_authentication.md.Dzvi_g69.js → recipes_authentication.md.BAISoxmN.js} +1 -0
  262. data/docs/assets/recipes_form-errors.md.Bv5RCKqH.js +66 -0
  263. data/docs/assets/recipes_form-errors.md.Bv5RCKqH.lean.js +1 -0
  264. data/docs/assets/tutorial.md.BM40jnoq.js +27 -0
  265. data/docs/assets/tutorial.md.BM40jnoq.lean.js +1 -0
  266. data/docs/assets/{tutorial.md.C4zR5XPG.js → tutorials_01-intro.md.B4sUBY3X.js} +13 -33
  267. data/docs/assets/tutorials_01-intro.md.B4sUBY3X.lean.js +1 -0
  268. data/docs/assets/tutorials_02-dialog.md.QTFeHdiA.js +274 -0
  269. data/docs/assets/tutorials_02-dialog.md.QTFeHdiA.lean.js +1 -0
  270. data/docs/assets.html +4 -4
  271. data/docs/brut-js/api/AjaxSubmit.html +16 -6
  272. data/docs/brut-js/api/AjaxSubmit.js.html +27 -7
  273. data/docs/brut-js/api/Autosubmit.html +1 -1
  274. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  275. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  276. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  277. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  278. data/docs/brut-js/api/BufferedLogger.html +1 -1
  279. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  280. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  281. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  282. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  283. data/docs/brut-js/api/ConstraintViolationMessage.html +55 -5
  284. data/docs/brut-js/api/ConstraintViolationMessage.js.html +18 -3
  285. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  286. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  287. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  288. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  289. data/docs/brut-js/api/Form.html +7 -10
  290. data/docs/brut-js/api/Form.js.html +20 -24
  291. data/docs/brut-js/api/I18nTranslation.html +1 -1
  292. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  293. data/docs/brut-js/api/LocaleDetection.html +1 -1
  294. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  295. data/docs/brut-js/api/Logger.html +1 -1
  296. data/docs/brut-js/api/Logger.js.html +1 -1
  297. data/docs/brut-js/api/Message.html +1 -1
  298. data/docs/brut-js/api/Message.js.html +1 -1
  299. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  300. data/docs/brut-js/api/RichString.html +1 -1
  301. data/docs/brut-js/api/RichString.js.html +1 -1
  302. data/docs/brut-js/api/Tabs.html +1 -1
  303. data/docs/brut-js/api/Tabs.js.html +1 -1
  304. data/docs/brut-js/api/Tracing.html +1 -1
  305. data/docs/brut-js/api/Tracing.js.html +1 -1
  306. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  307. data/docs/brut-js/api/external-Performance.html +1 -1
  308. data/docs/brut-js/api/external-Promise.html +1 -1
  309. data/docs/brut-js/api/external-ValidityState.html +1 -1
  310. data/docs/brut-js/api/external-Window.html +1 -1
  311. data/docs/brut-js/api/external-fetch.html +1 -1
  312. data/docs/brut-js/api/global.html +1 -1
  313. data/docs/brut-js/api/index.html +1 -1
  314. data/docs/brut-js/api/index.js.html +1 -1
  315. data/docs/brut-js/api/module-testing.html +1 -1
  316. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  317. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  318. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  319. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  320. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  321. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  322. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  323. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  324. data/docs/brut-js/api/testing_index.js.html +1 -1
  325. data/docs/brut-js.html +4 -4
  326. data/docs/business-logic.html +4 -4
  327. data/docs/cli.html +4 -4
  328. data/docs/components.html +8 -8
  329. data/docs/configuration.html +5 -5
  330. data/docs/css.html +4 -4
  331. data/docs/custom-element-tests.html +4 -4
  332. data/docs/database-access.html +4 -4
  333. data/docs/database-schema.html +4 -4
  334. data/docs/deployment.html +4 -4
  335. data/docs/dev-environment.html +4 -4
  336. data/docs/dir-structure.html +4 -4
  337. data/docs/doc-conventions.html +4 -4
  338. data/docs/end-to-end-tests.html +4 -4
  339. data/docs/features.html +4 -4
  340. data/docs/flash-and-session.html +4 -4
  341. data/docs/form-constraints.html +11 -11
  342. data/docs/forms.html +7 -7
  343. data/docs/getting-started.html +10 -7
  344. data/docs/handlers.html +4 -4
  345. data/docs/hashmap.json +1 -1
  346. data/docs/hooks.html +4 -4
  347. data/docs/i18n.html +4 -4
  348. data/docs/index.html +3 -3
  349. data/docs/instrumentation.html +61 -6
  350. data/docs/javascript.html +4 -4
  351. data/docs/jobs.html +4 -4
  352. data/docs/keyword-injection.html +4 -4
  353. data/docs/layouts.html +4 -4
  354. data/docs/lsp.html +4 -4
  355. data/docs/markdown-examples.html +4 -4
  356. data/docs/middleware.html +4 -4
  357. data/docs/overview.html +4 -4
  358. data/docs/pages.html +4 -4
  359. data/docs/recipes/alternate-layouts.html +4 -4
  360. data/docs/recipes/authentication.html +7 -6
  361. data/docs/recipes/blank-layouts.html +4 -4
  362. data/docs/recipes/custom-flash.html +4 -4
  363. data/docs/recipes/form-errors.html +94 -0
  364. data/docs/recipes/indexed-forms.html +4 -4
  365. data/docs/recipes/migrations.html +5 -5
  366. data/docs/recipes/text-field-component.html +4 -4
  367. data/docs/roadmap.html +4 -4
  368. data/docs/routes.html +4 -4
  369. data/docs/security.html +4 -4
  370. data/docs/seed-data.html +4 -4
  371. data/docs/space-time-continuum.html +4 -4
  372. data/docs/tutorial.html +12 -713
  373. data/docs/tutorials/01-intro.html +736 -0
  374. data/docs/tutorials/02-dialog.html +302 -0
  375. data/docs/unit-tests.html +4 -4
  376. data/docs/why.html +4 -4
  377. data/lib/brut/front_end/components/input.rb +1 -0
  378. data/lib/brut/front_end/components/inputs/button_tag.rb +40 -0
  379. data/lib/brut/front_end/components/inputs/input_tag.rb +3 -1
  380. data/lib/brut/front_end/components/inputs/select_tag_with_options.rb +0 -1
  381. data/lib/brut/front_end/components/inputs/textarea_tag.rb +0 -1
  382. data/lib/brut/front_end/form.rb +7 -5
  383. data/lib/brut/front_end/forms/button.rb +21 -0
  384. data/lib/brut/front_end/forms/button_input_definition.rb +37 -0
  385. data/lib/brut/front_end/forms/input_declarations.rb +10 -0
  386. data/lib/brut/front_end/forms/input_definition.rb +7 -2
  387. data/lib/brut/instrumentation/methods.rb +153 -0
  388. data/lib/brut/instrumentation/open_telemetry.rb +1 -0
  389. data/lib/brut/instrumentation.rb +1 -0
  390. data/lib/brut/version.rb +1 -1
  391. data/mkbrut/Gemfile.lock +1 -1
  392. data/mkbrut/bin/publish +1 -1
  393. data/mkbrut/lib/mkbrut/version.rb +1 -1
  394. data/specs/brut/instrumentation/methods.spec.rb +399 -0
  395. metadata +51 -21
  396. data/docs/assets/chunks/@localSearchIndexroot.Dn1xGMv_.js +0 -1
  397. data/docs/assets/instrumentation.md.BgcaGVYH.js +0 -35
  398. data/docs/assets/instrumentation.md.BgcaGVYH.lean.js +0 -1
  399. data/docs/assets/tutorial.md.C4zR5XPG.lean.js +0 -1
  400. /data/docs/assets/{components.md.B543a3Lm.lean.js → components.md.f4cdTyvV.lean.js} +0 -0
  401. /data/docs/assets/{configuration.md.-foE_iVv.lean.js → configuration.md.Bs4-rxnS.lean.js} +0 -0
  402. /data/docs/assets/{form-constraints.md.DK5adCgM.lean.js → form-constraints.md.KTv5cdR4.lean.js} +0 -0
  403. /data/docs/assets/{recipes_authentication.md.Dzvi_g69.lean.js → recipes_authentication.md.BAISoxmN.lean.js} +0 -0
@@ -1,4 +1,4 @@
1
- import{_ as i,c as a,o as t,ag as e}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Forms","description":"","frontmatter":{},"headers":[],"relativePath":"forms.md","filePath":"forms.md"}'),n={name:"forms.md"};function h(l,s,p,r,k,d){return t(),a("div",null,s[0]||(s[0]=[e(`<h1 id="forms" tabindex="-1">Forms <a class="header-anchor" href="#forms" aria-label="Permalink to &quot;Forms&quot;">​</a></h1><p>In HTML, forms are the way data is submit to the server. Forms attract complexity since they interact with user experience, data validation, and interaction with a back-end database.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>Forms in Brut accomplish three things:</p><ul><li>Forms model the data elements of a <code>&lt;form&gt;</code>, including client-side constraints (which Brut can check server-side as well).</li><li>Forms assist in HTML generation, to ensure the HTML elements are consistent and correct.</li><li>Forms hold data submitted to the server. No need for strong parameters or digging into a Hash of Whatever.</li></ul><p>Since forms can lead to a lot of complexity, this module will stick to the very basics. There are several recipes we&#39;ll link to that explain more complex interactions with forms.</p><h3 id="declaring-form-data-elements" tabindex="-1">Declaring Form Data/Elements <a class="header-anchor" href="#declaring-form-data-elements" aria-label="Permalink to &quot;Declaring Form Data/Elements&quot;">​</a></h3><p>When you <a href="/routes.html">create a form route</a>, this imlplies a form class exists to specify the data:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/app.rb</span></span>
1
+ import{_ as i,c as a,o as t,ag as e}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Forms","description":"","frontmatter":{},"headers":[],"relativePath":"forms.md","filePath":"forms.md"}'),n={name:"forms.md"};function h(l,s,p,r,k,o){return t(),a("div",null,s[0]||(s[0]=[e(`<h1 id="forms" tabindex="-1">Forms <a class="header-anchor" href="#forms" aria-label="Permalink to &quot;Forms&quot;">​</a></h1><p>In HTML, forms are the way data is submit to the server. Forms attract complexity since they interact with user experience, data validation, and interaction with a back-end database.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>Forms in Brut accomplish three things:</p><ul><li>Forms model the data elements of a <code>&lt;form&gt;</code>, including client-side constraints (which Brut can check server-side as well).</li><li>Forms assist in HTML generation, to ensure the HTML elements are consistent and correct.</li><li>Forms hold data submitted to the server. No need for strong parameters or digging into a Hash of Whatever.</li></ul><p>Since forms can lead to a lot of complexity, this module will stick to the very basics. There are several recipes we&#39;ll link to that explain more complex interactions with forms.</p><h3 id="declaring-form-data-elements" tabindex="-1">Declaring Form Data/Elements <a class="header-anchor" href="#declaring-form-data-elements" aria-label="Permalink to &quot;Declaring Form Data/Elements&quot;">​</a></h3><p>When you <a href="/routes.html">create a form route</a>, this imlplies a form class exists to specify the data:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/app.rb</span></span>
2
2
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">routes </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
3
3
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;new_widget&quot;</span></span>
4
4
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span>
@@ -9,7 +9,7 @@ import{_ as i,c as a,o as t,ag as e}from"./chunks/framework.1L-BeKqY.js";const c
9
9
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">minlength:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 3</span></span>
10
10
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:quantity</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">type:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :number</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">min:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 0</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">step:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 1</span></span>
11
11
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:description</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">required:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span></span>
12
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p><code>input</code> declares a form element that will ultimately be handled by an <code>&lt;input&gt;</code> or <code>&lt;textarea&gt;</code> tag. <code>select</code> and <code>radio_button_group</code> are also avaiable, and are discussed in recipes.</p><p><code>input</code> accepts an input name used for <code>&lt;input&gt;</code>&#39;s <code>name</code> attribute. It also accepts keyword arguments that match the initializer of <a href="/api/Brut/FrontEnd/Forms/InputDefinition.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Forms::InputDefinition</code></a>. You&#39;ll notice those values mirror the various attributes related to client-side constraint validations, for example <code>minlength:</code> and <code>pattern:</code>.</p><p>Form elements have some defaults, as described below:</p><table tabindex="0"><thead><tr><th>Declaration</th><th>Default Behavior</th></tr></thead><tbody><tr><td><code>input :email</code></td><td><code>type: :email</code></td></tr><tr><td><code>input :password</code></td><td><code>type: :password</code></td></tr><tr><td><code>input :password_confirmation</code></td><td><code>type: :password</code></td></tr><tr><td><code>input «any other name»</code></td><td><code>type: :text</code></td></tr><tr><td><code>input «name», type: :checkbox</code></td><td><code>required: false</code></td></tr><tr><td><code>input «name» type: «not checkbox»</code></td><td><code>required: true</code></td></tr></tbody></table><h3 id="using-forms-to-generate-html" tabindex="-1">Using Forms to Generate HTML <a class="header-anchor" href="#using-forms-to-generate-html" aria-label="Permalink to &quot;Using Forms to Generate HTML&quot;">​</a></h3><p>One reason Brut models forms as classes with declared inputs is that you can then use an instance of that class to generate HTML. Brut will generate appropriate HTML, optionally configured to show a pre-existing value from the form.</p><p>The classes that do this are in <a href="/api/Brut/FrontEnd/Components.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components</code></a></p><table tabindex="0"><thead><tr><th>Class</th><th>Purpose</th></tr></thead><tbody><tr><td><a href="/api/Brut/FrontEnd/Components/FormTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::FormTag</code></a></td><td>Creates a <code>&lt;form&gt;</code> tag that submits to the form&#39;s configured route and includes <a href="/security.html">CSRF protection</a>.</td></tr><tr><td><a href="/api/Brut/FrontEnd/Components/InputTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::InputTag</code></a></td><td>Creates an <code>&lt;input&gt;</code> tag</td></tr><tr><td><a href="/api/Brut/FrontEnd/Components/RadioButton.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::RadioButton</code></a></td><td>Creates an <code>&lt;input type=&quot;radio&quot;&gt;</code> tag</td></tr><tr><td>for use in a radio button group.</td><td></td></tr><tr><td><a href="/api/Brut/FrontEnd/Components/SelectTagWithOptions.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::SelectTagWithOptions</code></a></td><td>Creates a <code>&lt;select&gt;</code> tag with</td></tr><tr><td><code>&lt;option&gt;</code> tags inside.</td><td></td></tr><tr><td><a href="/api/Brut/FrontEnd/Components/TextareaTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::TextareaTag</code></a></td><td>Creates a <code>&lt;textarea&gt;</code> tag.</td></tr></tbody></table><p>All of these classes have an initializer that accepts:</p><ul><li><code>form:</code> the form object, used to figure out the HTML attributes and current value of the element.</li><li><code>input_name:</code> to know which input is being generated.</li><li><code>index:</code> for <a href="/recipes/indexed-forms.html">indexed form elements</a>.</li><li><code>**html_attributes</code> any other HTML attributesyou&#39;d like to include.</li></ul><p>These class names are quite long, but since these are Phlex components, you can <code>include</code> <a href="/api/Brut/FrontEnd/Components.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components</code></a> and access their initializers as a <a href="https://phlex.fun" target="_blank" rel="noreferrer">Phlex kit</a>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
12
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p><code>input</code> declares a form element that will ultimately be handled by an <code>&lt;input&gt;</code> or <code>&lt;textarea&gt;</code> tag. <code>select</code> and <code>radio_button_group</code> are also avaiable, and are discussed in recipes.</p><p><code>input</code> accepts an input name used for <code>&lt;input&gt;</code>&#39;s <code>name</code> attribute. It also accepts keyword arguments that match the initializer of <a href="/api/Brut/FrontEnd/Forms/InputDefinition.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Forms::InputDefinition</code></a>. You&#39;ll notice those values mirror the various attributes related to client-side constraint validations, for example <code>minlength:</code> and <code>pattern:</code>.</p><p>Form elements have some defaults, as described below:</p><table tabindex="0"><thead><tr><th>Declaration</th><th>Default Behavior</th></tr></thead><tbody><tr><td><code>input :email</code></td><td><code>type: :email</code></td></tr><tr><td><code>input :password</code></td><td><code>type: :password</code></td></tr><tr><td><code>input :password_confirmation</code></td><td><code>type: :password</code></td></tr><tr><td><code>input «any other name»</code></td><td><code>type: :text</code></td></tr><tr><td><code>input «name», type: :checkbox</code></td><td><code>required: false</code></td></tr><tr><td><code>input «name» type: «not checkbox»</code></td><td><code>required: true</code></td></tr></tbody></table><h3 id="using-forms-to-generate-html" tabindex="-1">Using Forms to Generate HTML <a class="header-anchor" href="#using-forms-to-generate-html" aria-label="Permalink to &quot;Using Forms to Generate HTML&quot;">​</a></h3><p>One reason Brut models forms as classes with declared inputs is that you can then use an instance of that class to generate HTML. Brut will generate appropriate HTML, optionally configured to show a pre-existing value from the form.</p><p>The classes that do this are in <a href="/api/Brut/FrontEnd/Components.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components</code></a></p><table tabindex="0"><thead><tr><th>Class</th><th>Purpose</th></tr></thead><tbody><tr><td><a href="/api/Brut/FrontEnd/Components/FormTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::FormTag</code></a></td><td>Creates a <code>&lt;form&gt;</code> tag that submits to the form&#39;s configured route and includes <a href="/security.html">CSRF protection</a>.</td></tr><tr><td><a href="/api/Brut/FrontEnd/Components/InputTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::InputTag</code></a></td><td>Creates an <code>&lt;input&gt;</code> tag</td></tr><tr><td><a href="/api/Brut/FrontEnd/Components/RadioButton.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::RadioButton</code></a></td><td>Creates an <code>&lt;input type=&quot;radio&quot;&gt;</code> tag</td></tr><tr><td>for use in a radio button group.</td><td></td></tr><tr><td><a href="/api/Brut/FrontEnd/Components/SelectTagWithOptions.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::SelectTagWithOptions</code></a></td><td>Creates a <code>&lt;select&gt;</code> tag with</td></tr><tr><td><code>&lt;option&gt;</code> tags inside.</td><td></td></tr><tr><td><a href="/api/Brut/FrontEnd/Components/TextareaTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::TextareaTag</code></a></td><td>Creates a <code>&lt;textarea&gt;</code> tag.</td></tr><tr><td><a href="/api/Brut/FrontEnd/Components/ButtonTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::ButtonTag</code></a></td><td>Creates a <code>&lt;button&gt;</code> tag to submit the form.</td></tr></tbody></table><p>All of these classes have an initializer that accepts:</p><ul><li><code>form:</code> the form object, used to figure out the HTML attributes and current value of the element.</li><li><code>input_name:</code> to know which input is being generated.</li><li><code>index:</code> for <a href="/recipes/indexed-forms.html">indexed form elements</a>.</li><li><code>**html_attributes</code> any other HTML attributesyou&#39;d like to include.</li></ul><p>These class names are quite long, but since these are Phlex components, you can <code>include</code> <a href="/api/Brut/FrontEnd/Components.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components</code></a> and access their initializers as a <a href="https://phlex.fun" target="_blank" rel="noreferrer">Phlex kit</a>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
13
13
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span></span>
14
14
  <span class="line"></span>
15
15
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
@@ -31,7 +31,7 @@ import{_ as i,c as a,o as t,ag as e}from"./chunks/framework.1L-BeKqY.js";const c
31
31
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;number&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;quantity&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> required</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> min</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;0&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> step</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;1&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
32
32
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">textarea</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;description&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
33
33
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">textarea</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
34
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><p>Forms accept a single initializer parameter, <code>params</code> that is a <code>Hash</code>. <a href="/api/Brut/FrontEnd/Form.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Form</code></a> implements this initializer, and will pluck values from the hash to initialize the inputs:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-MTALg" id="tab-sDAAiPt" checked><label data-title="Form Class" for="tab-sDAAiPt">Form Class</label><input type="radio" name="group-MTALg" id="tab-amgCfW6"><label data-title="HTML Generated" for="tab-amgCfW6">HTML Generated</label></div><div class="blocks"><div class="language-ruby vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
34
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><p>Forms accept a single initializer parameter, <code>params</code> that is a <code>Hash</code>. <a href="/api/Brut/FrontEnd/Form.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Form</code></a> implements this initializer, and will pluck values from the hash to initialize the inputs:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-23T-R" id="tab-hV4Vvwk" checked><label data-title="Form Class" for="tab-hV4Vvwk">Form Class</label><input type="radio" name="group-23T-R" id="tab-Im8xJ8o"><label data-title="HTML Generated" for="tab-Im8xJ8o">HTML Generated</label></div><div class="blocks"><div class="language-ruby vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
35
35
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span></span>
36
36
  <span class="line"></span>
37
37
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
@@ -1 +1 @@
1
- import{_ as i,c as a,o as t,ag as e}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Forms","description":"","frontmatter":{},"headers":[],"relativePath":"forms.md","filePath":"forms.md"}'),n={name:"forms.md"};function h(l,s,p,r,k,d){return t(),a("div",null,s[0]||(s[0]=[e("",48)]))}const g=i(n,[["render",h]]);export{c as __pageData,g as default};
1
+ import{_ as i,c as a,o as t,ag as e}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Forms","description":"","frontmatter":{},"headers":[],"relativePath":"forms.md","filePath":"forms.md"}'),n={name:"forms.md"};function h(l,s,p,r,k,o){return t(),a("div",null,s[0]||(s[0]=[e("",48)]))}const g=i(n,[["render",h]]);export{c as __pageData,g as default};
@@ -1,17 +1,20 @@
1
- import{_ as s,c as e,o as t,ag as n}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"getting-started.md","filePath":"getting-started.md"}'),i={name:"getting-started.md"};function p(l,a,o,d,r,h){return t(),e("div",null,a[0]||(a[0]=[n(`<h1 id="getting-started" tabindex="-1">Getting Started <a class="header-anchor" href="#getting-started" aria-label="Permalink to &quot;Getting Started&quot;">​</a></h1><p>Brut is developed alongside a separate gem called <code>mkbrut</code>, which allows you to create a new Brut app. It will set up your dev environment as well.</p><h2 id="get-mkbrut" tabindex="-1">Get <code>mkbrut</code> <a class="header-anchor" href="#get-mkbrut" aria-label="Permalink to &quot;Get \`mkbrut\`&quot;">​</a></h2><p>The simplest way to use <code>mkbrut</code> is to use an existing <a href="https://hub.docker.com/repository/docker/thirdtank/mkbrut/general" target="_blank" rel="noreferrer">Docker image</a>. You don&#39;t have to install or configure Ruby:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
1
+ import{_ as s,c as e,o as t,ag as n}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"getting-started.md","filePath":"getting-started.md"}'),i={name:"getting-started.md"};function p(l,a,o,r,d,h){return t(),e("div",null,a[0]||(a[0]=[n(`<h1 id="getting-started" tabindex="-1">Getting Started <a class="header-anchor" href="#getting-started" aria-label="Permalink to &quot;Getting Started&quot;">​</a></h1><p>Brut is developed alongside a separate gem called <code>mkbrut</code>, which allows you to create a new Brut app. It will set up your dev environment as well.</p><h2 id="get-mkbrut" tabindex="-1">Get <code>mkbrut</code> <a class="header-anchor" href="#get-mkbrut" aria-label="Permalink to &quot;Get \`mkbrut\`&quot;">​</a></h2><p>The simplest way to use <code>mkbrut</code> is to use an existing <a href="https://hub.docker.com/repository/docker/thirdtank/mkbrut/general" target="_blank" rel="noreferrer">Docker image</a>. You don&#39;t have to install or configure Ruby:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
2
+ <span class="line"><span> --pull always \\</span></span>
2
3
  <span class="line"><span> -v &quot;$PWD&quot;:&quot;$PWD&quot; \\</span></span>
3
4
  <span class="line"><span> -w &quot;$PWD&quot; \\</span></span>
4
5
  <span class="line"><span> -u $(id -u):$(id -g) \\</span></span>
5
6
  <span class="line"><span> -it \\</span></span>
6
7
  <span class="line"><span> thirdtank/mkbrut \\</span></span>
7
8
  <span class="line"><span> mkbrut my-new-app</span></span></code></pre></div><p>If you already have Ruby 3.4 installed, you can install <code>mkbrut</code> directly:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>&gt; gem install mkbrut</span></span>
8
- <span class="line"><span>&gt; mkbrut my-new-app</span></span></code></pre></div><h2 id="init-your-app" tabindex="-1">Init Your App <a class="header-anchor" href="#init-your-app" aria-label="Permalink to &quot;Init Your App&quot;">​</a></h2><p>A Brut app just needs a name, which will be used to derive a few more useful values. For now:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-CFm39" id="tab-7ecxuU-" checked><label data-title="Docker-based" for="tab-7ecxuU-">Docker-based</label><input type="radio" name="group-CFm39" id="tab-YdXMyd3"><label data-title="RubyGems-based" for="tab-YdXMyd3">RubyGems-based</label></div><div class="blocks"><div class="language- vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
9
+ <span class="line"><span>&gt; mkbrut my-new-app</span></span></code></pre></div><h2 id="init-your-app" tabindex="-1">Init Your App <a class="header-anchor" href="#init-your-app" aria-label="Permalink to &quot;Init Your App&quot;">​</a></h2><p>A Brut app just needs a name, which will be used to derive a few more useful values. For now:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-gDk2m" id="tab-N0mAUXh" checked><label data-title="Docker-based" for="tab-N0mAUXh">Docker-based</label><input type="radio" name="group-gDk2m" id="tab-LQ4JqFB"><label data-title="RubyGems-based" for="tab-LQ4JqFB">RubyGems-based</label></div><div class="blocks"><div class="language- vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
10
+ <span class="line"><span> --pull always \\</span></span>
9
11
  <span class="line"><span> -v &quot;$PWD&quot;:&quot;$PWD&quot; \\</span></span>
10
12
  <span class="line"><span> -w &quot;$PWD&quot; \\</span></span>
11
13
  <span class="line"><span> -u $(id -u):$(id -g) \\</span></span>
12
14
  <span class="line"><span> -it \\</span></span>
13
15
  <span class="line"><span> thirdtank/mkbrut \\</span></span>
14
- <span class="line"><span> mkbrut my-new-app</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>mkbrut my-new-app</span></span></code></pre></div></div></div><p>This will create your new app, along with some demo routes, components, handlers, and tests. If this is your first time using Brut, we recommend you examine these demo components.</p><p>To create your app without the demo components:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-tSbsb" id="tab-WuhE_gL" checked><label data-title="Docker-based" for="tab-WuhE_gL">Docker-based</label><input type="radio" name="group-tSbsb" id="tab-YuZNjLY"><label data-title="RubyGems-based" for="tab-YuZNjLY">RubyGems-based</label></div><div class="blocks"><div class="language- vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
16
+ <span class="line"><span> mkbrut my-new-app</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>mkbrut my-new-app</span></span></code></pre></div></div></div><p>This will create your new app, along with some demo routes, components, handlers, and tests. If this is your first time using Brut, we recommend you examine these demo components.</p><p>To create your app without the demo components:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-VP0Bq" id="tab-l6BggLw" checked><label data-title="Docker-based" for="tab-l6BggLw">Docker-based</label><input type="radio" name="group-VP0Bq" id="tab-XDN4kBK"><label data-title="RubyGems-based" for="tab-XDN4kBK">RubyGems-based</label></div><div class="blocks"><div class="language- vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
17
+ <span class="line"><span> --pull always \\</span></span>
15
18
  <span class="line"><span> -v &quot;$PWD&quot;:&quot;$PWD&quot; \\</span></span>
16
19
  <span class="line"><span> -w &quot;$PWD&quot; \\</span></span>
17
20
  <span class="line"><span> -u $(id -u):$(id -g) \\</span></span>
@@ -1 +1 @@
1
- import{_ as s,c as e,o as t,ag as n}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"getting-started.md","filePath":"getting-started.md"}'),i={name:"getting-started.md"};function p(l,a,o,d,r,h){return t(),e("div",null,a[0]||(a[0]=[n("",29)]))}const k=s(i,[["render",p]]);export{u as __pageData,k as default};
1
+ import{_ as s,c as e,o as t,ag as n}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"getting-started.md","filePath":"getting-started.md"}'),i={name:"getting-started.md"};function p(l,a,o,r,d,h){return t(),e("div",null,a[0]||(a[0]=[n("",29)]))}const k=s(i,[["render",p]]);export{u as __pageData,k as default};
@@ -0,0 +1,90 @@
1
+ import{_ as i,c as e,o as a,ag as t}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Instrumentation and Observability","description":"","frontmatter":{},"headers":[],"relativePath":"instrumentation.md","filePath":"instrumentation.md"}'),n={name:"instrumentation.md"};function l(h,s,o,r,p,d){return a(),e("div",null,s[0]||(s[0]=[t(`<h1 id="instrumentation-and-observability" tabindex="-1">Instrumentation and Observability <a class="header-anchor" href="#instrumentation-and-observability" aria-label="Permalink to &quot;Instrumentation and Observability&quot;">​</a></h1><p>Brut has built-in support for OpenTelemetry, which is an open standard used by many observability vendors to allow you to understand the behavior of your app in production. Brut also includes a configuration for the <a href="https://github.com/CtrlSpice/otel-desktop-viewer/" target="_blank" rel="noreferrer">otel-desktop-viewer</a>, which allows you to see instrumentation in development.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><h3 id="why-instrument" tabindex="-1">Why Instrument? <a class="header-anchor" href="#why-instrument" aria-label="Permalink to &quot;Why Instrument?&quot;">​</a></h3><p>In production, you&#39;ll need to know what your app is doing and how well it&#39;s working. Historically, logs can provide this information in a roundabout way. Over the last many years, Application Performance Monitoring (APM) vendors like New Relic and Data Dog allowed developers to see much richer detail about how an app is working.</p><p>You could see, for example, the 95th percentil of your dashboard controller&#39;s performance, or the top 10 slowest SQL statements your app is executing. OpenTelemetry attempts to unify the API used to communicate this information from your app to your chosen vendor, and most vendors support it.</p><p>Instrumentation, then, is a way to record what your app is doing, how long its taking, and perhaps even why it&#39;s doing what it&#39;s doing, down to a very specific level. If properly configured, you could examine the performance of the app for a particular user on a particular day.</p><h3 id="setting-up-instrumentation" tabindex="-1">Setting up Instrumentation <a class="header-anchor" href="#setting-up-instrumentation" aria-label="Permalink to &quot;Setting up Instrumentation&quot;">​</a></h3><p>Brut automatically sets up OpenTelemetry (OTel) tracing. The primary interface you will use is <a href="/api/Brut/Instrumentation/OpenTelemetry.html" target="_self" rel="noopener" data-no-router><code>Brut::Instrumentation::OpenTelemetry</code></a>, which is available via <code>Brut.container.instrumentation</code>. We&#39;ll discuss that in a moment.</p><p>To configure the specifics of where the traces will go, the OTel gem uses environment variables:</p><table tabindex="0"><thead><tr><th>Variable</th><th>Value</th><th>Purpose</th></tr></thead><tbody><tr><td><code>OTEL_EXPORTER_OTLP_ENDPOINT</code></td><td>Depends on environment</td><td>Where to send the tracers. This is provided by your vendor, but is <code>http://otel-desktop-viewer:4318</code> in development</td></tr><tr><td><code>OTEL_EXPORTER_OTLP_HEADERS</code></td><td>Depends on vendor</td><td>Your vendor may ask you to set this. It often contains identifying information or API keys</td></tr><tr><td><code>OTEL_EXPORTER_OTLP_PROTOCOL</code></td><td>http/protobuf</td><td>Your vendor may request a different protocol, but protobuf is common and supported by otel-desktop-viewer</td></tr><tr><td><code>OTEL_LOG_LEVEL</code></td><td>debug</td><td>Useful when setting everything up to understand why things aren&#39;t working if they aren&#39;t working</td></tr><tr><td><code>OTEL_RUBY_BSP_START_THREAD_ON_BOOT</code></td><td>false</td><td>Deals with esoteric issues with Puma. See <a href="https://github.com/open-telemetry/opentelemetry-ruby/issues/462" target="_blank" rel="noreferrer">this GitHub issue</a> for the details.</td></tr><tr><td><code>OTEL_SERVICE_NAME</code></td><td>Your app&#39;s <code>id</code> from <code>App</code></td><td>Identifiers your app&#39;s name to the vendor</td></tr><tr><td><code>OTEL_TRACES_EXPORTER</code></td><td>otlp</td><td>Configures the class inside the OTel gem that will export the instrumentation to the vendor. If you omit this, Brut will log the instrumentation to the console</td></tr></tbody></table><p>When you created your Brut app, your <code>.env.development</code> and <code>.env.test</code> should have values for all these environment variables that will send instrumentation to the otel-desktop-viewer that was also configured.</p><p>If you run your app using <code>bin/dev</code> and use the app for a bit, then go to <code>http://localhost:8000</code>, you will see the otel-desktop-viewer UI and can browse the spans and traces sent by Brut.</p><h3 id="what-is-instrumented-by-default" tabindex="-1">What is Instrumented By Default <a class="header-anchor" href="#what-is-instrumented-by-default" aria-label="Permalink to &quot;What is Instrumented By Default&quot;">​</a></h3><p>Brut attempts to automatically instrument useful things so you don&#39;t have to do anything to start getting data. Brut will attempt to conform to standard semantics for HTTP requests and SQL statements.</p><p>Here is a non-exhaustive list of what Brut automatically instruments:</p><ul><li>How long each page or handler request takes, broken down by components.</li><li>CLI execution time</li><li>Time to rebuild the schema for tests</li><li>Time to run tests</li><li>Time to apply migrations</li><li>Time spent inside a route hook</li><li>The locale detected from the browser</li><li>The layout class used when rendering a page</li><li>If a requested path is owned by Brut or not</li><li>Ignored parameters on all form submissions</li><li>How long reloading takes in development</li><li>CSP reporting results</li><li>SQL Statements</li></ul><div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p><p><code>Sequel::Extensions::BrutInstrumentation</code> sets up telemetry for Sequel, and it does it in a relatively simplistic way. The result is that <em>all</em> SQL statements are part of the telemetry, including the actual values inserted or used in <code>WHERE</code> clauses. While you should not be putting sensitive data into your database, be warned that this is happening. There are plans to improve this to be more flexible and reduce the chance of sensitive data being sent in traces.</p></div><h3 id="adding-your-own-instrumentation" tabindex="-1">Adding Your Own Instrumentation <a class="header-anchor" href="#adding-your-own-instrumentation" aria-label="Permalink to &quot;Adding Your Own Instrumentation&quot;">​</a></h3><p>You can add instrumentation in two main ways, both of which can be used together.</p><h4 id="instrumenting-existing-methods" tabindex="-1">Instrumenting Existing Methods <a class="header-anchor" href="#instrumenting-existing-methods" aria-label="Permalink to &quot;Instrumenting Existing Methods&quot;">​</a></h4><p>Although Brut instruments the entrypoints to pages, handlers, and components, you will likely have your own set of back-end business logic that needs to be instrumented. If you aren&#39;t trying to diagnose a specific problem and just want to see your back-end class&#39; methods show up in your instrumentation vendor&#39;s dashboard, <a href="/api/Brut/Instrumentation/Methods.html" target="_self" rel="noopener" data-no-router><code>Brut::Instrumentation::Methods</code></a> will be the easiest way to do that.</p><p><a href="/api/Brut/Instrumentation/Methods.html" target="_self" rel="noopener" data-no-router><code>Brut::Instrumentation::Methods</code></a> can be included in any class, and provides three class methods, which are <em>mutually exclusive</em>:</p><ul><li><code>instrument_all</code> instruments all methods, public and private.</li><li><code>instrument_public</code> instruments only public methods.</li><li><code>instrument</code> instruments one or more named methods.</li></ul><p><code>initialize</code> is never instrumented.</p><p>Consider this class:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Widget</span></span>
2
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span></span>
3
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
4
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
5
+ <span class="line"></span>
6
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> search</span></span>
7
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
8
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
9
+ <span class="line"></span>
10
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> save</span></span>
11
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
12
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
13
+ <span class="line"></span>
14
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">private</span></span>
15
+ <span class="line"></span>
16
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> delete_orphans</span></span>
17
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
18
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
19
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>If we use <code>instrument_all</code>…</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Widget</span></span>
20
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Instrumentation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Methods</span></span>
21
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> instrument_all</span></span>
22
+ <span class="line"></span>
23
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
24
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>…<code>search</code>, <code>save</code>, and <code>delete_orphans</code> will be instrumented. If we use <code>instrument_public</code>…</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Widget</span></span>
25
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Instrumentation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Methods</span></span>
26
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> instrument_public</span></span>
27
+ <span class="line"></span>
28
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
29
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>…then only <code>search</code> and <code>save</code> are instrumented.</p><p>We can pick and choose by using <code>instrument</code>.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Widget</span></span>
30
+ <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Instrumentation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Methods</span></span>
31
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span></span>
32
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
33
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
34
+ <span class="line"></span>
35
+ <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> instrument </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> search</span></span>
36
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
37
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
38
+ <span class="line"></span>
39
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> save</span></span>
40
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
41
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
42
+ <span class="line"></span>
43
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">private</span></span>
44
+ <span class="line"></span>
45
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> delete_orphans</span></span>
46
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
47
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
48
+ <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> instrument </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:delete_orphans</span></span>
49
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Above, <code>search</code> and <code>delete_orphans</code> are instrumented. Since <code>def</code> in Ruby returns a symbol, <code>instrument def search</code> is the same as <code>instrument :search</code>.</p><h4 id="explicit-instrumentation-with-spans-attributes-and-events" tabindex="-1">Explicit Instrumentation with Spans, Attributes, and Events <a class="header-anchor" href="#explicit-instrumentation-with-spans-attributes-and-events" aria-label="Permalink to &quot;Explicit Instrumentation with Spans, Attributes, and Events&quot;">​</a></h4><p>To add explicit instrumentation, you&#39;ll create one or more of the following:</p><ul><li><em>Spans</em> record a block of code. They are shown as a sub-span if one is already in effect. When you create a span, that means it will be shown in the context of the HTTP request.</li><li><em>Attributes</em> can be added to the current span to provide more context about what is happening. For example, the HTTP request method is an attribute of the span used for the HTTP request. These attributes allow for sophisticated querying in the vendor&#39;s UI.</li><li><em>Events</em> record things that happen and metadata about that thing. These are like log statements. They are associated with the span you are in when you add the event.</li></ul><p>These can all be added via <code>Brut.container.instrumentation</code>, which is a <a href="/api/Brut/Instrumentation/OpenTelemetry.html" target="_self" rel="noopener" data-no-router><code>Brut::Instrumentation::OpenTelemetry</code></a> instance.</p><p>These methods are available:</p><ul><li><code>span(name,**attributes,&amp;block)</code> - Create a new span around the block yielded.</li><li><code>add_attributes(attributes)</code> - Add attributes to the current span. These will be prefixed with your app&#39;s prefix so it&#39;s clear in the observability UI that they are for your app and not standard.</li><li><code>add_event(name,**attributes)</code> - Add an event with optional attributes for the current span.</li><li><code>record_exception(ex,attributes=nil)</code> - Record an exception that was caught.</li><li><code>record_and_reraise_exception!(ex,attributes=nil)</code> - Record an exception and raise it.</li></ul><p>Suppose you want to instrument <code>RequireAuthBeforeHook</code> from the <a href="/hooks.html">hooks</a> documentation. Although the hook&#39;s <code>before</code> method is instrumented by Brut already, let&#39;s add some metadata to that span, and also add a span around the login check.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/route_hooks/require_auth_before_hook.rb</span></span>
50
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> RequireAuthBeforeHook</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RouteHook</span></span>
51
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> before</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">request_context:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">request:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">env:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
52
+ <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> is_home_page</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = request.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">path_info</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">match</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/^</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold;">\\/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">?$/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
53
+ <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> is_auth_route</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = request.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">path_info</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">match?</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/^</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold;">\\/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">auth</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold;">\\/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
54
+ <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> is_brut_owned_path</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = env[</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;brut.owned_path&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">]</span></span>
55
+ <span class="line"></span>
56
+ <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> requires_login</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">is_home_page </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">&amp;&amp;</span></span>
57
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">is_auth_route </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">&amp;&amp;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> </span></span>
58
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">is_brut_owned_path</span></span>
59
+ <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">container</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">instrumentation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">add_attributes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
60
+ <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> requires_login:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
61
+ <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> is_home_page:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
62
+ <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> is_auth_route:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
63
+ <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> is_brut_owned_path:</span></span>
64
+ <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
65
+ <span class="line"></span>
66
+ <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">container</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">instrumentation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">span</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;login-check&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> |span|</span></span>
67
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">logged_in?</span></span>
68
+ <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> span.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">add_attributes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">logged_in:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
69
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> request_context[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:authenticated_account</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">authenticated_account</span></span>
70
+ <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> requires_login</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">false</span></span>
71
+ <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
72
+ <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> span.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">add_attributes</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">logged_in:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
73
+ <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
74
+ <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
75
+ <span class="line"></span>
76
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> requires_login</span></span>
77
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> redirect_to</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Auth</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">LoginPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
78
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
79
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> continue</span></span>
80
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
81
+ <span class="line"></span>
82
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
83
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Now, for every request someone makes to our app, we will see a span for the <code>RequireAuthBeforeHook</code>, and inside that span, we&#39;ll see the attributes we added as well as a sub-span representing the login check (which itself will have an attribute about the user&#39;s logged-in status).</p><h3 id="client-side-observability" tabindex="-1">Client-Side Observability <a class="header-anchor" href="#client-side-observability" aria-label="Permalink to &quot;Client-Side Observability&quot;">​</a></h3><p>The class <a href="/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Handlers::InstrumentationHandler</code></a> is set up to receive information from the client-side to provide insights about client-side behavior as part of a server-side request. Brut attempts to join up any client-side instrumentation to the request that served it.</p><p>It does this via the <a href="/api/Brut/FrontEnd/Components/Traceparent.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Traceparent</code></a> component, which is included in your default layout when you created your Brut app. This creates a <code>&lt;meta&gt;</code> tag containing standardized information used to connect the client-side behavior to the server-side request.</p><p>The Brut custom element <a href="/brut-js/api/Tracing.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-tracing&gt;</code></a> uses this information, along with statistics from the browser, to send a custom payload back to Brut at the route <code>/__brut/instrumentation</code>, which is handled by the aforementioned <code>InstrumentationHandler</code>.</p><p>You should then see client-side tracing information as a sub-span of your HTTP request. The information available depends on the browser, and some browsers don&#39;t send much. Also keep in mind that clock drift is real and while client-side timings are accurate, the timestamps will not be.</p><h3 id="logging" tabindex="-1">Logging <a class="header-anchor" href="#logging" aria-label="Permalink to &quot;Logging&quot;">​</a></h3><p>Brut configures <a href="https://logger.rocketjob.io/" target="_blank" rel="noreferrer">SemanticLogger</a>, but uses it sparingly. Currently, Brut performs very little logging, and no request logging. You may have noticed that your app doesn&#39;t produce a lot of output in development. Brut&#39;s assumption is that you will use an OpenTelemetry vendor to understand your app in production or the otel-desktop-viewer in developoment.</p><p>That said, since SemanticLogger is configured, you can use it at will:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> HomePage</span></span>
84
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
85
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> SemanticLogger</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">self</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">class</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">].</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">debug</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;page being rendered&quot;</span></span>
86
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> </span></span>
87
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
88
+ <span class="line"></span>
89
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
90
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>The logging system is currently not very configurable, and works as follows:</p><ul><li>In development, log messages are written to the standard output and to <code>logs/development.log</code></li><li>In test, log messages are written to <code>logs/test.log</code></li><li>In production, log messages are written to the standard output</li></ul><p>The default log level is &quot;debug&quot; for the web app at &quot;fatal&quot; for CLI apps. You can set <code>LOG_LEVEL</code> in the environment to change this:</p><ul><li><code>&quot;debug&quot;</code> - Show all messages</li><li><code>&quot;info&quot;</code> - Show info and above (not debug messages)</li><li><code>&quot;warn&quot;</code> - Show warnings and above (not info, not debug)</li><li><code>&quot;error&quot;</code> - Show errors and fatals only</li><li><code>&quot;fatal&quot;</code> - Show fatals only</li></ul><p>Most CLIs also allow <code>--log-level</code> to accept one of these strings as wel ass <code>--verbose</code> to set the log level to debug.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>Generally you don&#39;t want to test instrumentation unless it&#39;s highly complex and critical to the app&#39;s ability to be maintained. Ideally, your end-to-end tests will cover all the instrumentation code you write so you can be sure that none of that causes a problem.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>Entire books and conferences exist on how to properly instrument your app. Our suggestion is to take what you have by default and add additional instrumentation only to solve specific problems or identify specific issues.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated Aug 27, 2025</em></p><p>OpenTelemetry is notoriously opaque and, ironically, unobservable in its own behavior. Thus, the implementation is subject to change as I figure out what actually does what.</p><h3 id="web-requests" tabindex="-1">Web Requests <a class="header-anchor" href="#web-requests" aria-label="Permalink to &quot;Web Requests&quot;">​</a></h3><p><a href="/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Middlewares::OpenTelemetrySpan</code></a> is configured in <a href="/api/Brut/Framework/MCP.html" target="_self" rel="noopener" data-no-router><code>Brut::Framework::MCP</code></a> as the first middleware. It sets up the outer span for all web requests. Inside each request block (the code internal to Brut that calls handlers or pages), this span&#39;s name is modified with the HTTP method and path via the <code>brut.otel.root_span</code> in the Rack environment.</p><h3 id="client-side" tabindex="-1">Client-Side <a class="header-anchor" href="#client-side" aria-label="Permalink to &quot;Client-Side&quot;">​</a></h3><p>The client-side portion of this is highly customized. The Otel open source code for the client side is massive and hugely complex, so Brut decided to try to produce something simple and straightforward as a start. This can and will evolve over time.</p><h3 id="cli-commands" tabindex="-1">CLI Commands <a class="header-anchor" href="#cli-commands" aria-label="Permalink to &quot;CLI Commands&quot;">​</a></h3><p>Brut CLI commands are instrumented as well, in <a href="/api/Brut/CLI/App.html" target="_self" rel="noopener" data-no-router><code>Brut::CLI::App</code></a> in <code>execute!</code>, however the trace only begins if the underlying command is going to be executed. This may change.</p><h3 id="sidekiq-jobs" tabindex="-1">Sidekiq Jobs <a class="header-anchor" href="#sidekiq-jobs" aria-label="Permalink to &quot;Sidekiq Jobs&quot;">​</a></h3><p>Although Brut currently does not provide a default Sidekiq configuration, if you set up Sidekiq and include the <code>opentelemetry-instrumentation-sidekiq</code> gem in your app&#39;s <code>Gemfile</code>, you should see instrumentation of your Sidekiq jobs. In practice, this default set up doesn&#39;t seem to work very well, so expect this to change for the better.</p>`,74)]))}const u=i(n,[["render",l]]);export{c as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as i,c as e,o as a,ag as t}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Instrumentation and Observability","description":"","frontmatter":{},"headers":[],"relativePath":"instrumentation.md","filePath":"instrumentation.md"}'),n={name:"instrumentation.md"};function l(h,s,o,r,p,d){return a(),e("div",null,s[0]||(s[0]=[t("",74)]))}const u=i(n,[["render",l]]);export{c as __pageData,u as default};
@@ -6,6 +6,7 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const o
6
6
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
7
7
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
8
8
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>We&#39;ll also create <code>app/src/back_end/data_models/db/account.rb</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Account</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppDataModel</span></span>
9
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> has_external_id </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:a3</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # !IMPORTANT: Make sure this is unique amongst your DB models</span></span>
9
10
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Next, we&#39;ll create a factory for it in <code>specs/factories/db/account.factory.rb</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;bcrypt&quot;</span></span>
10
11
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FactoryBot</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">define</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
11
12
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> factory </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:account</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">class:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;DB::Account&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
@@ -0,0 +1,66 @@
1
+ import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.1L-BeKqY.js";const E=JSON.parse('{"title":"Styling Form Errors","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/form-errors.md","filePath":"recipes/form-errors.md"}'),t={name:"recipes/form-errors.md"};function h(l,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[e(`<h1 id="styling-form-errors" tabindex="-1">Styling Form Errors <a class="header-anchor" href="#styling-form-errors" aria-label="Permalink to &quot;Styling Form Errors&quot;">​</a></h1><p>Brut makes it as easy as possible to unify client-side and server-side constraint violation handling, including how you style those messages.</p><h2 id="requirements" tabindex="-1">Requirements <a class="header-anchor" href="#requirements" aria-label="Permalink to &quot;Requirements&quot;">​</a></h2><p>What you want:</p><ul><li>When a form is rendered for the first time, there should be errors shown.</li><li>When a visitor interacts with a form before submissions, no errors are shown.</li><li>When the form is submitted, client-side constraint violations should be shown.</li><li>When JavaScript is circumvented and the form is submitted with client-side constraint violations, the form should be re-generated, showing those violations the same is if JavaScripts was <em>not</em> circumvented</li><li>When there are no client-side constraint violations, but there <em>are</em> server-side violations, the form should be re-generated, showing those violations the same is if JavaScripts was <em>not</em> circumvented</li></ul><p>This can be achieved through CSS.</p><h2 id="recipe" tabindex="-1">Recipe <a class="header-anchor" href="#recipe" aria-label="Permalink to &quot;Recipe&quot;">​</a></h2><h3 id="create-pages-and-html" tabindex="-1">Create Pages and HTML <a class="header-anchor" href="#create-pages-and-html" aria-label="Permalink to &quot;Create Pages and HTML&quot;">​</a></h3><p>First, create a form and handler:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/scaffold form /new_widget</span></span></code></pre></div><p>Edit <code>app/src/front_end/forms/new_widget_form.rb</code></p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetForm</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppForm</span></span>
2
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">minlength:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 3</span></span>
3
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:description</span></span>
4
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Now, implement the handler in <code>app/src/front_end/handlers/new_widget_handler.rb</code> to check for client-side violations <em>and</em> require that the description have at least 5 words in it.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppHandler</span></span>
5
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
6
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form</span></span>
7
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
8
+ <span class="line"></span>
9
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> handle</span></span>
10
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">valid?</span></span>
11
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">description</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">split</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold;">\\s</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">+/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">length</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 5</span></span>
12
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">server_side_constraint_violation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
13
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :description</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
14
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> key:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :not_enough_words</span></span>
15
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
16
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
17
+ <span class="line"></span>
18
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">constraint_violations?</span></span>
19
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form)</span></span>
20
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
21
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> redirect_to</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">HomePage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
22
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
23
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
24
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Add the new error message to <code>app/config/i18n/en/2_app.rb</code></p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">{</span></span>
25
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> en:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
26
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nevermind:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Nevermind&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
27
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> cv:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
28
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ss:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
29
+ <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> not_enough_words:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;%{field} must have at least %{minwords} words&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
30
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> },</span></span>
31
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span></code></pre></div><p>Now, build a minimal <code>NewWidgetPage</code>:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/scaffold page /new_widget\`</span></span></code></pre></div><p>We&#39;ll create the bare minimum in <code>app/src/front_end/pages/new_widget_page.rb</code></p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
32
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
33
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">||</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetForm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span></span>
34
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
35
+ <span class="line"></span>
36
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
37
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> brut_form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
38
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> FormTag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">for:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
39
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> label </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
40
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">InputTag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
41
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ConstraintViolations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
42
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> span { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Name&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
43
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
44
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> label </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
45
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextareaTag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
46
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ConstraintViolations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
47
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> span { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Description&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
48
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
49
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
50
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
51
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
52
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><h3 id="create-css" tabindex="-1">Create CSS <a class="header-anchor" href="#create-css" aria-label="Permalink to &quot;Create CSS&quot;">​</a></h3><p>The most minimal CSS would be as followed, which you can place in <code>app/src/front_end/css/index.css</code></p><div class="language-css vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">css</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
53
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> display</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">none</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
54
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span>
55
+ <span class="line"></span>
56
+ <span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">submitted-invalid</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
57
+ <span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">server-generated</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] {</span></span>
58
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> display</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">block</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
59
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">red</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
60
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span></code></pre></div><p>This will show the messages in <a href="/brut-js/api/ConstraintViolationMessage.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-cv&gt;</code></a> <em>only</em> if:</p><ul><li>Form submission was attempted (<code>submitted-invalid</code> would be set on <a href="/brut-js/api/Form.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-form&gt;</code></a>)</li><li>The server generated via <code>ConstraintViolations</code> (only if the handler was triggered)</li></ul><p>Because more than one <a href="/brut-js/api/ConstraintViolationMessage.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-cv&gt;</code></a> could be generated or inserted, you may want to style the <a href="/brut-js/api/ConstraintViolationMessages.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-cv-messages&gt;</code></a> that contains them, but you only want it to show up if it has contents.</p><p>You can&#39;t just do <code>brut-cv-messages:has(brut-cv)</code>, because your container would show up before the form submission was attempted.</p><p>Here is what you want instead. This will put the error messages in a red box:</p><div class="language-css vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">css</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">submitted-invalid</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv-messages</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">:has</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">),</span></span>
61
+ <span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv-messages</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">:has</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">server-generated</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">]) {</span></span>
62
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">red</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
63
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> background-color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">pink</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
64
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> border</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">solid</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> thin</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> red</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
65
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> border-radius</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
66
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span></code></pre></div>`,29)]))}const g=i(t,[["render",h]]);export{E as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.1L-BeKqY.js";const E=JSON.parse('{"title":"Styling Form Errors","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/form-errors.md","filePath":"recipes/form-errors.md"}'),t={name:"recipes/form-errors.md"};function h(l,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[e("",29)]))}const g=i(t,[["render",h]]);export{E as __pageData,g as default};
@@ -0,0 +1,27 @@
1
+ import{_ as a,c as t,o as i,ag as e}from"./chunks/framework.1L-BeKqY.js";const k=JSON.parse('{"title":"Tutorials","description":"","frontmatter":{},"headers":[],"relativePath":"tutorial.md","filePath":"tutorial.md"}'),n={name:"tutorial.md"};function l(o,s,h,p,d,r){return i(),t("div",null,s[0]||(s[0]=[e(`<h1 id="tutorials" tabindex="-1">Tutorials <a class="header-anchor" href="#tutorials" aria-label="Permalink to &quot;Tutorials&quot;">​</a></h1><p>Below are several tutorials, along with screencasts showing the tutorial steps as a video. The first one is to <a href="https://video.hardlimit.com/w/ae7EMhwjDq9kSH5dqQ9swV" target="_blank" rel="noreferrer">build a blog in 15 minutes</a>. The remainder of the tutorials assumey you are going in order, however code for each starting point is available, so you can skip around.</p><p>If you&#39;d just like to read source code, there are two apps you can check out:</p><ul><li><a href="https://github.com/thirdtank/blog-demo" target="_blank" rel="noreferrer">The blog we&#39;ll build here</a></li><li><a href="https://github.com/thirdtank/adrs.cloud" target="_blank" rel="noreferrer">ADRs.cloud</a>, which is a more realistic app that has mulitple database tables, progressively-enhanced UI, and background jobs.</li></ul><p>You can be running either of these locally in minutes as long as you have Docker installed.</p><h2 id="understanding-these-tutorials" tabindex="-1">Understanding These Tutorials <a class="header-anchor" href="#understanding-these-tutorials" aria-label="Permalink to &quot;Understanding These Tutorials&quot;">​</a></h2><p>These tutorials will show you command line invocations and code. You should be able to follow along and just type what we say and it should work.</p><p>That said, it&#39;s not always clear what we are talking about.</p><h3 id="understanding-command-line-invocations" tabindex="-1">Understanding Command Line Invocations <a class="header-anchor" href="#understanding-command-line-invocations" aria-label="Permalink to &quot;Understanding Command Line Invocations&quot;">​</a></h3><p>If you aren&#39;t comfortable on the command line, it can be hard to understand what parts of this tutorial represent stuff you should type/paste and what is output from those commands. Here is how that works.</p><p>When we want you to run a command, the preceding text will tell you something like &quot;run this command&quot;, and then you&#39;ll see a codeblock that has the label &quot;bash&quot; in the upper right corner, like so:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">ls</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> -l</span></span></code></pre></div><p>If you hover over it, an icon with the tooltip &quot;Copy Code&quot; will appear on the right, and you can click that to copy the command-line invocation. Or, you can select it and copy it, or you can type it in manually.</p><p>In any case, you are expected to type/paste/execute the entire thing. Other parts of this documentation site may precede command lines with <code>&gt;</code> to indicate it&#39;s a shell command. For this tutorial, we aren&#39;t doing that.</p><p>Sometimes, commands are long. They can be split up by entering a backslash (<code>\\</code>) as the last character of a line, hitting return, and continuing the command. For example this command:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">git</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> push</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> origin</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> main</span></span></code></pre></div><p>Could be executed like so:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">git</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> push</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> \\</span></span>
2
+ <span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> origin</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> \\</span></span>
3
+ <span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> main</span></span></code></pre></div><p>In both cases, you can copy/type these as written and they will work.</p><p>To show output of a command, a separate code block will be used, and the first line of the output will be the string <code># OUTPUT:</code>, and there should <strong>not</strong> be a &quot;bash&quot; label in the upper right corner:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
4
+ <span class="line"><span>app dx puma.config.rb</span></span>
5
+ <span class="line"><span>bin Gemfile README.md</span></span>
6
+ <span class="line"><span>config.ru package.json specs</span></span>
7
+ <span class="line"><span>docker-compose.dx.yml Procfile.development</span></span>
8
+ <span class="line"><span>Dockerfile.dx Procfile.test</span></span></code></pre></div><p>Sometimes, output is very long and very irrelevant. In that case, the string <code>«LOTS OF OUTPUT»</code> will be used as a placeholder:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
9
+ <span class="line"><span>app</span></span>
10
+ <span class="line"><span>dx</span></span>
11
+ <span class="line"><span>puma.config.rb</span></span>
12
+ <span class="line"><span>«LOTS OF OUTPUTS»</span></span></code></pre></div><h3 id="understanding-code-changes" tabindex="-1">Understanding Code Changes <a class="header-anchor" href="#understanding-code-changes" aria-label="Permalink to &quot;Understanding Code Changes&quot;">​</a></h3><p>In most cases, we&#39;ll show you the entire code for a file/class, and you should make your copy look like it. Suppose you have this:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> SomeComponent</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppComponent</span></span>
13
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>We might say &quot;add the <code>view_template</code> to your component so it looks like this:&quot;</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> SomeComponent</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppComponent</span></span>
14
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> view_template</span></span>
15
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> h3 { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;My component&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
16
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">href:HelpPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Would You Like to Know More?&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
17
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
18
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>That means you can replace the file with this code. Other times, we may only focus on one method. We might write &quot;Change <code>view_template</code> in <code>SomeComponent</code> so it looks like so:&quot;</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> view_template</span></span>
19
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> h3 { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;My component&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
20
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">href:HelpPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Would You Like to Know More?&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
21
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>In this case, you&#39;d replace the method, but the leave the rest of the class as-is.</p><p>On occasion we&#39;ll want to only change a few lines and, in that case, we&#39;ll use a diff format like so:</p><div class="language-diff vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">diff</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#B31D28;--shiki-dark:#FDAEB7;">- a(href: &quot;&quot;) { &quot;Write New Blog Post&quot; }</span></span>
22
+ <span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">+ a(href: BlogPostEditorPage.routing) { &quot;Write New Blog Post&quot; }</span></span></code></pre></div><p>If a change is more complex, sometimes we&#39;ll show line numbers and highlight the changes. Below, we&#39;re showing a change to lines 14,15, and 16 of the file.</p><div class="language-ruby vp-adaptive-theme line-numbers-mode"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">href:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;BlogPostEditorPage.routing&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
23
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> plain { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Edit&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
24
+ <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> span </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
25
+ <span class="line highlighted"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> inline_svg</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;edit_icon&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
26
+ <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
27
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div><p>This says to find the line that looks like the first one (preceded with a <code>-</code> and shown in red) and replace it with the second one (preceded with a <code>+</code> and shown in green). <strong>Do not use the <code>+</code> or <code>-</code> in your code</strong>, that is just to indicate which line is which.</p><p>Lastly, we&#39;ll try to mention the path to the file either in the preceding text or as a comment in the code.</p><h2 id="tutorials-1" tabindex="-1">Tutorials <a class="header-anchor" href="#tutorials-1" aria-label="Permalink to &quot;Tutorials&quot;">​</a></h2><p>These go mostly in order, each building on the last, but you can start anywhere by using the tutorial on GitHub. The only one that starts from nothing is the first one.</p><table tabindex="0"><thead><tr><th>Index</th><th>Title</th><th>Tutorial</th><th>Screencast</th></tr></thead><tbody><tr><td>1</td><td>Build a Blog in 15 Minutes</td><td><a href="./tutorials/01-intro.html">Tutorial</a></td><td><a href="https://video.hardlimit.com/w/ae7EMhwjDq9kSH5dqQ9swV" target="_blank" rel="noreferrer">Screencast</a></td></tr><tr><td>2</td><td>Adding a Styled Confirmation Dialog</td><td><a href="./tutorials/02-dialog.html">Tutorial</a></td><td><a href="https://video.hardlimit.com/w/4y8Pjd8VVPDK372mozCUdj" target="_blank" rel="noreferrer">Screencast</a></td></tr><tr><td>3</td><td>Leveraging Externalizable IDs (coming soon)</td><td></td><td></td></tr><tr><td>4</td><td>Form Basics (coming soon)</td><td></td><td></td></tr><tr><td>5</td><td>Advanced Forms (coming soon)</td><td></td><td></td></tr><tr><td>6</td><td>AJax Form Submissions (coming soon)</td><td></td><td></td></tr><tr><td>7</td><td>Authentication (coming soon)</td><td></td><td></td></tr><tr><td>8</td><td>Background Jobs with Sidekiq (coming soon)</td><td></td><td></td></tr><tr><td>9</td><td>How to Leverage BrutJS and Custom Elements (coming soon)</td><td></td><td></td></tr></tbody></table>`,40)]))}const u=a(n,[["render",l]]);export{k as __pageData,u as default};
@@ -0,0 +1 @@
1
+ import{_ as a,c as t,o as i,ag as e}from"./chunks/framework.1L-BeKqY.js";const k=JSON.parse('{"title":"Tutorials","description":"","frontmatter":{},"headers":[],"relativePath":"tutorial.md","filePath":"tutorial.md"}'),n={name:"tutorial.md"};function l(o,s,h,p,d,r){return i(),t("div",null,s[0]||(s[0]=[e("",40)]))}const u=a(n,[["render",l]]);export{k as __pageData,u as default};