a helper for quick and dirty reporting on paid social creatives

version 1 with pretty print

stunwin.com c09e169f

+327
+23
.github/workflows/test.yml
··· 1 + name: test 2 + 3 + on: 4 + push: 5 + branches: 6 + - master 7 + - main 8 + pull_request: 9 + 10 + jobs: 11 + test: 12 + runs-on: ubuntu-latest 13 + steps: 14 + - uses: actions/checkout@v4 15 + - uses: erlef/setup-beam@v1 16 + with: 17 + otp-version: "28" 18 + gleam-version: "1.14.0" 19 + rebar3-version: "3" 20 + # elixir-version: "1" 21 + - run: gleam deps download 22 + - run: gleam test 23 + - run: gleam format --check src test
+4
.gitignore
··· 1 + *.beam 2 + *.ez 3 + /build 4 + erl_crash.dump
+24
README.md
··· 1 + # gleport 2 + 3 + [![Package Version](https://img.shields.io/hexpm/v/gleport)](https://hex.pm/packages/gleport) 4 + [![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/gleport/) 5 + 6 + ```sh 7 + gleam add gleport@1 8 + ``` 9 + ```gleam 10 + import gleport 11 + 12 + pub fn main() -> Nil { 13 + // TODO: An example of the project in use 14 + } 15 + ``` 16 + 17 + Further documentation can be found at <https://hexdocs.pm/gleport>. 18 + 19 + ## Development 20 + 21 + ```sh 22 + gleam run # Run the project 23 + gleam test # Run the tests 24 + ```
+23
gleam.toml
··· 1 + name = "gleport" 2 + version = "1.0.0" 3 + 4 + # Fill out these fields if you intend to generate HTML documentation or publish 5 + # your project to the Hex package manager. 6 + # 7 + # description = "" 8 + # licences = ["Apache-2.0"] 9 + # repository = { type = "github", user = "", repo = "" } 10 + # links = [{ title = "Website", href = "" }] 11 + # 12 + # For a full reference of all the available options, you can have a look at 13 + # https://gleam.run/writing-gleam/gleam-toml/. 14 + 15 + [dependencies] 16 + gleam_stdlib = ">= 0.44.0 and < 2.0.0" 17 + argv = ">= 1.0.2 and < 2.0.0" 18 + gsv = ">= 5.0.0 and < 6.0.0" 19 + simplifile = ">= 2.3.2 and < 3.0.0" 20 + glam = ">= 2.0.3 and < 3.0.0" 21 + 22 + [dev-dependencies] 23 + gleeunit = ">= 1.0.0 and < 2.0.0"
+22
manifest.toml
··· 1 + # This file was generated by Gleam 2 + # You typically do not need to edit this file 3 + 4 + packages = [ 5 + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, 6 + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 7 + { name = "glam", version = "2.0.3", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "237C2CE218A2A0A5D46D625F8EF5B78F964BC91018B78D692B17E1AB84295229" }, 8 + { name = "gleam_stdlib", version = "0.68.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "EEC7E7A18B8A53B7A28B7F0A2198CE53BAFF05D45479E4806C387EDF26DA842D" }, 9 + { name = "glearray", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "5E272F7CB278CC05A929C58DEB58F5D5AC6DB5B879A681E71138658D0061C38A" }, 10 + { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, 11 + { name = "gsv", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glearray", "splitter"], otp_app = "gsv", source = "hex", outer_checksum = "41B98AA485FADD4D498CD4BEC37DAA221AE20B9667D0350AA310A14997B52049" }, 12 + { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" }, 13 + { name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" }, 14 + ] 15 + 16 + [requirements] 17 + argv = { version = ">= 1.0.2 and < 2.0.0" } 18 + glam = { version = ">= 2.0.3 and < 3.0.0" } 19 + gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 20 + gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 21 + gsv = { version = ">= 5.0.0 and < 6.0.0" } 22 + simplifile = { version = ">= 2.3.2 and < 3.0.0" }
+196
src/gleport.gleam
··· 1 + import glam/doc.{type Document} 2 + import gleam/dict 3 + import gleam/float 4 + import gleam/int 5 + import gleam/io 6 + import gleam/list 7 + import gleam/string 8 + import gsv 9 + import simplifile 10 + 11 + pub type ResultType { 12 + Click 13 + Purchase 14 + } 15 + 16 + pub type AdRow { 17 + AdRow( 18 + name: String, 19 + cost: Float, 20 + results: Int, 21 + result_type: ResultType, 22 + cost_per_result: Float, 23 + ctr: Float, 24 + impressions: Int, 25 + clicks: Int, 26 + ) 27 + } 28 + 29 + pub fn main() { 30 + let assert Ok(file) = simplifile.read("test.csv") 31 + let assert Ok(rows) = gsv.to_dicts(file, ",") 32 + 33 + let ad_rows = 34 + rows 35 + |> list.fold([], build_row_sets) 36 + let ad_names = 37 + list.map(ad_rows, fn(r) { r.name }) 38 + |> list.unique 39 + 40 + let condensed_rows = condense_ad_rows(ad_names, ad_rows) 41 + 42 + adrows_to_tuples(condensed_rows) 43 + |> pretty_print 44 + 45 + adrows_to_tuples(condensed_rows) 46 + |> list.map(dict.from_list) 47 + |> gsv.from_dicts(",", gsv.Unix) 48 + |> simplifile.write(to: "./test_output.csv", contents: _) 49 + // pretty_print(ad_rows) 50 + } 51 + 52 + fn pretty_print(adrows: List(List(#(String, String)))) { 53 + adrows 54 + |> list.map(to_documents) 55 + |> doc.concat_join(with: [doc.lines(2)]) 56 + |> doc.to_string(80) 57 + |> io.println 58 + } 59 + 60 + fn to_documents(list: List(#(String, String))) { 61 + list 62 + |> list.map(fn(field: #(String, String)) { 63 + doc.from_string(field.0 <> ": " <> field.1) 64 + }) 65 + |> doc.concat_join(with: [doc.line]) 66 + } 67 + 68 + // name: String, 69 + // cost: Float, 70 + // results: Int, 71 + // result_type: ResultType, 72 + // cost_per_result: Float, 73 + // ctr: Int, 74 + // impressions: Int, 75 + // clicks: Int 76 + fn adrows_to_tuples(list: List(AdRow)) -> List(List(#(String, String))) { 77 + list 78 + |> list.map(fn(row) { 79 + [ 80 + #("name", row.name), 81 + #("cost", "$" <> row.cost |> float.to_precision(2) |> float.to_string), 82 + #("results", row.results |> int.to_string |> add_commas), 83 + #("result type", row.result_type |> reverse_set_result_type), 84 + #( 85 + "cost per result", 86 + "$" <> row.cost_per_result |> float.to_precision(2) |> float.to_string, 87 + ), 88 + #("ctr", row.ctr |> float.to_precision(2) |> float.to_string <> "%"), 89 + #("impressions", row.impressions |> int.to_string |> add_commas), 90 + #("clicks", row.clicks |> int.to_string |> add_commas), 91 + ] 92 + }) 93 + } 94 + 95 + // pub fn pretty_print(input: List(AdRow)) -> Nil { 96 + // let fields = ["Ad Name", "Cost", "Results", "Cost Per", "CTR", "Impressions"] 97 + // } 98 + 99 + fn add_commas(input: String) -> String { 100 + input 101 + |> string.to_graphemes 102 + |> list.reverse 103 + |> list.sized_chunk(3) 104 + |> list.intersperse([","]) 105 + |> list.flatten 106 + |> list.reverse 107 + |> string.concat 108 + } 109 + 110 + pub fn condense_ad_rows( 111 + ad_names: List(String), 112 + full_list: List(AdRow), 113 + ) -> List(AdRow) { 114 + use ad <- list.map(ad_names) 115 + let filtered = 116 + full_list 117 + |> list.filter(fn(x) { x.name == ad }) 118 + |> build_condensed_ad_row 119 + } 120 + 121 + fn build_condensed_ad_row(ad_rows: List(AdRow)) -> AdRow { 122 + let assert [first] = list.take(ad_rows, 1) 123 + let name = first.name 124 + let cost = list.fold(ad_rows, 0.0, fn(acc, r) { acc +. r.cost }) 125 + let results = list.fold(ad_rows, 0, fn(acc, r) { acc + r.results }) 126 + let result_type = first.result_type 127 + let cost_per_result = cost /. int.to_float(results) 128 + let clicks = list.fold(ad_rows, 0, fn(acc, r) { acc + r.clicks }) 129 + let impressions = list.fold(ad_rows, 0, fn(acc, r) { acc + r.impressions }) 130 + let ctr = int.to_float(clicks) /. int.to_float(impressions) 131 + 132 + AdRow( 133 + name:, 134 + cost:, 135 + results:, 136 + result_type:, 137 + cost_per_result:, 138 + clicks:, 139 + impressions:, 140 + ctr:, 141 + ) 142 + } 143 + 144 + fn build_row_sets( 145 + list: List(AdRow), 146 + dict: dict.Dict(String, String), 147 + ) -> List(AdRow) { 148 + let new_row = 149 + AdRow( 150 + name: quick_dict(dict, "Ad name"), 151 + cost: quick_dict(dict, "Amount spent (USD)") |> quick_float, 152 + results: quick_dict(dict, "Results") |> quick_int, 153 + result_type: set_result_type(dict), 154 + cost_per_result: quick_dict(dict, "Cost per results") |> quick_float, 155 + ctr: quick_dict(dict, "CTR (link click-through rate)") 156 + |> quick_int 157 + |> int.to_float, 158 + impressions: quick_dict(dict, "Impressions") |> quick_int, 159 + clicks: quick_dict(dict, "Link clicks") |> quick_int, 160 + ) 161 + [new_row, ..list] 162 + } 163 + 164 + fn set_result_type(dict: dict.Dict(String, String)) -> ResultType { 165 + case quick_dict(dict, "Result indicator") { 166 + "actions:link_click" -> Click 167 + _ -> Purchase 168 + } 169 + } 170 + 171 + fn reverse_set_result_type(input: ResultType) -> String { 172 + case input { 173 + Click -> "clicks" 174 + _ -> "purchases" 175 + } 176 + } 177 + 178 + fn quick_dict(dict: dict.Dict(a, b), key: a) -> b { 179 + let assert Ok(output) = dict.get(dict, key) 180 + output 181 + } 182 + 183 + fn quick_int(input: String) -> Int { 184 + let assert Ok(output) = int.parse(input) 185 + output 186 + } 187 + 188 + fn quick_float(input: String) -> Float { 189 + case input { 190 + "0" -> 0.0 191 + _ -> { 192 + let assert Ok(output) = float.parse(input) 193 + output 194 + } 195 + } 196 + }
+14
test.csv
··· 1 + Reporting starts,Reporting ends,Ad name,Ad delivery,Attribution setting,Results,Result indicator,Reach,Frequency,Cost per results,Ad set budget,Ad set budget type,Amount spent (USD),Ends,Quality ranking,Engagement rate ranking,Conversion rate ranking,Impressions,"CPM (cost per 1,000 impressions) (USD)",Link clicks,shop_clicks,CPC (cost per link click) (USD),CTR (link click-through rate),Clicks (all),CTR (all),CPC (all) (USD),Landing page views,Cost per landing page view (USD),Ad set ID,Campaign ID 2 + 2025-09-29,2026-01-02,GLGOY AV,not_delivering,7-day click or 1-day view,1332,actions:link_click,26774,1.396504,0.26572072,Using campaign budget,0,353.94,2025-12-31,-,-,-,37390,9.466167,1332,0.265721,3.56245,2304,6.162075,0.15362,579,0.611295,1.20238333856401E+017,1.20238333856391E+017, 3 + 2025-09-29,2026-01-02,GLGOY AV,inactive,7-day click or 1-day view,170,actions:link_click,3271,1.314277,0.341,Using campaign budget,0,57.97,2025-12-31,-,-,-,4299,13.484531,170,0.341,3.954408,297,6.908583,0.195185,113,0.513009,1.20238468587391E+017,1.20238333856391E+017, 4 + 2025-09-29,2026-01-02,GLGOY Static Vincentians,inactive,7-day click or 1-day view,20,actions:link_click,2471,1.0862,0.283,Using campaign budget,0,5.66,2025-12-31,-,-,-,2684,2.108793,20,0.283,0.745156,21,0.782414,0.269524,10,0.566,1.20238468587391E+017,1.20238333856391E+017, 5 + 2025-09-29,2026-01-02,GLGOY Static Thrift Store,inactive,7-day click or 1-day view,3,actions:link_click,208,1.509615,0.81666667,Using campaign budget,0,2.45,2025-12-31,-,-,-,314,7.802548,3,0.816667,0.955414,7,2.229299,0.35,3,0.816667,1.20238468587391E+017,1.20238333856391E+017, 6 + 2025-09-29,2026-01-02,GLGOY Static camp,inactive,7-day click or 1-day view,11,actions:link_click,813,1.03813,0.27545455,Using campaign budget,0,3.03,2025-12-31,-,-,-,844,3.590047,11,0.275455,1.303318,13,1.540284,0.233077,8,0.37875,1.20238468587391E+017,1.20238333856391E+017, 7 + 2025-09-29,2026-01-02,GLGOY Static cmc,inactive,7-day click or 1-day view,16,actions:link_click,414,1.123188,0.136875,Using campaign budget,0,2.19,2025-12-31,-,-,-,465,4.709677,16,0.136875,3.44086,19,4.086022,0.115263,10,0.219,1.20238468587391E+017,1.20238333856391E+017, 8 + 2025-09-29,2026-01-02,GLGOY Static camp,not_delivering,7-day click or 1-day view,12326,actions:link_click,173180,2.066832,0.19477771,Using campaign budget,0,2400.83,2025-12-31,-,-,-,357934,6.707466,12326,0.194778,3.443652,13697,3.826683,0.175281,4295,0.558983,1.20238333856401E+017,1.20238333856391E+017, 9 + 2025-09-29,2026-01-02,GLGOY Static cmc,not_delivering,7-day click or 1-day view,4196,actions:link_click,56306,2.027049,0.1985081,Using campaign budget,0,832.94,2025-12-31,-,-,-,114135,7.297849,4196,0.198508,3.676348,4759,4.169624,0.175024,1455,0.572467,1.20238333856401E+017,1.20238333856391E+017, 10 + 2025-09-29,2026-01-02,GLGOY Static Vincentians,inactive,7-day click or 1-day view,6,actions:link_click,422,1.253555,0.46333333,Using campaign budget,0,2.78,2025-12-31,-,-,-,529,5.255198,6,0.463333,1.134216,7,1.323251,0.397143,2,1.39,1.20238333856401E+017,1.20238333856391E+017, 11 + 2025-09-29,2026-01-02,GLGOY Static Thrift Store,not_delivering,7-day click or 1-day view,196,actions:link_click,12494,1.114855,0.86127551,Using campaign budget,0,168.81,2025-12-31,-,-,-,13929,12.119319,196,0.861276,1.407136,269,1.931223,0.627546,92,1.834891,1.20238333856401E+017,1.20238333856391E+017, 12 + 2025-09-29,2026-01-02,Promoting lead generation: Lead Gen,not_delivering,7-day click or 1-day view,924,actions:leadgen.other,109764,3.153812,5.64571429,Using campaign budget,0,5216.64,2026-01-01,Above average,Average,Above average,346175,15.069372,5887,0.886129,1.700585,8994,2.598108,0.580013,418,12.48,1.20239137661141E+017,1.20239137661161E+017, 13 + 2025-09-29,2026-01-02,GLGOY Briana,not_delivering,7-day click or 1-day view,609,actions:link_click,10684,1.334519,0.18,Using campaign budget,0,109.62,2025-12-31,Below average - Bottom 35% of ads,Above average,Above average,14258,7.688315,609,0.18,4.271286,986,6.915416,0.111176,446,0.245785,1.20238468587391E+017,1.20238333856391E+017, 14 + 2025-09-29,2026-01-02,GLGOY Briana,not_delivering,7-day click or 1-day view,1755,actions:link_click,22630,1.603535,0.13254131,Using campaign budget,0,232.61,2025-12-31,Below average - Bottom 35% of ads,Above average,Above average,36288,6.410108,1755,0.132541,4.83631,2183,6.015763,0.106555,1278,0.182011,1.20238333856401E+017,1.20238333856391E+017,
+13
test/gleport_test.gleam
··· 1 + import gleeunit 2 + 3 + pub fn main() -> Nil { 4 + gleeunit.main() 5 + } 6 + 7 + // gleeunit test functions end in `_test` 8 + pub fn hello_world_test() { 9 + let name = "Joe" 10 + let greeting = "Hello, " <> name <> "!" 11 + 12 + assert greeting == "Hello, Joe!" 13 + }
+8
test_output.csv
··· 1 + clicks,cost,cost per result,ctr,impressions,name,result type,results 2 + "2,364",$342.23,$0.14,0.05%,"50,546",GLGOY Briana,clicks,"2,364" 3 + "5,887",$5216.64,$5.65,0.02%,"346,175",Promoting lead generation: Lead Gen,purchases,924 4 + 199,$171.26,$0.86,0.01%,"14,243",GLGOY Static Thrift Store,clicks,199 5 + 26,$8.44,$0.32,0.01%,"3,213",GLGOY Static Vincentians,clicks,26 6 + "4,212",$835.13,$0.2,0.04%,"114,600",GLGOY Static cmc,clicks,"4,212" 7 + "12,337",$2403.86,$0.19,0.03%,"358,778",GLGOY Static camp,clicks,"12,337" 8 + "1,502",$411.91,$0.27,0.04%,"41,689",GLGOY AV,clicks,"1,502"