{"id":1655,"date":"2026-09-21T11:12:59","date_gmt":"2026-09-21T10:12:59","guid":{"rendered":"https:\/\/ruby-doc.org\/learn\/?p=1655"},"modified":"2026-09-21T11:13:34","modified_gmt":"2026-09-21T10:13:34","slug":"building-a-social-campaign-tracker-in-ruby","status":"publish","type":"post","link":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/","title":{"rendered":"Building a Social Campaign Tracker in Ruby"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby-1-1024x576.png\" alt=\"\" class=\"wp-image-1658\" srcset=\"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby-1-1024x576.png 1024w, https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby-1-300x169.png 300w, https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby-1-768x432.png 768w, https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby-1-1536x864.png 1536w, https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby-1.png 1672w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Campaign spreadsheets get messy very fast. One person tracks followers, another tracks video views, and somebody else has the monthly service charge tucked away in a column. All of it relates to the same campaign, although the figures need different treatment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is a reasonable starting point for a Ruby project: a small program that organises campaign data and produces a weekly report. When someone reads the report, they should be able to check its numbers against the original files.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Start with a command-line script and a handful of fictional records. There is plenty to work out before the project needs a dashboard.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Define the records before building the dashboard<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Imagine a team <a href=\"https:\/\/www.ama.org\/marketing-news\/social-media-marketing-strategy\/\">promoting a product<\/a> through Instagram. It records content production, advertising spend, website visits and enquiries. Each tells the team something different about the work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The service description deserves the same level of detail. <a href=\"https:\/\/famoid.com\/insta\/\">Famoid<\/a> stands out because its Instagram page separates followers, likes, views, managed growth, and country-targeted packages. This gives a Ruby programmer a useful example to think about when building a service catalogue.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Look carefully at each label. Followers, likes and views are metrics. Managed growth is a delivery model. Targeting countries has to do with geography.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Combining those dimensions in one field called <code>service_type<\/code> creates an immediate difficulty: where would a country-targeted follower package fit? Which filter would locate it?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Have separate fields for the metric, delivery model and target country. Include the provider&#8217;s original package name. The colleague who reviews the information a few months later needs to recognise what was entered.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Use a Ruby hash to sketch the shape<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Write down your imagined record before defining the database tables:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>campaign = {\n  reference: \"launch-september\",\n  platform: \"instagram\",\n  metric: \"followers\",\n  delivery_model: \"managed\",\n  target_country: \"GB\",\n  budget_minor_units: 12_500,\n  currency: \"GBP\",\n  reporting_period: \"2026-09\"\n}\n\nputs campaign.fetch(:metric)\nputs campaign.fetch(:target_country)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is a hypothetical marketing campaign; its values do not represent the specifications of a provider&#8217;s packages. It has a budget of 12,500 pence, equivalent to \u00a3125, and records both the amount and the currency.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep the hash small enough to discuss with the person who will use the report. What does the country field represent? It could mean where the client is located, or where the intended audience lives. Does the budget figure represent an approved spending limit, or money already spent?<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">These questions are easier to resolve before there are months of imports to contend with. Rename unclear fields now, while relatively little depends on them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once you are satisfied with the structure, create validation within a simple Ruby class. Give that class one responsibility: accept a proposed record and either return valid data or explain what was wrong with it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Import a small file with clear rules<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The first release should be fine with a CSV created by hand. Agree with your teammates on the column names and use data they are authorised to access.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ruby&#8217;s CSV library <a href=\"https:\/\/gist.github.com\/chris-roerig\/39bbf6191c6dc1b8d44ad2c705ba7932\">supports<\/a> reading rows with headers through <code>CSV.foreach(path, headers: true)<\/code>. Add the <code>csv<\/code> gem as a project dependency and load it with <code>require \"csv\"<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Validate each row before storing it. If a row fails, record its number and the reason in an import report. A \u201cmissing currency\u201d message tells the user what to correct. A general failure message gives them very little to work with.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Numeric columns need special consideration. A missing spending value indicates that the amount is unknown. A zero indicates that no spend was recorded. Convert a blank to zero and you have quietly turned an unanswered question into a figure that looks complete.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For this project, expect dates in year-month-day format and budgets in whole minor currency units. Reject other formats with an explanation. Silently interpreting an unexpected date or amount means making an assumption on behalf of the person who supplied the file.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep a copy of the original file and assign a reference to each import. That gives you a source to return to when someone asks why a reported value has changed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Keep planned activity separate from measured results<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Record a service order, a budget and an observed result separately.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">An order states what was requested. A measurement records what a source reported at a particular point in time. Linking the two can help someone investigate a campaign. It does not establish that the order caused a change in the measurement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Suppose an account gains followers during a campaign. Your team has also published a video that attracted attention that week. Both activities appear in the tracker, but the tracker cannot tell you which one accounted for each additional follower.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Each measurement in the Ruby model should carry a metric name, a measured value, an observation time and the source. Where it covers a period, add the start and end of that period.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You also need to distinguish an event from a snapshot. A recorded enquiry is an event. An account&#8217;s follower count at a particular time is a snapshot.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Adding Monday&#8217;s follower count to Friday&#8217;s gives a meaningless total. Comparing the two gives the net change between those observations. Put that rule in the reporting logic, where it can be checked, rather than leaving it for somebody to remember each week.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Make repeated imports predictable<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">At some point, a duplicate file will be uploaded. Before that happens, decide how the application should respond.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A checksum can recognise an unchanged file that was imported previously. When the same data appears in several different exports, each record also needs a way to identify it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Where possible, use a record identifier supplied by the source. If none is available, choose a key from fields that identify the measurement in your dataset. Source, account, metric and observation time could potentially serve that purpose.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Confirm those assumptions against the actual data. A source may legitimately supply several measurements with identical timestamps. A key that merges them into one record would lose information.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">As soon as you add a database, place a unique constraint on the field or fields you have selected as the record&#8217;s identity. Consider corrections too. A revised measurement needs to be recognised as an update, with earlier values retained where the report requires an audit history.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is a good point to try a deliberately repeated import. The useful question is whether the report stays consistent after the second attempt.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Write a report that answers a real question<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Ask the reader which decisions they need to make each week. What information will influence those decisions? Their answers should determine the output.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One team may care about spending per campaign, visits to landing pages, enquiries and missing inputs. Another may only need to reconcile approved budgets with actual spending.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep currencies separate unless there is a documented conversion method. Include the reporting period beside each comparison. If information is unavailable, say so in the output.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Plain text is acceptable for the first reports. Identify the campaign, explain where each measurement came from and list any rows rejected during import. These details are useful when someone queries a figure, even after the application has acquired charts and a web interface.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Grow the application when the workflow requires it<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After the importer and report have proved useful, a Rails interface can give the team somewhere to upload files and review errors.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep parsing and reporting logic in separate Ruby objects. The web interface and command-line tool should call the same rules. Maintaining two nearly identical importers makes discrepancies harder to investigate.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Check the situations most likely to damage a report: duplicate files, missing currency, empty amounts, corrected measurements and snapshots whose values might accidentally be added together.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your first version is ready when a colleague can import a file, understand which rows were rejected and trace each reported value to its source. You will then have a useful application to work with, and real feedback to guide the next feature.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Campaign spreadsheets get messy very fast. One person tracks followers, another tracks video views, and somebody else has the monthly service charge tucked away in a column. All of it relates to the same campaign, although the figures need different treatment. That is a reasonable starting point for a Ruby project: a small program that [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":1657,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[],"class_list":["post-1655","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ruby-tips"],"blocksy_meta":{"styles_descriptor":{"styles":{"desktop":"","tablet":"","mobile":""},"google_fonts":[],"version":7}},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Building a Social Campaign Tracker in Ruby - Ruby-Doc Learn<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building a Social Campaign Tracker in Ruby - Ruby-Doc Learn\" \/>\n<meta property=\"og:description\" content=\"Campaign spreadsheets get messy very fast. One person tracks followers, another tracks video views, and somebody else has the monthly service charge tucked away in a column. All of it relates to the same campaign, although the figures need different treatment. That is a reasonable starting point for a Ruby project: a small program that [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/\" \/>\n<meta property=\"og:site_name\" content=\"Ruby-Doc Learn\" \/>\n<meta property=\"article:published_time\" content=\"2026-09-21T10:12:59+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-21T10:13:34+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1672\" \/>\n\t<meta property=\"og:image:height\" content=\"941\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"James Britt\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"James Britt\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/\"},\"author\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/james-britt\\\/#james-britt\"},\"headline\":\"Building a Social Campaign Tracker in Ruby\",\"datePublished\":\"2026-09-21T10:12:59+00:00\",\"dateModified\":\"2026-09-21T10:13:34+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/\"},\"wordCount\":1285,\"publisher\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/Building-a-Social-Campaign-Tracker-in-Ruby.png\",\"articleSection\":[\"Ruby tips\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/\",\"name\":\"Building a Social Campaign Tracker in Ruby - Ruby-Doc Learn\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/Building-a-Social-Campaign-Tracker-in-Ruby.png\",\"datePublished\":\"2026-09-21T10:12:59+00:00\",\"dateModified\":\"2026-09-21T10:13:34+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/#primaryimage\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/Building-a-Social-Campaign-Tracker-in-Ruby.png\",\"contentUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/Building-a-Social-Campaign-Tracker-in-Ruby.png\",\"width\":1672,\"height\":941},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/building-a-social-campaign-tracker-in-ruby\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Building a Social Campaign Tracker in Ruby\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#website\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/\",\"name\":\"Ruby-Doc Learn\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#organization\",\"name\":\"Ruby-Doc Learn\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/ruby-doc-logo-transparent.png\",\"contentUrl\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/ruby-doc-logo-transparent.png\",\"width\":1650,\"height\":305,\"caption\":\"Ruby-Doc Learn\"},\"image\":{\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/james-britt\\\/#james-britt\",\"name\":\"James Britt\",\"image\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/jgb_self-portrait-20140914.png\",\"description\":\"Creator and long-term maintainer of Ruby-Doc.org, founder of Neurogami, and author of practical Ruby tutorials for Ruby-Doc Learn.\",\"sameAs\":[\"https:\\\/\\\/jamesbritt.com\\\/\",\"https:\\\/\\\/neurogami.com\\\/\",\"https:\\\/\\\/www.oreilly.com\\\/pub\\\/au\\\/2595\",\"https:\\\/\\\/www.rubyevents.org\\\/profiles\\\/james-britt\"],\"url\":\"https:\\\/\\\/ruby-doc.org\\\/learn\\\/james-britt\\\/\",\"jobTitle\":\"Ruby developer and writer\",\"knowsAbout\":[\"Ruby programming language\",\"Ruby documentation\",\"JRuby\",\"Open Sound Control\",\"Software development\"],\"affiliation\":{\"@type\":\"Organization\",\"name\":\"Ruby-Doc.org\",\"url\":\"https:\\\/\\\/ruby-doc.org\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Building a Social Campaign Tracker in Ruby - Ruby-Doc Learn","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/","og_locale":"en_US","og_type":"article","og_title":"Building a Social Campaign Tracker in Ruby - Ruby-Doc Learn","og_description":"Campaign spreadsheets get messy very fast. One person tracks followers, another tracks video views, and somebody else has the monthly service charge tucked away in a column. All of it relates to the same campaign, although the figures need different treatment. That is a reasonable starting point for a Ruby project: a small program that [&hellip;]","og_url":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/","og_site_name":"Ruby-Doc Learn","article_published_time":"2026-09-21T10:12:59+00:00","article_modified_time":"2026-09-21T10:13:34+00:00","og_image":[{"width":1672,"height":941,"url":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby.png","type":"image\/png"}],"author":"James Britt","twitter_card":"summary_large_image","twitter_misc":{"Written by":"James Britt","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/#article","isPartOf":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/"},"author":{"@id":"https:\/\/ruby-doc.org\/learn\/james-britt\/#james-britt"},"headline":"Building a Social Campaign Tracker in Ruby","datePublished":"2026-09-21T10:12:59+00:00","dateModified":"2026-09-21T10:13:34+00:00","mainEntityOfPage":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/"},"wordCount":1285,"publisher":{"@id":"https:\/\/ruby-doc.org\/learn\/#organization"},"image":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/#primaryimage"},"thumbnailUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby.png","articleSection":["Ruby tips"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/","url":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/","name":"Building a Social Campaign Tracker in Ruby - Ruby-Doc Learn","isPartOf":{"@id":"https:\/\/ruby-doc.org\/learn\/#website"},"primaryImageOfPage":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/#primaryimage"},"image":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/#primaryimage"},"thumbnailUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby.png","datePublished":"2026-09-21T10:12:59+00:00","dateModified":"2026-09-21T10:13:34+00:00","breadcrumb":{"@id":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/#primaryimage","url":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby.png","contentUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/Building-a-Social-Campaign-Tracker-in-Ruby.png","width":1672,"height":941},{"@type":"BreadcrumbList","@id":"https:\/\/ruby-doc.org\/learn\/building-a-social-campaign-tracker-in-ruby\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/ruby-doc.org\/learn\/"},{"@type":"ListItem","position":2,"name":"Building a Social Campaign Tracker in Ruby"}]},{"@type":"WebSite","@id":"https:\/\/ruby-doc.org\/learn\/#website","url":"https:\/\/ruby-doc.org\/learn\/","name":"Ruby-Doc Learn","description":"","publisher":{"@id":"https:\/\/ruby-doc.org\/learn\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/ruby-doc.org\/learn\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/ruby-doc.org\/learn\/#organization","name":"Ruby-Doc Learn","url":"https:\/\/ruby-doc.org\/learn\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/ruby-doc.org\/learn\/#\/schema\/logo\/image\/","url":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/ruby-doc-logo-transparent.png","contentUrl":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/ruby-doc-logo-transparent.png","width":1650,"height":305,"caption":"Ruby-Doc Learn"},"image":{"@id":"https:\/\/ruby-doc.org\/learn\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/ruby-doc.org\/learn\/james-britt\/#james-britt","name":"James Britt","image":"https:\/\/ruby-doc.org\/learn\/wp-content\/uploads\/2026\/09\/jgb_self-portrait-20140914.png","description":"Creator and long-term maintainer of Ruby-Doc.org, founder of Neurogami, and author of practical Ruby tutorials for Ruby-Doc Learn.","sameAs":["https:\/\/jamesbritt.com\/","https:\/\/neurogami.com\/","https:\/\/www.oreilly.com\/pub\/au\/2595","https:\/\/www.rubyevents.org\/profiles\/james-britt"],"url":"https:\/\/ruby-doc.org\/learn\/james-britt\/","jobTitle":"Ruby developer and writer","knowsAbout":["Ruby programming language","Ruby documentation","JRuby","Open Sound Control","Software development"],"affiliation":{"@type":"Organization","name":"Ruby-Doc.org","url":"https:\/\/ruby-doc.org\/"}}]}},"_links":{"self":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts\/1655","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/comments?post=1655"}],"version-history":[{"count":2,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts\/1655\/revisions"}],"predecessor-version":[{"id":1659,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/posts\/1655\/revisions\/1659"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/media\/1657"}],"wp:attachment":[{"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/media?parent=1655"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/categories?post=1655"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/ruby-doc.org\/learn\/wp-json\/wp\/v2\/tags?post=1655"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}